diff --git a/TEST_MAPPING b/TEST_MAPPING index c45916b0e104b33b57d1ef5d334e0b1b9e468133..1098296c17b43f55d21f44e616bdcd9cedde14de 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -45,5 +45,22 @@ { "path": "packages/modules/NetworkStack" } + ], + "imports": [ + { + "path": "frameworks/base/core/java/android/net" + }, + { + "path": "packages/modules/NetworkStack" + }, + { + "path": "packages/modules/CaptivePortalLogin" + }, + { + "path": "packages/modules/Connectivity" + }, + { + "path": "packages/modules/Connectivity/Tethering" + } ] } diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp index d868f39c98f3f6267d9d2efc5e96c853d6f970ac..fce436079beaca62dfc4960454f27314009fc3fd 100644 --- a/Tethering/common/TetheringLib/Android.bp +++ b/Tethering/common/TetheringLib/Android.bp @@ -21,7 +21,6 @@ java_sdk_library { name: "framework-tethering", defaults: ["framework-module-defaults"], impl_library_visibility: [ - "//frameworks/base/packages/Tethering:__subpackages__", "//packages/modules/Connectivity/Tethering:__subpackages__", ], @@ -30,7 +29,7 @@ java_sdk_library { stub_only_libs: ["framework-connectivity.stubs.module_lib"], aidl: { include_dirs: [ - "frameworks/base/packages/Connectivity/framework/aidl-export", + "packages/modules/Connectivity/framework/aidl-export", ], }, diff --git a/framework/Android.bp b/framework/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..ee71e154dc9945cc3beae224530cb65c586c2f9a --- /dev/null +++ b/framework/Android.bp @@ -0,0 +1,161 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +filegroup { + name: "framework-connectivity-internal-sources", + srcs: [ + "src/**/*.java", + "src/**/*.aidl", + ], + path: "src", + visibility: [ + "//visibility:private", + ], +} + +filegroup { + name: "framework-connectivity-aidl-export-sources", + srcs: [ + "aidl-export/**/*.aidl", + ], + path: "aidl-export", + visibility: [ + "//visibility:private", + ], +} + +// TODO: use a java_library in the bootclasspath instead +filegroup { + name: "framework-connectivity-sources", + srcs: [ + ":framework-connectivity-internal-sources", + ":framework-connectivity-aidl-export-sources", + ], + visibility: [ + "//frameworks/base", + "//packages/modules/Connectivity:__subpackages__", + ], +} + +java_library { + name: "framework-connectivity-annotations", + sdk_version: "module_current", + srcs: [ + "src/android/net/ConnectivityAnnotations.java", + ], + libs: [ + "framework-annotations-lib", + "framework-connectivity", + ], + visibility: [ + "//frameworks/base:__subpackages__", + "//packages/modules/Connectivity:__subpackages__", + ], +} + +java_sdk_library { + name: "framework-connectivity", + sdk_version: "module_current", + min_sdk_version: "30", + defaults: ["framework-module-defaults"], + installable: true, + srcs: [ + ":framework-connectivity-sources", + ":net-utils-framework-common-srcs", + ], + aidl: { + include_dirs: [ + // Include directories for parcelables that are part of the stable API, and need a + // one-line "parcelable X" .aidl declaration to be used in AIDL interfaces. + // TODO(b/180293679): remove these dependencies as they should not be necessary once + // the module builds against API (the parcelable declarations exist in framework.aidl) + "frameworks/base/core/java", // For framework parcelables + "frameworks/native/aidl/binder", // For PersistableBundle.aidl + ], + }, + impl_only_libs: [ + // TODO (b/183097033) remove once module_current includes core_platform + "stable.core.platform.api.stubs", + "framework-tethering.stubs.module_lib", + "framework-wifi.stubs.module_lib", + "net-utils-device-common", + ], + libs: [ + "unsupportedappusage", + ], + jarjar_rules: "jarjar-rules.txt", + permitted_packages: ["android.net"], + impl_library_visibility: [ + "//packages/modules/Connectivity/Tethering/apex", + // In preparation for future move + "//packages/modules/Connectivity/apex", + "//packages/modules/Connectivity/service", + "//frameworks/base/packages/Connectivity/service", + "//frameworks/base", + + // Tests using hidden APIs + "//cts/tests/netlegacy22.api", + "//external/sl4a:__subpackages__", + "//frameworks/base/packages/Connectivity/tests:__subpackages__", + "//frameworks/libs/net/common/testutils", + "//frameworks/libs/net/common/tests:__subpackages__", + "//frameworks/opt/telephony/tests/telephonytests", + "//packages/modules/CaptivePortalLogin/tests", + "//packages/modules/Connectivity/Tethering/tests:__subpackages__", + "//packages/modules/Connectivity/tests:__subpackages__", + "//packages/modules/NetworkStack/tests:__subpackages__", + "//packages/modules/Wifi/service/tests/wifitests", + ], + apex_available: [ + "com.android.tethering", + ], +} + +cc_library_shared { + name: "libframework-connectivity-jni", + min_sdk_version: "30", + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + // Don't warn about S API usage even with + // min_sdk 30: the library is only loaded + // on S+ devices + "-Wno-unguarded-availability", + "-Wthread-safety", + ], + srcs: [ + "jni/android_net_NetworkUtils.cpp", + "jni/onload.cpp", + ], + shared_libs: [ + "libandroid", + "liblog", + "libnativehelper", + ], + header_libs: [ + "dnsproxyd_protocol_headers", + ], + stl: "none", + apex_available: [ + "com.android.tethering", + ], +} diff --git a/framework/aidl-export/android/net/CaptivePortalData.aidl b/framework/aidl-export/android/net/CaptivePortalData.aidl new file mode 100644 index 0000000000000000000000000000000000000000..1d57ee75913602df8711752012640592bb2a6e0c --- /dev/null +++ b/framework/aidl-export/android/net/CaptivePortalData.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +@JavaOnlyStableParcelable parcelable CaptivePortalData; diff --git a/framework/aidl-export/android/net/ConnectivityDiagnosticsManager.aidl b/framework/aidl-export/android/net/ConnectivityDiagnosticsManager.aidl new file mode 100644 index 0000000000000000000000000000000000000000..82ba0ca113c56220529b29f14689a83aec1933d1 --- /dev/null +++ b/framework/aidl-export/android/net/ConnectivityDiagnosticsManager.aidl @@ -0,0 +1,21 @@ +/** + * + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable ConnectivityDiagnosticsManager.ConnectivityReport; +parcelable ConnectivityDiagnosticsManager.DataStallReport; \ No newline at end of file diff --git a/framework/aidl-export/android/net/DhcpInfo.aidl b/framework/aidl-export/android/net/DhcpInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..29cd21fe76522451e8113a2fec68e3d3783d904b --- /dev/null +++ b/framework/aidl-export/android/net/DhcpInfo.aidl @@ -0,0 +1,19 @@ +/** + * 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; + +parcelable DhcpInfo; diff --git a/framework/aidl-export/android/net/IpConfiguration.aidl b/framework/aidl-export/android/net/IpConfiguration.aidl new file mode 100644 index 0000000000000000000000000000000000000000..7a30f0e79cad9b73fd006f9935a46e7e73fbbeb9 --- /dev/null +++ b/framework/aidl-export/android/net/IpConfiguration.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.net; + +parcelable IpConfiguration; diff --git a/framework/aidl-export/android/net/IpPrefix.aidl b/framework/aidl-export/android/net/IpPrefix.aidl new file mode 100644 index 0000000000000000000000000000000000000000..3495efc1f1d033b3514cd4da80f45ef62c81339b --- /dev/null +++ b/framework/aidl-export/android/net/IpPrefix.aidl @@ -0,0 +1,22 @@ +/** + * + * 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 android.net; + +// @JavaOnlyStableParcelable only affects the parcelable when built as stable aidl (aidl_interface +// build rule). +@JavaOnlyStableParcelable parcelable IpPrefix; diff --git a/framework/aidl-export/android/net/KeepalivePacketData.aidl b/framework/aidl-export/android/net/KeepalivePacketData.aidl new file mode 100644 index 0000000000000000000000000000000000000000..d456b53fd188071e494d9ea7f9097c05a437a706 --- /dev/null +++ b/framework/aidl-export/android/net/KeepalivePacketData.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable KeepalivePacketData; diff --git a/framework/aidl-export/android/net/LinkAddress.aidl b/framework/aidl-export/android/net/LinkAddress.aidl new file mode 100644 index 0000000000000000000000000000000000000000..9c804db08d6172546fdee3727426d672961a24f2 --- /dev/null +++ b/framework/aidl-export/android/net/LinkAddress.aidl @@ -0,0 +1,21 @@ +/** + * + * 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; + +@JavaOnlyStableParcelable parcelable LinkAddress; + diff --git a/framework/aidl-export/android/net/LinkProperties.aidl b/framework/aidl-export/android/net/LinkProperties.aidl new file mode 100644 index 0000000000000000000000000000000000000000..a8b3c7b0392fbacd251b89ceadb017e87a4f0113 --- /dev/null +++ b/framework/aidl-export/android/net/LinkProperties.aidl @@ -0,0 +1,20 @@ +/* +** +** 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; + +@JavaOnlyStableParcelable parcelable LinkProperties; diff --git a/framework/aidl-export/android/net/MacAddress.aidl b/framework/aidl-export/android/net/MacAddress.aidl new file mode 100644 index 0000000000000000000000000000000000000000..48a18a7ac821873bc151fbb5feef0f214a146682 --- /dev/null +++ b/framework/aidl-export/android/net/MacAddress.aidl @@ -0,0 +1,20 @@ +/** + * + * 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; + +@JavaOnlyStableParcelable parcelable MacAddress; diff --git a/framework/aidl-export/android/net/Network.aidl b/framework/aidl-export/android/net/Network.aidl new file mode 100644 index 0000000000000000000000000000000000000000..05622025bf33c6dfbbf8019d6fb57964863c45a9 --- /dev/null +++ b/framework/aidl-export/android/net/Network.aidl @@ -0,0 +1,20 @@ +/* +** +** 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 android.net; + +@JavaOnlyStableParcelable parcelable Network; diff --git a/framework/aidl-export/android/net/NetworkAgentConfig.aidl b/framework/aidl-export/android/net/NetworkAgentConfig.aidl new file mode 100644 index 0000000000000000000000000000000000000000..cb70bdd31260557ef1b711d64b62b4e0f47ed22c --- /dev/null +++ b/framework/aidl-export/android/net/NetworkAgentConfig.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.net; + +parcelable NetworkAgentConfig; diff --git a/framework/aidl-export/android/net/NetworkCapabilities.aidl b/framework/aidl-export/android/net/NetworkCapabilities.aidl new file mode 100644 index 0000000000000000000000000000000000000000..01d328605de49c4f49404f55bdc714d4a1fa777b --- /dev/null +++ b/framework/aidl-export/android/net/NetworkCapabilities.aidl @@ -0,0 +1,21 @@ +/* +** +** 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 android.net; + +@JavaOnlyStableParcelable parcelable NetworkCapabilities; + diff --git a/framework/aidl-export/android/net/NetworkInfo.aidl b/framework/aidl-export/android/net/NetworkInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..f501873029669bbf814d92182b3273df90f2d7a4 --- /dev/null +++ b/framework/aidl-export/android/net/NetworkInfo.aidl @@ -0,0 +1,19 @@ +/** + * 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. + */ + +package android.net; + +parcelable NetworkInfo; diff --git a/framework/aidl-export/android/net/NetworkRequest.aidl b/framework/aidl-export/android/net/NetworkRequest.aidl new file mode 100644 index 0000000000000000000000000000000000000000..508defc6b497fbd8ffe0c34abf3f7957b6d8921e --- /dev/null +++ b/framework/aidl-export/android/net/NetworkRequest.aidl @@ -0,0 +1,20 @@ +/** + * 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 android.net; + +parcelable NetworkRequest; + diff --git a/framework/aidl-export/android/net/NetworkScore.aidl b/framework/aidl-export/android/net/NetworkScore.aidl new file mode 100644 index 0000000000000000000000000000000000000000..af12dcf7f17a5304e696e88a472b01ab49276c2f --- /dev/null +++ b/framework/aidl-export/android/net/NetworkScore.aidl @@ -0,0 +1,20 @@ +/** + * 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; + +parcelable NetworkScore; + diff --git a/framework/aidl-export/android/net/OemNetworkPreferences.aidl b/framework/aidl-export/android/net/OemNetworkPreferences.aidl new file mode 100644 index 0000000000000000000000000000000000000000..2b6a4ceef592b7d44ea8957b80a26e879ce39f79 --- /dev/null +++ b/framework/aidl-export/android/net/OemNetworkPreferences.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable OemNetworkPreferences; diff --git a/framework/aidl-export/android/net/ProxyInfo.aidl b/framework/aidl-export/android/net/ProxyInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..a5d0c120e7474fc8678bfc8258658ad83dd4dc43 --- /dev/null +++ b/framework/aidl-export/android/net/ProxyInfo.aidl @@ -0,0 +1,21 @@ +/* +** +** 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; + +@JavaOnlyStableParcelable parcelable ProxyInfo; + diff --git a/framework/aidl-export/android/net/QosFilterParcelable.aidl b/framework/aidl-export/android/net/QosFilterParcelable.aidl new file mode 100644 index 0000000000000000000000000000000000000000..312d6352ee920872b1445f801485f556519fe185 --- /dev/null +++ b/framework/aidl-export/android/net/QosFilterParcelable.aidl @@ -0,0 +1,21 @@ +/* +** +** Copyright (C) 2020 The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.net; + +parcelable QosFilterParcelable; + diff --git a/framework/aidl-export/android/net/QosSession.aidl b/framework/aidl-export/android/net/QosSession.aidl new file mode 100644 index 0000000000000000000000000000000000000000..c2cf36624b554134ca11a440eb59b60da73e6cbc --- /dev/null +++ b/framework/aidl-export/android/net/QosSession.aidl @@ -0,0 +1,21 @@ +/* +** +** Copyright (C) 2020 The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.net; + +parcelable QosSession; + diff --git a/framework/aidl-export/android/net/QosSocketInfo.aidl b/framework/aidl-export/android/net/QosSocketInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..476c0900e23eeaa81da62a41459085b24558c9ee --- /dev/null +++ b/framework/aidl-export/android/net/QosSocketInfo.aidl @@ -0,0 +1,21 @@ +/* +** +** Copyright (C) 2020 The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.net; + +parcelable QosSocketInfo; + diff --git a/framework/aidl-export/android/net/RouteInfo.aidl b/framework/aidl-export/android/net/RouteInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..7af9fdaef3420c2583bbb9f1ea4f8b479b26cdb0 --- /dev/null +++ b/framework/aidl-export/android/net/RouteInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 RouteInfo; diff --git a/framework/aidl-export/android/net/StaticIpConfiguration.aidl b/framework/aidl-export/android/net/StaticIpConfiguration.aidl new file mode 100644 index 0000000000000000000000000000000000000000..8aac701fe7e19f83c5d1a52a4938723cca95ac61 --- /dev/null +++ b/framework/aidl-export/android/net/StaticIpConfiguration.aidl @@ -0,0 +1,20 @@ +/* +** +** 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; + +@JavaOnlyStableParcelable parcelable StaticIpConfiguration; \ No newline at end of file diff --git a/framework/aidl-export/android/net/TestNetworkInterface.aidl b/framework/aidl-export/android/net/TestNetworkInterface.aidl new file mode 100644 index 0000000000000000000000000000000000000000..e1f4f9f794eb626ff591ecd1adb9daa032cb477e --- /dev/null +++ b/framework/aidl-export/android/net/TestNetworkInterface.aidl @@ -0,0 +1,20 @@ +/* + * 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; + +/** @hide */ +parcelable TestNetworkInterface; diff --git a/framework/aidl-export/android/net/apf/ApfCapabilities.aidl b/framework/aidl-export/android/net/apf/ApfCapabilities.aidl new file mode 100644 index 0000000000000000000000000000000000000000..7c4d4c2da4bcec9cfc242fb06ac991d6d5e94a82 --- /dev/null +++ b/framework/aidl-export/android/net/apf/ApfCapabilities.aidl @@ -0,0 +1,20 @@ +/* +** +** 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.apf; + +@JavaOnlyStableParcelable parcelable ApfCapabilities; \ No newline at end of file diff --git a/framework/api/current.txt b/framework/api/current.txt new file mode 100644 index 0000000000000000000000000000000000000000..33f4d14148e099f3913d79b9933c133b7acbc624 --- /dev/null +++ b/framework/api/current.txt @@ -0,0 +1,476 @@ +// Signature format: 2.0 +package android.net { + + public class CaptivePortal implements android.os.Parcelable { + method public int describeContents(); + method public void ignoreNetwork(); + method public void reportCaptivePortalDismissed(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortal> CREATOR; + } + + public class ConnectivityDiagnosticsManager { + method public void registerConnectivityDiagnosticsCallback(@NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback); + method public void unregisterConnectivityDiagnosticsCallback(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback); + } + + public abstract static class ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback { + ctor public ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback(); + method public void onConnectivityReportAvailable(@NonNull android.net.ConnectivityDiagnosticsManager.ConnectivityReport); + method public void onDataStallSuspected(@NonNull android.net.ConnectivityDiagnosticsManager.DataStallReport); + method public void onNetworkConnectivityReported(@NonNull android.net.Network, boolean); + } + + public static final class ConnectivityDiagnosticsManager.ConnectivityReport implements android.os.Parcelable { + ctor public ConnectivityDiagnosticsManager.ConnectivityReport(@NonNull android.net.Network, long, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle); + method public int describeContents(); + method @NonNull public android.os.PersistableBundle getAdditionalInfo(); + method @NonNull public android.net.LinkProperties getLinkProperties(); + method @NonNull public android.net.Network getNetwork(); + method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities(); + method public long getReportTimestamp(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.ConnectivityReport> CREATOR; + field public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = "networkProbesAttempted"; + field public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = "networkProbesSucceeded"; + field public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult"; + field public static final int NETWORK_PROBE_DNS = 4; // 0x4 + field public static final int NETWORK_PROBE_FALLBACK = 32; // 0x20 + field public static final int NETWORK_PROBE_HTTP = 8; // 0x8 + field public static final int NETWORK_PROBE_HTTPS = 16; // 0x10 + field public static final int NETWORK_PROBE_PRIVATE_DNS = 64; // 0x40 + field public static final int NETWORK_VALIDATION_RESULT_INVALID = 0; // 0x0 + field public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2; // 0x2 + field public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3; // 0x3 + field public static final int NETWORK_VALIDATION_RESULT_VALID = 1; // 0x1 + } + + public static final class ConnectivityDiagnosticsManager.DataStallReport implements android.os.Parcelable { + ctor public ConnectivityDiagnosticsManager.DataStallReport(@NonNull android.net.Network, long, int, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkCapabilities, @NonNull android.os.PersistableBundle); + method public int describeContents(); + method public int getDetectionMethod(); + method @NonNull public android.net.LinkProperties getLinkProperties(); + method @NonNull public android.net.Network getNetwork(); + method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities(); + method public long getReportTimestamp(); + method @NonNull public android.os.PersistableBundle getStallDetails(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.ConnectivityDiagnosticsManager.DataStallReport> CREATOR; + field public static final int DETECTION_METHOD_DNS_EVENTS = 1; // 0x1 + field public static final int DETECTION_METHOD_TCP_METRICS = 2; // 0x2 + field public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts"; + field public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS = "tcpMetricsCollectionPeriodMillis"; + field public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate"; + } + + public class ConnectivityManager { + method public void addDefaultNetworkActiveListener(android.net.ConnectivityManager.OnNetworkActiveListener); + method public boolean bindProcessToNetwork(@Nullable android.net.Network); + method @NonNull public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull android.net.IpSecManager.UdpEncapsulationSocket, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); + method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network getActiveNetwork(); + method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getActiveNetworkInfo(); + method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo[] getAllNetworkInfo(); + method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.Network[] getAllNetworks(); + method @Deprecated public boolean getBackgroundDataSetting(); + method @Nullable public android.net.Network getBoundNetworkForProcess(); + method public int getConnectionOwnerUid(int, @NonNull java.net.InetSocketAddress, @NonNull java.net.InetSocketAddress); + method @Nullable public android.net.ProxyInfo getDefaultProxy(); + method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.LinkProperties getLinkProperties(@Nullable android.net.Network); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getMultipathPreference(@Nullable android.net.Network); + method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkCapabilities getNetworkCapabilities(@Nullable android.net.Network); + method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(int); + method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public android.net.NetworkInfo getNetworkInfo(@Nullable android.net.Network); + method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public int getNetworkPreference(); + method @Nullable public byte[] getNetworkWatchlistConfigHash(); + method @Deprecated @Nullable public static android.net.Network getProcessDefaultNetwork(); + method public int getRestrictBackgroundStatus(); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public boolean isActiveNetworkMetered(); + method public boolean isDefaultNetworkActive(); + method @Deprecated public static boolean isNetworkTypeValid(int); + method public void registerBestMatchingNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerNetworkCallback(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent); + method public void releaseNetworkRequest(@NonNull android.app.PendingIntent); + method public void removeDefaultNetworkActiveListener(@NonNull android.net.ConnectivityManager.OnNetworkActiveListener); + method @Deprecated public void reportBadNetwork(@Nullable android.net.Network); + method public void reportNetworkConnectivity(@Nullable android.net.Network, boolean); + method public boolean requestBandwidthUpdate(@NonNull android.net.Network); + method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback); + method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, int); + method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler, int); + method public void requestNetwork(@NonNull android.net.NetworkRequest, @NonNull android.app.PendingIntent); + method @Deprecated public void setNetworkPreference(int); + method @Deprecated public static boolean setProcessDefaultNetwork(@Nullable android.net.Network); + method public void unregisterNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback); + method public void unregisterNetworkCallback(@NonNull android.app.PendingIntent); + field @Deprecated public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"; + field public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL"; + field public static final String ACTION_RESTRICT_BACKGROUND_CHANGED = "android.net.conn.RESTRICT_BACKGROUND_CHANGED"; + field @Deprecated public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + field @Deprecated public static final int DEFAULT_NETWORK_PREFERENCE = 1; // 0x1 + field public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL"; + field public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL"; + field @Deprecated public static final String EXTRA_EXTRA_INFO = "extraInfo"; + field @Deprecated public static final String EXTRA_IS_FAILOVER = "isFailover"; + field public static final String EXTRA_NETWORK = "android.net.extra.NETWORK"; + field @Deprecated public static final String EXTRA_NETWORK_INFO = "networkInfo"; + field public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST"; + field @Deprecated public static final String EXTRA_NETWORK_TYPE = "networkType"; + field public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity"; + field @Deprecated public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork"; + field public static final String EXTRA_REASON = "reason"; + field public static final int MULTIPATH_PREFERENCE_HANDOVER = 1; // 0x1 + field public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 4; // 0x4 + field public static final int MULTIPATH_PREFERENCE_RELIABILITY = 2; // 0x2 + field public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; // 0x1 + field public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; // 0x3 + field public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; // 0x2 + field @Deprecated public static final int TYPE_BLUETOOTH = 7; // 0x7 + field @Deprecated public static final int TYPE_DUMMY = 8; // 0x8 + field @Deprecated public static final int TYPE_ETHERNET = 9; // 0x9 + field @Deprecated public static final int TYPE_MOBILE = 0; // 0x0 + field @Deprecated public static final int TYPE_MOBILE_DUN = 4; // 0x4 + field @Deprecated public static final int TYPE_MOBILE_HIPRI = 5; // 0x5 + field @Deprecated public static final int TYPE_MOBILE_MMS = 2; // 0x2 + field @Deprecated public static final int TYPE_MOBILE_SUPL = 3; // 0x3 + field @Deprecated public static final int TYPE_VPN = 17; // 0x11 + field @Deprecated public static final int TYPE_WIFI = 1; // 0x1 + field @Deprecated public static final int TYPE_WIMAX = 6; // 0x6 + } + + public static class ConnectivityManager.NetworkCallback { + ctor public ConnectivityManager.NetworkCallback(); + ctor public ConnectivityManager.NetworkCallback(int); + method public void onAvailable(@NonNull android.net.Network); + method public void onBlockedStatusChanged(@NonNull android.net.Network, boolean); + method public void onCapabilitiesChanged(@NonNull android.net.Network, @NonNull android.net.NetworkCapabilities); + method public void onLinkPropertiesChanged(@NonNull android.net.Network, @NonNull android.net.LinkProperties); + method public void onLosing(@NonNull android.net.Network, int); + method public void onLost(@NonNull android.net.Network); + method public void onUnavailable(); + field public static final int FLAG_INCLUDE_LOCATION_INFO = 1; // 0x1 + } + + public static interface ConnectivityManager.OnNetworkActiveListener { + method public void onNetworkActive(); + } + + public class DhcpInfo implements android.os.Parcelable { + ctor public DhcpInfo(); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.DhcpInfo> CREATOR; + field public int dns1; + field public int dns2; + field public int gateway; + field public int ipAddress; + field public int leaseDuration; + field public int netmask; + field public int serverAddress; + } + + public final class DnsResolver { + method @NonNull public static android.net.DnsResolver getInstance(); + method public void query(@Nullable android.net.Network, @NonNull String, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>); + method public void query(@Nullable android.net.Network, @NonNull String, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super java.util.List<java.net.InetAddress>>); + method public void rawQuery(@Nullable android.net.Network, @NonNull byte[], int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>); + method public void rawQuery(@Nullable android.net.Network, @NonNull String, int, int, int, @NonNull java.util.concurrent.Executor, @Nullable android.os.CancellationSignal, @NonNull android.net.DnsResolver.Callback<? super byte[]>); + field public static final int CLASS_IN = 1; // 0x1 + field public static final int ERROR_PARSE = 0; // 0x0 + field public static final int ERROR_SYSTEM = 1; // 0x1 + field public static final int FLAG_EMPTY = 0; // 0x0 + field public static final int FLAG_NO_CACHE_LOOKUP = 4; // 0x4 + field public static final int FLAG_NO_CACHE_STORE = 2; // 0x2 + field public static final int FLAG_NO_RETRY = 1; // 0x1 + field public static final int TYPE_A = 1; // 0x1 + field public static final int TYPE_AAAA = 28; // 0x1c + } + + public static interface DnsResolver.Callback<T> { + method public void onAnswer(@NonNull T, int); + method public void onError(@NonNull android.net.DnsResolver.DnsException); + } + + public static class DnsResolver.DnsException extends java.lang.Exception { + field public final int code; + } + + public class InetAddresses { + method public static boolean isNumericAddress(@NonNull String); + method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String); + } + + public final class IpPrefix implements android.os.Parcelable { + method public boolean contains(@NonNull java.net.InetAddress); + method public int describeContents(); + method @NonNull public java.net.InetAddress getAddress(); + method @IntRange(from=0, to=128) public int getPrefixLength(); + method @NonNull public byte[] getRawAddress(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.IpPrefix> CREATOR; + } + + public class LinkAddress implements android.os.Parcelable { + method public int describeContents(); + method public java.net.InetAddress getAddress(); + method public int getFlags(); + method @IntRange(from=0, to=128) public int getPrefixLength(); + method public int getScope(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkAddress> CREATOR; + } + + public final class LinkProperties implements android.os.Parcelable { + ctor public LinkProperties(); + method public boolean addRoute(@NonNull android.net.RouteInfo); + method public void clear(); + method public int describeContents(); + method @Nullable public java.net.Inet4Address getDhcpServerAddress(); + method @NonNull public java.util.List<java.net.InetAddress> getDnsServers(); + method @Nullable public String getDomains(); + method @Nullable public android.net.ProxyInfo getHttpProxy(); + method @Nullable public String getInterfaceName(); + method @NonNull public java.util.List<android.net.LinkAddress> getLinkAddresses(); + method public int getMtu(); + method @Nullable public android.net.IpPrefix getNat64Prefix(); + method @Nullable public String getPrivateDnsServerName(); + method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(); + method public boolean isPrivateDnsActive(); + method public boolean isWakeOnLanSupported(); + method public void setDhcpServerAddress(@Nullable java.net.Inet4Address); + method public void setDnsServers(@NonNull java.util.Collection<java.net.InetAddress>); + method public void setDomains(@Nullable String); + method public void setHttpProxy(@Nullable android.net.ProxyInfo); + method public void setInterfaceName(@Nullable String); + method public void setLinkAddresses(@NonNull java.util.Collection<android.net.LinkAddress>); + method public void setMtu(int); + method public void setNat64Prefix(@Nullable android.net.IpPrefix); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.LinkProperties> CREATOR; + } + + public final class MacAddress implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public static android.net.MacAddress fromBytes(@NonNull byte[]); + method @NonNull public static android.net.MacAddress fromString(@NonNull String); + method public int getAddressType(); + method @Nullable public java.net.Inet6Address getLinkLocalIpv6FromEui48Mac(); + method public boolean isLocallyAssigned(); + method public boolean matches(@NonNull android.net.MacAddress, @NonNull android.net.MacAddress); + method @NonNull public byte[] toByteArray(); + method @NonNull public String toOuiString(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.net.MacAddress BROADCAST_ADDRESS; + field @NonNull public static final android.os.Parcelable.Creator<android.net.MacAddress> CREATOR; + field public static final int TYPE_BROADCAST = 3; // 0x3 + field public static final int TYPE_MULTICAST = 2; // 0x2 + field public static final int TYPE_UNICAST = 1; // 0x1 + } + + public class Network implements android.os.Parcelable { + method public void bindSocket(java.net.DatagramSocket) throws java.io.IOException; + method public void bindSocket(java.net.Socket) throws java.io.IOException; + method public void bindSocket(java.io.FileDescriptor) throws java.io.IOException; + method public int describeContents(); + method public static android.net.Network fromNetworkHandle(long); + method public java.net.InetAddress[] getAllByName(String) throws java.net.UnknownHostException; + method public java.net.InetAddress getByName(String) throws java.net.UnknownHostException; + method public long getNetworkHandle(); + method public javax.net.SocketFactory getSocketFactory(); + method public java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException; + method public java.net.URLConnection openConnection(java.net.URL, java.net.Proxy) throws java.io.IOException; + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.Network> CREATOR; + } + + public final class NetworkCapabilities implements android.os.Parcelable { + ctor public NetworkCapabilities(); + ctor public NetworkCapabilities(android.net.NetworkCapabilities); + method public int describeContents(); + method @NonNull public int[] getCapabilities(); + method public int getLinkDownstreamBandwidthKbps(); + method public int getLinkUpstreamBandwidthKbps(); + method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier(); + method public int getOwnerUid(); + method public int getSignalStrength(); + method @Nullable public android.net.TransportInfo getTransportInfo(); + method public boolean hasCapability(int); + method public boolean hasTransport(int); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkCapabilities> CREATOR; + field public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; // 0x11 + field public static final int NET_CAPABILITY_CBS = 5; // 0x5 + field public static final int NET_CAPABILITY_DUN = 2; // 0x2 + field public static final int NET_CAPABILITY_EIMS = 10; // 0xa + field public static final int NET_CAPABILITY_ENTERPRISE = 29; // 0x1d + field public static final int NET_CAPABILITY_FOREGROUND = 19; // 0x13 + field public static final int NET_CAPABILITY_FOTA = 3; // 0x3 + field public static final int NET_CAPABILITY_HEAD_UNIT = 32; // 0x20 + field public static final int NET_CAPABILITY_IA = 7; // 0x7 + field public static final int NET_CAPABILITY_IMS = 4; // 0x4 + field public static final int NET_CAPABILITY_INTERNET = 12; // 0xc + field public static final int NET_CAPABILITY_MCX = 23; // 0x17 + field public static final int NET_CAPABILITY_MMS = 0; // 0x0 + field public static final int NET_CAPABILITY_NOT_CONGESTED = 20; // 0x14 + field public static final int NET_CAPABILITY_NOT_METERED = 11; // 0xb + field public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; // 0xd + field public static final int NET_CAPABILITY_NOT_ROAMING = 18; // 0x12 + field public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; // 0x15 + field public static final int NET_CAPABILITY_NOT_VPN = 15; // 0xf + field public static final int NET_CAPABILITY_RCS = 8; // 0x8 + field public static final int NET_CAPABILITY_SUPL = 1; // 0x1 + field public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; // 0x19 + field public static final int NET_CAPABILITY_TRUSTED = 14; // 0xe + field public static final int NET_CAPABILITY_VALIDATED = 16; // 0x10 + field public static final int NET_CAPABILITY_WIFI_P2P = 6; // 0x6 + field public static final int NET_CAPABILITY_XCAP = 9; // 0x9 + field public static final int SIGNAL_STRENGTH_UNSPECIFIED = -2147483648; // 0x80000000 + field public static final int TRANSPORT_BLUETOOTH = 2; // 0x2 + field public static final int TRANSPORT_CELLULAR = 0; // 0x0 + field public static final int TRANSPORT_ETHERNET = 3; // 0x3 + field public static final int TRANSPORT_LOWPAN = 6; // 0x6 + field public static final int TRANSPORT_USB = 8; // 0x8 + field public static final int TRANSPORT_VPN = 4; // 0x4 + field public static final int TRANSPORT_WIFI = 1; // 0x1 + field public static final int TRANSPORT_WIFI_AWARE = 5; // 0x5 + } + + @Deprecated public class NetworkInfo implements android.os.Parcelable { + ctor @Deprecated public NetworkInfo(int, int, @Nullable String, @Nullable String); + method @Deprecated public int describeContents(); + method @Deprecated @NonNull public android.net.NetworkInfo.DetailedState getDetailedState(); + method @Deprecated public String getExtraInfo(); + method @Deprecated public String getReason(); + method @Deprecated public android.net.NetworkInfo.State getState(); + method @Deprecated public int getSubtype(); + method @Deprecated public String getSubtypeName(); + method @Deprecated public int getType(); + method @Deprecated public String getTypeName(); + method @Deprecated public boolean isAvailable(); + method @Deprecated public boolean isConnected(); + method @Deprecated public boolean isConnectedOrConnecting(); + method @Deprecated public boolean isFailover(); + method @Deprecated public boolean isRoaming(); + method @Deprecated public void setDetailedState(@NonNull android.net.NetworkInfo.DetailedState, @Nullable String, @Nullable String); + method @Deprecated public void writeToParcel(android.os.Parcel, int); + field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkInfo> CREATOR; + } + + @Deprecated public enum NetworkInfo.DetailedState { + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState AUTHENTICATING; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState BLOCKED; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CAPTIVE_PORTAL_CHECK; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTED; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState CONNECTING; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTED; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState DISCONNECTING; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState FAILED; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState IDLE; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState OBTAINING_IPADDR; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SCANNING; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState SUSPENDED; + enum_constant @Deprecated public static final android.net.NetworkInfo.DetailedState VERIFYING_POOR_LINK; + } + + @Deprecated public enum NetworkInfo.State { + enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTED; + enum_constant @Deprecated public static final android.net.NetworkInfo.State CONNECTING; + enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTED; + enum_constant @Deprecated public static final android.net.NetworkInfo.State DISCONNECTING; + enum_constant @Deprecated public static final android.net.NetworkInfo.State SUSPENDED; + enum_constant @Deprecated public static final android.net.NetworkInfo.State UNKNOWN; + } + + public class NetworkRequest implements android.os.Parcelable { + method public boolean canBeSatisfiedBy(@Nullable android.net.NetworkCapabilities); + method public int describeContents(); + method @NonNull public int[] getCapabilities(); + method @Nullable public android.net.NetworkSpecifier getNetworkSpecifier(); + method @NonNull public int[] getTransportTypes(); + method public boolean hasCapability(int); + method public boolean hasTransport(int); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkRequest> CREATOR; + } + + public static class NetworkRequest.Builder { + ctor public NetworkRequest.Builder(); + ctor public NetworkRequest.Builder(@NonNull android.net.NetworkRequest); + method public android.net.NetworkRequest.Builder addCapability(int); + method public android.net.NetworkRequest.Builder addTransportType(int); + method public android.net.NetworkRequest build(); + method @NonNull public android.net.NetworkRequest.Builder clearCapabilities(); + method public android.net.NetworkRequest.Builder removeCapability(int); + method public android.net.NetworkRequest.Builder removeTransportType(int); + method @NonNull public android.net.NetworkRequest.Builder setIncludeOtherUidNetworks(boolean); + method @Deprecated public android.net.NetworkRequest.Builder setNetworkSpecifier(String); + method public android.net.NetworkRequest.Builder setNetworkSpecifier(android.net.NetworkSpecifier); + } + + public class ParseException extends java.lang.RuntimeException { + ctor public ParseException(@NonNull String); + ctor public ParseException(@NonNull String, @NonNull Throwable); + field public String response; + } + + public class ProxyInfo implements android.os.Parcelable { + ctor public ProxyInfo(@Nullable android.net.ProxyInfo); + method public static android.net.ProxyInfo buildDirectProxy(String, int); + method public static android.net.ProxyInfo buildDirectProxy(String, int, java.util.List<java.lang.String>); + method public static android.net.ProxyInfo buildPacProxy(android.net.Uri); + method @NonNull public static android.net.ProxyInfo buildPacProxy(@NonNull android.net.Uri, int); + method public int describeContents(); + method public String[] getExclusionList(); + method public String getHost(); + method public android.net.Uri getPacFileUrl(); + method public int getPort(); + method public boolean isValid(); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.ProxyInfo> CREATOR; + } + + public final class RouteInfo implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.net.IpPrefix getDestination(); + method @Nullable public java.net.InetAddress getGateway(); + method @Nullable public String getInterface(); + method public boolean hasGateway(); + method public boolean isDefaultRoute(); + method public boolean matches(java.net.InetAddress); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.RouteInfo> CREATOR; + } + + public abstract class SocketKeepalive implements java.lang.AutoCloseable { + method public final void close(); + method public final void start(@IntRange(from=0xa, to=0xe10) int); + method public final void stop(); + field public static final int ERROR_HARDWARE_ERROR = -31; // 0xffffffe1 + field public static final int ERROR_INSUFFICIENT_RESOURCES = -32; // 0xffffffe0 + field public static final int ERROR_INVALID_INTERVAL = -24; // 0xffffffe8 + field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb + field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9 + field public static final int ERROR_INVALID_NETWORK = -20; // 0xffffffec + field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea + field public static final int ERROR_INVALID_SOCKET = -25; // 0xffffffe7 + field public static final int ERROR_SOCKET_NOT_IDLE = -26; // 0xffffffe6 + field public static final int ERROR_UNSUPPORTED = -30; // 0xffffffe2 + } + + public static class SocketKeepalive.Callback { + ctor public SocketKeepalive.Callback(); + method public void onDataReceived(); + method public void onError(int); + method public void onStarted(); + method public void onStopped(); + } + + public interface TransportInfo { + } + +} + diff --git a/framework/api/lint-baseline.txt b/framework/api/lint-baseline.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f4004ab723d08cac3e5b930c936685927365181 --- /dev/null +++ b/framework/api/lint-baseline.txt @@ -0,0 +1,4 @@ +// Baseline format: 1.0 +VisiblySynchronized: android.net.NetworkInfo#toString(): + Internal locks must not be exposed (synchronizing on this or class is still + externally observable): method android.net.NetworkInfo.toString() diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt new file mode 100644 index 0000000000000000000000000000000000000000..6c454bcd4cd78d5c1702d1cfb2875afb92030981 --- /dev/null +++ b/framework/api/module-lib-current.txt @@ -0,0 +1,179 @@ +// Signature format: 2.0 +package android.net { + + public final class ConnectivityFrameworkInitializer { + method public static void registerServiceWrappers(); + } + + public class ConnectivityManager { + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void factoryReset(); + method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots(); + method @Nullable public android.net.ProxyInfo getGlobalProxy(); + method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange(); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void requestBackgroundNetwork(@NonNull android.net.NetworkRequest, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler); + method @Deprecated public boolean requestRouteToHostAddress(int, java.net.InetAddress); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptPartialConnectivity(@NonNull android.net.Network, boolean, boolean); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAcceptUnvalidated(@NonNull android.net.Network, boolean, boolean); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setAvoidUnvalidated(@NonNull android.net.Network); + method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setGlobalProxy(@Nullable android.net.ProxyInfo); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setLegacyLockdownVpnEnabled(boolean); + method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void setProfileNetworkPreference(@NonNull android.os.UserHandle, int, @Nullable java.util.concurrent.Executor, @Nullable Runnable); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void setRequireVpnForUids(boolean, @NonNull java.util.Collection<android.util.Range<java.lang.Integer>>); + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_STACK}) public void simulateDataStall(int, long, @NonNull android.net.Network, @NonNull android.os.PersistableBundle); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void startCaptivePortalApp(@NonNull android.net.Network); + method public void systemReady(); + field public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE"; + field public static final String ACTION_PROMPT_LOST_VALIDATION = "android.net.action.PROMPT_LOST_VALIDATION"; + field public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = "android.net.action.PROMPT_PARTIAL_CONNECTIVITY"; + field public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED"; + field public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 262144; // 0x40000 + field public static final int BLOCKED_METERED_REASON_DATA_SAVER = 65536; // 0x10000 + field public static final int BLOCKED_METERED_REASON_MASK = -65536; // 0xffff0000 + field public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 131072; // 0x20000 + field public static final int BLOCKED_REASON_APP_STANDBY = 4; // 0x4 + field public static final int BLOCKED_REASON_BATTERY_SAVER = 1; // 0x1 + field public static final int BLOCKED_REASON_DOZE = 2; // 0x2 + field public static final int BLOCKED_REASON_LOCKDOWN_VPN = 16; // 0x10 + field public static final int BLOCKED_REASON_NONE = 0; // 0x0 + field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8 + field public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; // 0x0 + field public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; // 0x1 + } + + public static class ConnectivityManager.NetworkCallback { + method public void onBlockedStatusChanged(@NonNull android.net.Network, int); + } + + public class ConnectivitySettingsManager { + method public static void clearGlobalProxy(@NonNull android.content.Context); + method @NonNull public static java.util.Set<java.lang.String> getAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context); + method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context); + method public static int getCaptivePortalMode(@NonNull android.content.Context, int); + method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration); + method @NonNull public static android.util.Range<java.lang.Integer> getDnsResolverSampleRanges(@NonNull android.content.Context); + method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int); + method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context); + method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static boolean getMobileDataAlwaysOn(@NonNull android.content.Context, boolean); + method @NonNull public static java.util.Set<java.lang.Integer> getMobileDataPreferredUids(@NonNull android.content.Context); + method public static int getNetworkAvoidBadWifi(@NonNull android.content.Context); + method @Nullable public static String getNetworkMeteredMultipathPreference(@NonNull android.content.Context); + method public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, int); + method @NonNull public static java.time.Duration getNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration); + method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context); + method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context); + method public static int getPrivateDnsMode(@NonNull android.content.Context); + method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean); + method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static void setAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.String>); + method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String); + method public static void setCaptivePortalMode(@NonNull android.content.Context, int); + method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static void setDnsResolverSampleRanges(@NonNull android.content.Context, @NonNull android.util.Range<java.lang.Integer>); + method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int); + method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo); + method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean); + method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.Integer>); + method public static void setNetworkAvoidBadWifi(@NonNull android.content.Context, int); + method public static void setNetworkMeteredMultipathPreference(@NonNull android.content.Context, @NonNull String); + method public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull android.content.Context, @IntRange(from=0) int); + method public static void setNetworkSwitchNotificationRateDuration(@NonNull android.content.Context, @NonNull java.time.Duration); + method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int); + method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String); + method public static void setPrivateDnsMode(@NonNull android.content.Context, int); + method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean); + method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); + field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2 + field public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; // 0x0 + field public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; // 0x1 + field public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; // 0x2 + field public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; // 0x0 + field public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; // 0x1 + field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1 + field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2 + field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3 + } + + public final class NetworkAgentConfig implements android.os.Parcelable { + method @Nullable public String getSubscriberId(); + method public boolean isBypassableVpn(); + } + + public static final class NetworkAgentConfig.Builder { + method @NonNull public android.net.NetworkAgentConfig.Builder setBypassableVpn(boolean); + method @NonNull public android.net.NetworkAgentConfig.Builder setSubscriberId(@Nullable String); + } + + public final class NetworkCapabilities implements android.os.Parcelable { + method @Nullable public java.util.Set<android.util.Range<java.lang.Integer>> getUids(); + method public boolean hasForbiddenCapability(int); + field public static final long REDACT_ALL = -1L; // 0xffffffffffffffffL + field public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1L; // 0x1L + field public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 2L; // 0x2L + field public static final long REDACT_FOR_NETWORK_SETTINGS = 4L; // 0x4L + field public static final long REDACT_NONE = 0L; // 0x0L + field public static final int TRANSPORT_TEST = 7; // 0x7 + } + + public static final class NetworkCapabilities.Builder { + method @NonNull public android.net.NetworkCapabilities.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>); + } + + public class NetworkRequest implements android.os.Parcelable { + method @NonNull public int[] getForbiddenCapabilities(); + method public boolean hasForbiddenCapability(int); + } + + public static class NetworkRequest.Builder { + method @NonNull public android.net.NetworkRequest.Builder addForbiddenCapability(int); + method @NonNull public android.net.NetworkRequest.Builder removeForbiddenCapability(int); + method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>); + } + + public final class TestNetworkInterface implements android.os.Parcelable { + ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String); + method public int describeContents(); + method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor(); + method @NonNull public String getInterfaceName(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR; + } + + public class TestNetworkManager { + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTapInterface(); + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>); + method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder); + method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void teardownTestNetwork(@NonNull android.net.Network); + field public static final String TEST_TAP_PREFIX = "testtap"; + } + + public final class TestNetworkSpecifier extends android.net.NetworkSpecifier implements android.os.Parcelable { + ctor public TestNetworkSpecifier(@NonNull String); + method public int describeContents(); + method @Nullable public String getInterfaceName(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkSpecifier> CREATOR; + } + + public interface TransportInfo { + method public default long getApplicableRedactions(); + method @NonNull public default android.net.TransportInfo makeCopy(long); + } + + public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo { + ctor public VpnTransportInfo(int, @Nullable String); + method public int describeContents(); + method @Nullable public String getSessionId(); + method public int getType(); + method @NonNull public android.net.VpnTransportInfo makeCopy(long); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.VpnTransportInfo> CREATOR; + } + +} + diff --git a/framework/api/module-lib-removed.txt b/framework/api/module-lib-removed.txt new file mode 100644 index 0000000000000000000000000000000000000000..d802177e249b3f97128699222e65c35e57ba7540 --- /dev/null +++ b/framework/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/framework/api/removed.txt b/framework/api/removed.txt new file mode 100644 index 0000000000000000000000000000000000000000..303a1e6173baf7a73d57b8b75e850eed386a161a --- /dev/null +++ b/framework/api/removed.txt @@ -0,0 +1,11 @@ +// Signature format: 2.0 +package android.net { + + public class ConnectivityManager { + method @Deprecated public boolean requestRouteToHost(int, int); + method @Deprecated public int startUsingNetworkFeature(int, String); + method @Deprecated public int stopUsingNetworkFeature(int, String); + } + +} + diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt new file mode 100644 index 0000000000000000000000000000000000000000..d1d51da151bf8601d3deb8510c8ca57c13675906 --- /dev/null +++ b/framework/api/system-current.txt @@ -0,0 +1,505 @@ +// Signature format: 2.0 +package android.net { + + public class CaptivePortal implements android.os.Parcelable { + method @Deprecated public void logEvent(int, @NonNull String); + method @RequiresPermission(android.Manifest.permission.NETWORK_STACK) public void reevaluateNetwork(); + method public void useNetwork(); + field public static final int APP_REQUEST_REEVALUATION_REQUIRED = 100; // 0x64 + field public static final int APP_RETURN_DISMISSED = 0; // 0x0 + field public static final int APP_RETURN_UNWANTED = 1; // 0x1 + field public static final int APP_RETURN_WANTED_AS_IS = 2; // 0x2 + } + + public final class CaptivePortalData implements android.os.Parcelable { + method public int describeContents(); + method public long getByteLimit(); + method public long getExpiryTimeMillis(); + method public long getRefreshTimeMillis(); + method @Nullable public android.net.Uri getUserPortalUrl(); + method public int getUserPortalUrlSource(); + method @Nullable public CharSequence getVenueFriendlyName(); + method @Nullable public android.net.Uri getVenueInfoUrl(); + method public int getVenueInfoUrlSource(); + method public boolean isCaptive(); + method public boolean isSessionExtendable(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0; // 0x0 + field public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; // 0x1 + field @NonNull public static final android.os.Parcelable.Creator<android.net.CaptivePortalData> CREATOR; + } + + public static class CaptivePortalData.Builder { + ctor public CaptivePortalData.Builder(); + ctor public CaptivePortalData.Builder(@Nullable android.net.CaptivePortalData); + method @NonNull public android.net.CaptivePortalData build(); + method @NonNull public android.net.CaptivePortalData.Builder setBytesRemaining(long); + method @NonNull public android.net.CaptivePortalData.Builder setCaptive(boolean); + method @NonNull public android.net.CaptivePortalData.Builder setExpiryTime(long); + method @NonNull public android.net.CaptivePortalData.Builder setRefreshTime(long); + method @NonNull public android.net.CaptivePortalData.Builder setSessionExtendable(boolean); + method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri); + method @NonNull public android.net.CaptivePortalData.Builder setUserPortalUrl(@Nullable android.net.Uri, int); + method @NonNull public android.net.CaptivePortalData.Builder setVenueFriendlyName(@Nullable CharSequence); + method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri); + method @NonNull public android.net.CaptivePortalData.Builder setVenueInfoUrl(@Nullable android.net.Uri, int); + } + + public class ConnectivityManager { + method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createNattKeepalive(@NonNull android.net.Network, @NonNull android.os.ParcelFileDescriptor, @NonNull java.net.InetAddress, @NonNull java.net.InetAddress, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); + method @NonNull @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) public android.net.SocketKeepalive createSocketKeepalive(@NonNull android.net.Network, @NonNull java.net.Socket, @NonNull java.util.concurrent.Executor, @NonNull android.net.SocketKeepalive.Callback); + method @Deprecated @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) public String getCaptivePortalServerUrl(); + method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void getLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEntitlementResultListener); + method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public boolean isTetheringSupported(); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public int registerNetworkProvider(@NonNull android.net.NetworkProvider); + method public void registerQosCallback(@NonNull android.net.QosSocketInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.QosCallback); + method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.ConnectivityManager.OnTetheringEventCallback); + method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void requestNetwork(@NonNull android.net.NetworkRequest, int, int, @NonNull android.os.Handler, @NonNull android.net.ConnectivityManager.NetworkCallback); + method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_AIRPLANE_MODE, android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.NETWORK_STACK}) public void setAirplaneMode(boolean); + method @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE) public void setOemNetworkPreference(@NonNull android.net.OemNetworkPreferences, @Nullable java.util.concurrent.Executor, @Nullable Runnable); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public boolean shouldAvoidBadWifi(); + method @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public void startCaptivePortalApp(@NonNull android.net.Network, @NonNull android.os.Bundle); + method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback); + method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void startTethering(int, boolean, android.net.ConnectivityManager.OnStartTetheringCallback, android.os.Handler); + method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void stopTethering(int); + method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_FACTORY}) public void unregisterNetworkProvider(@NonNull android.net.NetworkProvider); + method public void unregisterQosCallback(@NonNull android.net.QosCallback); + method @Deprecated @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public void unregisterTetheringEventCallback(@NonNull android.net.ConnectivityManager.OnTetheringEventCallback); + field public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC"; + field public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT = "android.net.extra.CAPTIVE_PORTAL_USER_AGENT"; + field public static final int TETHERING_BLUETOOTH = 2; // 0x2 + field public static final int TETHERING_USB = 1; // 0x1 + field public static final int TETHERING_WIFI = 0; // 0x0 + field @Deprecated public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13; // 0xd + field @Deprecated public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0 + field @Deprecated public static final int TETHER_ERROR_PROVISION_FAILED = 11; // 0xb + field public static final int TYPE_NONE = -1; // 0xffffffff + field @Deprecated public static final int TYPE_PROXY = 16; // 0x10 + field @Deprecated public static final int TYPE_WIFI_P2P = 13; // 0xd + } + + @Deprecated public abstract static class ConnectivityManager.OnStartTetheringCallback { + ctor @Deprecated public ConnectivityManager.OnStartTetheringCallback(); + method @Deprecated public void onTetheringFailed(); + method @Deprecated public void onTetheringStarted(); + } + + @Deprecated public static interface ConnectivityManager.OnTetheringEntitlementResultListener { + method @Deprecated public void onTetheringEntitlementResult(int); + } + + @Deprecated public abstract static class ConnectivityManager.OnTetheringEventCallback { + ctor @Deprecated public ConnectivityManager.OnTetheringEventCallback(); + method @Deprecated public void onUpstreamChanged(@Nullable android.net.Network); + } + + public final class InvalidPacketException extends java.lang.Exception { + ctor public InvalidPacketException(int); + method public int getError(); + field public static final int ERROR_INVALID_IP_ADDRESS = -21; // 0xffffffeb + field public static final int ERROR_INVALID_LENGTH = -23; // 0xffffffe9 + field public static final int ERROR_INVALID_PORT = -22; // 0xffffffea + } + + public final class IpConfiguration implements android.os.Parcelable { + ctor public IpConfiguration(); + ctor public IpConfiguration(@NonNull android.net.IpConfiguration); + method public int describeContents(); + method @Nullable public android.net.ProxyInfo getHttpProxy(); + method @NonNull public android.net.IpConfiguration.IpAssignment getIpAssignment(); + method @NonNull public android.net.IpConfiguration.ProxySettings getProxySettings(); + method @Nullable public android.net.StaticIpConfiguration getStaticIpConfiguration(); + method public void setHttpProxy(@Nullable android.net.ProxyInfo); + method public void setIpAssignment(@NonNull android.net.IpConfiguration.IpAssignment); + method public void setProxySettings(@NonNull android.net.IpConfiguration.ProxySettings); + method public void setStaticIpConfiguration(@Nullable android.net.StaticIpConfiguration); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.IpConfiguration> CREATOR; + } + + public enum IpConfiguration.IpAssignment { + enum_constant public static final android.net.IpConfiguration.IpAssignment DHCP; + enum_constant public static final android.net.IpConfiguration.IpAssignment STATIC; + enum_constant public static final android.net.IpConfiguration.IpAssignment UNASSIGNED; + } + + public enum IpConfiguration.ProxySettings { + enum_constant public static final android.net.IpConfiguration.ProxySettings NONE; + enum_constant public static final android.net.IpConfiguration.ProxySettings PAC; + enum_constant public static final android.net.IpConfiguration.ProxySettings STATIC; + enum_constant public static final android.net.IpConfiguration.ProxySettings UNASSIGNED; + } + + public final class IpPrefix implements android.os.Parcelable { + ctor public IpPrefix(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int); + ctor public IpPrefix(@NonNull String); + } + + public class KeepalivePacketData { + ctor protected KeepalivePacketData(@NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull java.net.InetAddress, @IntRange(from=0, to=65535) int, @NonNull byte[]) throws android.net.InvalidPacketException; + method @NonNull public java.net.InetAddress getDstAddress(); + method public int getDstPort(); + method @NonNull public byte[] getPacket(); + method @NonNull public java.net.InetAddress getSrcAddress(); + method public int getSrcPort(); + } + + public class LinkAddress implements android.os.Parcelable { + ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int); + ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int, int, int, long, long); + ctor public LinkAddress(@NonNull java.net.InetAddress, @IntRange(from=0, to=128) int); + ctor public LinkAddress(@NonNull String); + ctor public LinkAddress(@NonNull String, int, int); + method public long getDeprecationTime(); + method public long getExpirationTime(); + method public boolean isGlobalPreferred(); + method public boolean isIpv4(); + method public boolean isIpv6(); + method public boolean isSameAddressAs(@Nullable android.net.LinkAddress); + field public static final long LIFETIME_PERMANENT = 9223372036854775807L; // 0x7fffffffffffffffL + field public static final long LIFETIME_UNKNOWN = -1L; // 0xffffffffffffffffL + } + + public final class LinkProperties implements android.os.Parcelable { + ctor public LinkProperties(@Nullable android.net.LinkProperties); + ctor public LinkProperties(@Nullable android.net.LinkProperties, boolean); + method public boolean addDnsServer(@NonNull java.net.InetAddress); + method public boolean addLinkAddress(@NonNull android.net.LinkAddress); + method public boolean addPcscfServer(@NonNull java.net.InetAddress); + method @NonNull public java.util.List<java.net.InetAddress> getAddresses(); + method @NonNull public java.util.List<java.lang.String> getAllInterfaceNames(); + method @NonNull public java.util.List<android.net.LinkAddress> getAllLinkAddresses(); + method @NonNull public java.util.List<android.net.RouteInfo> getAllRoutes(); + method @Nullable public android.net.Uri getCaptivePortalApiUrl(); + method @Nullable public android.net.CaptivePortalData getCaptivePortalData(); + method @NonNull public java.util.List<java.net.InetAddress> getPcscfServers(); + method @Nullable public String getTcpBufferSizes(); + method @NonNull public java.util.List<java.net.InetAddress> getValidatedPrivateDnsServers(); + method public boolean hasGlobalIpv6Address(); + method public boolean hasIpv4Address(); + method public boolean hasIpv4DefaultRoute(); + method public boolean hasIpv4DnsServer(); + method public boolean hasIpv6DefaultRoute(); + method public boolean hasIpv6DnsServer(); + method public boolean isIpv4Provisioned(); + method public boolean isIpv6Provisioned(); + method public boolean isProvisioned(); + method public boolean isReachable(@NonNull java.net.InetAddress); + method public boolean removeDnsServer(@NonNull java.net.InetAddress); + method public boolean removeLinkAddress(@NonNull android.net.LinkAddress); + method public boolean removeRoute(@NonNull android.net.RouteInfo); + method public void setCaptivePortalApiUrl(@Nullable android.net.Uri); + method public void setCaptivePortalData(@Nullable android.net.CaptivePortalData); + method public void setPcscfServers(@NonNull java.util.Collection<java.net.InetAddress>); + method public void setPrivateDnsServerName(@Nullable String); + method public void setTcpBufferSizes(@Nullable String); + method public void setUsePrivateDns(boolean); + method public void setValidatedPrivateDnsServers(@NonNull java.util.Collection<java.net.InetAddress>); + } + + public final class NattKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable { + ctor public NattKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[]) throws android.net.InvalidPacketException; + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.NattKeepalivePacketData> CREATOR; + } + + public class Network implements android.os.Parcelable { + ctor public Network(@NonNull android.net.Network); + method public int getNetId(); + method @NonNull public android.net.Network getPrivateDnsBypassingCopy(); + } + + public abstract class NetworkAgent { + ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, int, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider); + ctor public NetworkAgent(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties, @NonNull android.net.NetworkScore, @NonNull android.net.NetworkAgentConfig, @Nullable android.net.NetworkProvider); + method @Nullable public android.net.Network getNetwork(); + method public void markConnected(); + method public void onAddKeepalivePacketFilter(int, @NonNull android.net.KeepalivePacketData); + method public void onAutomaticReconnectDisabled(); + method public void onBandwidthUpdateRequested(); + method public void onNetworkCreated(); + method public void onNetworkDestroyed(); + method public void onNetworkUnwanted(); + method public void onQosCallbackRegistered(int, @NonNull android.net.QosFilter); + method public void onQosCallbackUnregistered(int); + method public void onRemoveKeepalivePacketFilter(int); + method public void onSaveAcceptUnvalidated(boolean); + method public void onSignalStrengthThresholdsUpdated(@NonNull int[]); + method public void onStartSocketKeepalive(int, @NonNull java.time.Duration, @NonNull android.net.KeepalivePacketData); + method public void onStopSocketKeepalive(int); + method public void onValidationStatus(int, @Nullable android.net.Uri); + method @NonNull public android.net.Network register(); + method public final void sendLinkProperties(@NonNull android.net.LinkProperties); + method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities); + method public final void sendNetworkScore(@NonNull android.net.NetworkScore); + method public final void sendNetworkScore(@IntRange(from=0, to=99) int); + method public final void sendQosCallbackError(int, int); + method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes); + method public final void sendQosSessionLost(int, int, int); + method public final void sendSocketKeepaliveEvent(int, int); + method @Deprecated public void setLegacySubtype(int, @NonNull String); + method public void setLingerDuration(@NonNull java.time.Duration); + method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int); + method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>); + method public void unregister(); + field public static final int VALIDATION_STATUS_NOT_VALID = 2; // 0x2 + field public static final int VALIDATION_STATUS_VALID = 1; // 0x1 + } + + public final class NetworkAgentConfig implements android.os.Parcelable { + method public int describeContents(); + method public int getLegacyType(); + method @NonNull public String getLegacyTypeName(); + method public boolean isExplicitlySelected(); + method public boolean isPartialConnectivityAcceptable(); + method public boolean isUnvalidatedConnectivityAcceptable(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkAgentConfig> CREATOR; + } + + public static final class NetworkAgentConfig.Builder { + ctor public NetworkAgentConfig.Builder(); + method @NonNull public android.net.NetworkAgentConfig build(); + method @NonNull public android.net.NetworkAgentConfig.Builder setExplicitlySelected(boolean); + method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyExtraInfo(@NonNull String); + method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubType(int); + method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubTypeName(@NonNull String); + method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyType(int); + method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyTypeName(@NonNull String); + method @NonNull public android.net.NetworkAgentConfig.Builder setNat64DetectionEnabled(boolean); + method @NonNull public android.net.NetworkAgentConfig.Builder setPartialConnectivityAcceptable(boolean); + method @NonNull public android.net.NetworkAgentConfig.Builder setProvisioningNotificationEnabled(boolean); + method @NonNull public android.net.NetworkAgentConfig.Builder setUnvalidatedConnectivityAcceptable(boolean); + } + + public final class NetworkCapabilities implements android.os.Parcelable { + method @NonNull public int[] getAdministratorUids(); + method @Nullable public static String getCapabilityCarrierName(int); + method @Nullable public String getSsid(); + method @NonNull public java.util.Set<java.lang.Integer> getSubscriptionIds(); + method @NonNull public int[] getTransportTypes(); + method public boolean isPrivateDnsBroken(); + method public boolean satisfiedByNetworkCapabilities(@Nullable android.net.NetworkCapabilities); + field public static final int NET_CAPABILITY_BIP = 31; // 0x1f + field public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28; // 0x1c + field public static final int NET_CAPABILITY_OEM_PAID = 22; // 0x16 + field public static final int NET_CAPABILITY_OEM_PRIVATE = 26; // 0x1a + field public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24; // 0x18 + field public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27; // 0x1b + field public static final int NET_CAPABILITY_VSIM = 30; // 0x1e + } + + public static final class NetworkCapabilities.Builder { + ctor public NetworkCapabilities.Builder(); + ctor public NetworkCapabilities.Builder(@NonNull android.net.NetworkCapabilities); + method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int); + method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int); + method @NonNull public android.net.NetworkCapabilities build(); + method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int); + method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]); + method @NonNull public android.net.NetworkCapabilities.Builder setLinkDownstreamBandwidthKbps(int); + method @NonNull public android.net.NetworkCapabilities.Builder setLinkUpstreamBandwidthKbps(int); + method @NonNull public android.net.NetworkCapabilities.Builder setNetworkSpecifier(@Nullable android.net.NetworkSpecifier); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setOwnerUid(int); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorPackageName(@Nullable String); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setRequestorUid(int); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkCapabilities.Builder setSignalStrength(int); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String); + method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>); + method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo); + method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities(); + } + + public class NetworkProvider { + ctor public NetworkProvider(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String); + method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void declareNetworkRequestUnfulfillable(@NonNull android.net.NetworkRequest); + method public int getProviderId(); + method public void onNetworkRequestWithdrawn(@NonNull android.net.NetworkRequest); + method public void onNetworkRequested(@NonNull android.net.NetworkRequest, @IntRange(from=0, to=99) int, int); + method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void registerNetworkOffer(@NonNull android.net.NetworkScore, @NonNull android.net.NetworkCapabilities, @NonNull java.util.concurrent.Executor, @NonNull android.net.NetworkProvider.NetworkOfferCallback); + method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void unregisterNetworkOffer(@NonNull android.net.NetworkProvider.NetworkOfferCallback); + field public static final int ID_NONE = -1; // 0xffffffff + } + + public static interface NetworkProvider.NetworkOfferCallback { + method public void onNetworkNeeded(@NonNull android.net.NetworkRequest); + method public void onNetworkUnneeded(@NonNull android.net.NetworkRequest); + } + + public class NetworkReleasedException extends java.lang.Exception { + } + + public class NetworkRequest implements android.os.Parcelable { + method @Nullable public String getRequestorPackageName(); + method public int getRequestorUid(); + } + + public static class NetworkRequest.Builder { + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) public android.net.NetworkRequest.Builder setSignalStrength(int); + method @NonNull public android.net.NetworkRequest.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>); + } + + public final class NetworkScore implements android.os.Parcelable { + method public int describeContents(); + method public int getKeepConnectedReason(); + method public int getLegacyInt(); + method public boolean isExiting(); + method public boolean isTransportPrimary(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.NetworkScore> CREATOR; + field public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; // 0x1 + field public static final int KEEP_CONNECTED_NONE = 0; // 0x0 + } + + public static final class NetworkScore.Builder { + ctor public NetworkScore.Builder(); + method @NonNull public android.net.NetworkScore build(); + method @NonNull public android.net.NetworkScore.Builder setExiting(boolean); + method @NonNull public android.net.NetworkScore.Builder setKeepConnectedReason(int); + method @NonNull public android.net.NetworkScore.Builder setLegacyInt(int); + method @NonNull public android.net.NetworkScore.Builder setTransportPrimary(boolean); + } + + public final class OemNetworkPreferences implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.Map<java.lang.String,java.lang.Integer> getNetworkPreferences(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.OemNetworkPreferences> CREATOR; + field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1; // 0x1 + field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2; // 0x2 + field public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3; // 0x3 + field public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4; // 0x4 + field public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0; // 0x0 + } + + public static final class OemNetworkPreferences.Builder { + ctor public OemNetworkPreferences.Builder(); + ctor public OemNetworkPreferences.Builder(@NonNull android.net.OemNetworkPreferences); + method @NonNull public android.net.OemNetworkPreferences.Builder addNetworkPreference(@NonNull String, int); + method @NonNull public android.net.OemNetworkPreferences build(); + method @NonNull public android.net.OemNetworkPreferences.Builder clearNetworkPreference(@NonNull String); + } + + public abstract class QosCallback { + ctor public QosCallback(); + method public void onError(@NonNull android.net.QosCallbackException); + method public void onQosSessionAvailable(@NonNull android.net.QosSession, @NonNull android.net.QosSessionAttributes); + method public void onQosSessionLost(@NonNull android.net.QosSession); + } + + public static class QosCallback.QosCallbackRegistrationException extends java.lang.RuntimeException { + } + + public final class QosCallbackException extends java.lang.Exception { + } + + public abstract class QosFilter { + method @NonNull public abstract android.net.Network getNetwork(); + method public abstract boolean matchesLocalAddress(@NonNull java.net.InetAddress, int, int); + method public abstract boolean matchesRemoteAddress(@NonNull java.net.InetAddress, int, int); + } + + public final class QosSession implements android.os.Parcelable { + ctor public QosSession(int, int); + method public int describeContents(); + method public int getSessionId(); + method public int getSessionType(); + method public long getUniqueId(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSession> CREATOR; + field public static final int TYPE_EPS_BEARER = 1; // 0x1 + field public static final int TYPE_NR_BEARER = 2; // 0x2 + } + + public interface QosSessionAttributes { + } + + public final class QosSocketInfo implements android.os.Parcelable { + ctor public QosSocketInfo(@NonNull android.net.Network, @NonNull java.net.Socket) throws java.io.IOException; + method public int describeContents(); + method @NonNull public java.net.InetSocketAddress getLocalSocketAddress(); + method @NonNull public android.net.Network getNetwork(); + method @Nullable public java.net.InetSocketAddress getRemoteSocketAddress(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSocketInfo> CREATOR; + } + + public final class RouteInfo implements android.os.Parcelable { + ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int); + ctor public RouteInfo(@Nullable android.net.IpPrefix, @Nullable java.net.InetAddress, @Nullable String, int, int); + method public int getMtu(); + method public int getType(); + field public static final int RTN_THROW = 9; // 0x9 + field public static final int RTN_UNICAST = 1; // 0x1 + field public static final int RTN_UNREACHABLE = 7; // 0x7 + } + + public abstract class SocketKeepalive implements java.lang.AutoCloseable { + field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf + field public static final int SUCCESS = 0; // 0x0 + } + + public class SocketLocalAddressChangedException extends java.lang.Exception { + } + + public class SocketNotBoundException extends java.lang.Exception { + } + + public final class StaticIpConfiguration implements android.os.Parcelable { + ctor public StaticIpConfiguration(); + ctor public StaticIpConfiguration(@Nullable android.net.StaticIpConfiguration); + method public void addDnsServer(@NonNull java.net.InetAddress); + method public void clear(); + method public int describeContents(); + method @NonNull public java.util.List<java.net.InetAddress> getDnsServers(); + method @Nullable public String getDomains(); + method @Nullable public java.net.InetAddress getGateway(); + method @Nullable public android.net.LinkAddress getIpAddress(); + method @NonNull public java.util.List<android.net.RouteInfo> getRoutes(@Nullable String); + method public void writeToParcel(android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.StaticIpConfiguration> CREATOR; + } + + public static final class StaticIpConfiguration.Builder { + ctor public StaticIpConfiguration.Builder(); + method @NonNull public android.net.StaticIpConfiguration build(); + method @NonNull public android.net.StaticIpConfiguration.Builder setDnsServers(@NonNull Iterable<java.net.InetAddress>); + method @NonNull public android.net.StaticIpConfiguration.Builder setDomains(@Nullable String); + method @NonNull public android.net.StaticIpConfiguration.Builder setGateway(@Nullable java.net.InetAddress); + method @NonNull public android.net.StaticIpConfiguration.Builder setIpAddress(@Nullable android.net.LinkAddress); + } + + public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable { + ctor public TcpKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[], int, int, int, int, int, int) throws android.net.InvalidPacketException; + method public int describeContents(); + method public int getIpTos(); + method public int getIpTtl(); + method public int getTcpAck(); + method public int getTcpSeq(); + method public int getTcpWindow(); + method public int getTcpWindowScale(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.net.TcpKeepalivePacketData> CREATOR; + } + +} + +package android.net.apf { + + public final class ApfCapabilities implements android.os.Parcelable { + ctor public ApfCapabilities(int, int, int); + method public int describeContents(); + method public static boolean getApfDrop8023Frames(); + method @NonNull public static int[] getApfEtherTypeBlackList(); + method public boolean hasDataAccess(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.net.apf.ApfCapabilities> CREATOR; + field public final int apfPacketFormat; + field public final int apfVersionSupported; + field public final int maximumApfProgramSize; + } + +} + diff --git a/framework/api/system-lint-baseline.txt b/framework/api/system-lint-baseline.txt new file mode 100644 index 0000000000000000000000000000000000000000..9a97707763f1187a07e6f3e3aff9247b5a727146 --- /dev/null +++ b/framework/api/system-lint-baseline.txt @@ -0,0 +1 @@ +// Baseline format: 1.0 diff --git a/framework/api/system-removed.txt b/framework/api/system-removed.txt new file mode 100644 index 0000000000000000000000000000000000000000..d802177e249b3f97128699222e65c35e57ba7540 --- /dev/null +++ b/framework/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..2e5848cb100d3e4a92c26ae71fe61f5081a13dc8 --- /dev/null +++ b/framework/jarjar-rules.txt @@ -0,0 +1,2 @@ +rule com.android.net.module.util.** android.net.connectivity.framework.util.@1 +rule android.net.NetworkFactory* android.net.connectivity.framework.NetworkFactory@1 diff --git a/framework/jni/android_net_NetworkUtils.cpp b/framework/jni/android_net_NetworkUtils.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7478b3e73d0f92a47b6a73448778659f0407cd24 --- /dev/null +++ b/framework/jni/android_net_NetworkUtils.cpp @@ -0,0 +1,271 @@ +/* + * Copyright 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "NetworkUtils" + +#include <android/file_descriptor_jni.h> +#include <android/multinetwork.h> +#include <linux/filter.h> +#include <linux/tcp.h> +#include <netinet/in.h> +#include <string.h> + +#include <DnsProxydProtocol.h> // NETID_USE_LOCAL_NAMESERVERS +#include <nativehelper/JNIPlatformHelp.h> +#include <utils/Log.h> + +#include "jni.h" + +#define NETUTILS_PKG_NAME "android/net/NetworkUtils" + +namespace android { + +constexpr int MAXPACKETSIZE = 8 * 1024; +// FrameworkListener limits the size of commands to 4096 bytes. +constexpr int MAXCMDSIZE = 4096; + +static volatile jclass class_Network = 0; +static volatile jmethodID method_fromNetworkHandle = 0; + +static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) { + jclass clazz = env->FindClass(class_name); + LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name); + return clazz; +} + +template <typename T> +static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) { + jobject res = env->NewGlobalRef(in); + LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference."); + return static_cast<T>(res); +} + +static void android_net_utils_attachDropAllBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd) +{ + struct sock_filter filter_code[] = { + // Reject all. + BPF_STMT(BPF_RET | BPF_K, 0) + }; + struct sock_fprog filter = { + sizeof(filter_code) / sizeof(filter_code[0]), + filter_code, + }; + + int fd = AFileDescriptor_getFd(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_utils_detachBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd) +{ + int optval_ignored = 0; + int fd = AFileDescriptor_getFd(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_DETACH_FILTER, &optval_ignored, sizeof(optval_ignored)) != + 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_DETACH_FILTER): %s", strerror(errno)); + } +} + +static jboolean android_net_utils_bindProcessToNetworkHandle(JNIEnv *env, jobject thiz, + jlong netHandle) +{ + return (jboolean) !android_setprocnetwork(netHandle); +} + +static jlong android_net_utils_getBoundNetworkHandleForProcess(JNIEnv *env, jobject thiz) +{ + net_handle_t network; + if (android_getprocnetwork(&network) != 0) { + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "android_getprocnetwork(): %s", strerror(errno)); + return NETWORK_UNSPECIFIED; + } + return (jlong) network; +} + +static jboolean android_net_utils_bindProcessToNetworkForHostResolution(JNIEnv *env, jobject thiz, + jint netId, jlong netHandle) +{ + return (jboolean) !android_setprocdns(netHandle); +} + +static jint android_net_utils_bindSocketToNetworkHandle(JNIEnv *env, jobject thiz, jobject javaFd, + jlong netHandle) { + return android_setsocknetwork(netHandle, AFileDescriptor_getFd(env, javaFd)); +} + +static bool checkLenAndCopy(JNIEnv* env, const jbyteArray& addr, int len, void* dst) +{ + if (env->GetArrayLength(addr) != len) { + return false; + } + env->GetByteArrayRegion(addr, 0, len, reinterpret_cast<jbyte*>(dst)); + return true; +} + +static jobject android_net_utils_resNetworkQuery(JNIEnv *env, jobject thiz, jlong netHandle, + jstring dname, jint ns_class, jint ns_type, jint flags) { + const jsize javaCharsCount = env->GetStringLength(dname); + const jsize byteCountUTF8 = env->GetStringUTFLength(dname); + + // Only allow dname which could be simply formatted to UTF8. + // In native layer, res_mkquery would re-format the input char array to packet. + char queryname[byteCountUTF8 + 1]; + memset(queryname, 0, (byteCountUTF8 + 1) * sizeof(char)); + + env->GetStringUTFRegion(dname, 0, javaCharsCount, queryname); + int fd = android_res_nquery(netHandle, queryname, ns_class, ns_type, flags); + + if (fd < 0) { + jniThrowErrnoException(env, "resNetworkQuery", -fd); + return nullptr; + } + + return jniCreateFileDescriptor(env, fd); +} + +static jobject android_net_utils_resNetworkSend(JNIEnv *env, jobject thiz, jlong netHandle, + jbyteArray msg, jint msgLen, jint flags) { + uint8_t data[MAXCMDSIZE]; + + checkLenAndCopy(env, msg, msgLen, data); + int fd = android_res_nsend(netHandle, data, msgLen, flags); + + if (fd < 0) { + jniThrowErrnoException(env, "resNetworkSend", -fd); + return nullptr; + } + + return jniCreateFileDescriptor(env, fd); +} + +static jobject android_net_utils_resNetworkResult(JNIEnv *env, jobject thiz, jobject javaFd) { + int fd = AFileDescriptor_getFd(env, javaFd); + int rcode; + uint8_t buf[MAXPACKETSIZE] = {0}; + + int res = android_res_nresult(fd, &rcode, buf, MAXPACKETSIZE); + jniSetFileDescriptorOfFD(env, javaFd, -1); + if (res < 0) { + jniThrowErrnoException(env, "resNetworkResult", -res); + return nullptr; + } + + jbyteArray answer = env->NewByteArray(res); + if (answer == nullptr) { + jniThrowErrnoException(env, "resNetworkResult", ENOMEM); + return nullptr; + } else { + env->SetByteArrayRegion(answer, 0, res, reinterpret_cast<jbyte*>(buf)); + } + + jclass class_DnsResponse = env->FindClass("android/net/DnsResolver$DnsResponse"); + jmethodID ctor = env->GetMethodID(class_DnsResponse, "<init>", "([BI)V"); + + return env->NewObject(class_DnsResponse, ctor, answer, rcode); +} + +static void android_net_utils_resNetworkCancel(JNIEnv *env, jobject thiz, jobject javaFd) { + int fd = AFileDescriptor_getFd(env, javaFd); + android_res_cancel(fd); + jniSetFileDescriptorOfFD(env, javaFd, -1); +} + +static jobject android_net_utils_getDnsNetwork(JNIEnv *env, jobject thiz) { + net_handle_t dnsNetHandle = NETWORK_UNSPECIFIED; + if (int res = android_getprocdns(&dnsNetHandle) < 0) { + jniThrowErrnoException(env, "getDnsNetwork", -res); + return nullptr; + } + + if (method_fromNetworkHandle == 0) { + // This may be called multiple times concurrently but that is fine + class_Network = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/net/Network")); + method_fromNetworkHandle = env->GetStaticMethodID(class_Network, "fromNetworkHandle", + "(J)Landroid/net/Network;"); + } + return env->CallStaticObjectMethod(class_Network, method_fromNetworkHandle, + static_cast<jlong>(dnsNetHandle)); +} + +static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) { + if (javaFd == NULL) { + jniThrowNullPointerException(env, NULL); + return NULL; + } + + int fd = AFileDescriptor_getFd(env, javaFd); + struct tcp_repair_window trw = {}; + socklen_t size = sizeof(trw); + + // Obtain the parameters of the TCP repair window. + int rc = getsockopt(fd, IPPROTO_TCP, TCP_REPAIR_WINDOW, &trw, &size); + if (rc == -1) { + jniThrowErrnoException(env, "getsockopt : TCP_REPAIR_WINDOW", errno); + return NULL; + } + + struct tcp_info tcpinfo = {}; + socklen_t tcpinfo_size = sizeof(tcp_info); + + // Obtain the window scale from the tcp info structure. This contains a scale factor that + // should be applied to the window size. + rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, &tcpinfo, &tcpinfo_size); + if (rc == -1) { + jniThrowErrnoException(env, "getsockopt : TCP_INFO", errno); + return NULL; + } + + jclass class_TcpRepairWindow = env->FindClass("android/net/TcpRepairWindow"); + jmethodID ctor = env->GetMethodID(class_TcpRepairWindow, "<init>", "(IIIIII)V"); + + return env->NewObject(class_TcpRepairWindow, ctor, trw.snd_wl1, trw.snd_wnd, trw.max_window, + trw.rcv_wnd, trw.rcv_wup, tcpinfo.tcpi_rcv_wscale); +} + +// ---------------------------------------------------------------------------- + +/* + * JNI registration. + */ +// clang-format off +static const JNINativeMethod gNetworkUtilMethods[] = { + /* name, signature, funcPtr */ + { "bindProcessToNetworkHandle", "(J)Z", (void*) android_net_utils_bindProcessToNetworkHandle }, + { "getBoundNetworkHandleForProcess", "()J", (void*) android_net_utils_getBoundNetworkHandleForProcess }, + { "bindProcessToNetworkForHostResolution", "(I)Z", (void*) android_net_utils_bindProcessToNetworkForHostResolution }, + { "bindSocketToNetworkHandle", "(Ljava/io/FileDescriptor;J)I", (void*) android_net_utils_bindSocketToNetworkHandle }, + { "attachDropAllBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDropAllBPFFilter }, + { "detachBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_detachBPFFilter }, + { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow }, + { "resNetworkSend", "(J[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend }, + { "resNetworkQuery", "(JLjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery }, + { "resNetworkResult", "(Ljava/io/FileDescriptor;)Landroid/net/DnsResolver$DnsResponse;", (void*) android_net_utils_resNetworkResult }, + { "resNetworkCancel", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_resNetworkCancel }, + { "getDnsNetwork", "()Landroid/net/Network;", (void*) android_net_utils_getDnsNetwork }, +}; +// clang-format on + +int register_android_net_NetworkUtils(JNIEnv* env) +{ + return jniRegisterNativeMethods(env, NETUTILS_PKG_NAME, gNetworkUtilMethods, + NELEM(gNetworkUtilMethods)); +} + +}; // namespace android diff --git a/framework/jni/onload.cpp b/framework/jni/onload.cpp new file mode 100644 index 0000000000000000000000000000000000000000..435f4343ed145d7eefc5203aea7a3786bcad5435 --- /dev/null +++ b/framework/jni/onload.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <nativehelper/JNIHelp.h> +#include <log/log.h> + +namespace android { + +int register_android_net_NetworkUtils(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) { + ALOGE("GetEnv failed"); + return JNI_ERR; + } + + if (register_android_net_NetworkUtils(env) < 0) { + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} + +}; \ No newline at end of file diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml new file mode 100644 index 0000000000000000000000000000000000000000..099202f97c1cbdd18d456c5ad3ec1bf7c8c65abf --- /dev/null +++ b/framework/lint-baseline.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0"> + + <issue + id="NewApi" + message="Call requires API level 31 (current min is 30): `new android.net.ParseException`" + errorLine1=" ParseException pe = new ParseException(e.reason, e.getCause());" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/framework/src/android/net/DnsResolver.java" + line="301" + column="37"/> + </issue> + + <issue + id="NewApi" + message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback`" + errorLine1=" protected class ActiveDataSubscriptionIdListener extends TelephonyCallback" + errorLine2=" ~~~~~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java" + line="96" + column="62"/> + </issue> + + <issue + id="NewApi" + message="Class requires API level 31 (current min is 30): `android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener`" + errorLine1=" implements TelephonyCallback.ActiveDataSubscriptionIdListener {" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java" + line="97" + column="24"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#registerTelephonyCallback`" + errorLine1=" ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback(" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/framework/src/android/net/util/MultinetworkPolicyTracker.java" + line="126" + column="54"/> + </issue> + +</issues> diff --git a/framework/src/android/net/CaptivePortal.java b/framework/src/android/net/CaptivePortal.java new file mode 100644 index 0000000000000000000000000000000000000000..4a7b6016427be418f75ccb3f140fe35874aea3c7 --- /dev/null +++ b/framework/src/android/net/CaptivePortal.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed urnder the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.RequiresPermission; +import android.annotation.SystemApi; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +/** + * A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN} + * activity to indicate to the system different outcomes of captive portal sign in. This class is + * passed as an extra named {@link ConnectivityManager#EXTRA_CAPTIVE_PORTAL} with the + * {@code ACTION_CAPTIVE_PORTAL_SIGN_IN} activity. + */ +public class CaptivePortal implements Parcelable { + /** + * Response code from the captive portal application, indicating that the portal was dismissed + * and the network should be re-validated. + * @see ICaptivePortal#appResponse(int) + * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int) + * @hide + */ + @SystemApi + public static final int APP_RETURN_DISMISSED = 0; + /** + * Response code from the captive portal application, indicating that the user did not login and + * does not want to use the captive portal network. + * @see ICaptivePortal#appResponse(int) + * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int) + * @hide + */ + @SystemApi + public static final int APP_RETURN_UNWANTED = 1; + /** + * Response code from the captive portal application, indicating that the user does not wish to + * login but wants to use the captive portal network as-is. + * @see ICaptivePortal#appResponse(int) + * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int) + * @hide + */ + @SystemApi + public static final int APP_RETURN_WANTED_AS_IS = 2; + /** Event offset of request codes from captive portal application. */ + private static final int APP_REQUEST_BASE = 100; + /** + * Request code from the captive portal application, indicating that the network condition may + * have changed and the network should be re-validated. + * @see ICaptivePortal#appRequest(int) + * @see android.net.INetworkMonitor#forceReevaluation(int) + * @hide + */ + @SystemApi + public static final int APP_REQUEST_REEVALUATION_REQUIRED = APP_REQUEST_BASE + 0; + + private final IBinder mBinder; + + /** @hide */ + public CaptivePortal(@NonNull IBinder binder) { + mBinder = binder; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeStrongBinder(mBinder); + } + + public static final @android.annotation.NonNull Parcelable.Creator<CaptivePortal> CREATOR + = new Parcelable.Creator<CaptivePortal>() { + @Override + public CaptivePortal createFromParcel(Parcel in) { + return new CaptivePortal(in.readStrongBinder()); + } + + @Override + public CaptivePortal[] newArray(int size) { + return new CaptivePortal[size]; + } + }; + + /** + * Indicate to the system that the captive portal has been + * dismissed. In response the framework will re-evaluate the network's + * connectivity and might take further action thereafter. + */ + public void reportCaptivePortalDismissed() { + try { + ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_DISMISSED); + } catch (RemoteException e) { + } + } + + /** + * Indicate to the system that the user does not want to pursue signing in to the + * captive portal and the system should continue to prefer other networks + * without captive portals for use as the default active data network. The + * system will not retest the network for a captive portal so as to avoid + * disturbing the user with further sign in to network notifications. + */ + public void ignoreNetwork() { + try { + ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_UNWANTED); + } catch (RemoteException e) { + } + } + + /** + * Indicate to the system the user wants to use this network as is, even though + * the captive portal is still in place. The system will treat the network + * as if it did not have a captive portal when selecting the network to use + * as the default active data network. This may result in this network + * becoming the default active data network, which could disrupt network + * connectivity for apps because the captive portal is still in place. + * @hide + */ + @SystemApi + public void useNetwork() { + try { + ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_WANTED_AS_IS); + } catch (RemoteException e) { + } + } + + /** + * Request that the system reevaluates the captive portal status. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) + public void reevaluateNetwork() { + try { + ICaptivePortal.Stub.asInterface(mBinder).appRequest(APP_REQUEST_REEVALUATION_REQUIRED); + } catch (RemoteException e) { + } + } + + /** + * Log a captive portal login event. + * @param eventId one of the CAPTIVE_PORTAL_LOGIN_* constants in metrics_constants.proto. + * @param packageName captive portal application package name. + * @hide + * @deprecated The event will not be logged in Android S and above. The + * caller is migrating to statsd. + */ + @Deprecated + @SystemApi + public void logEvent(int eventId, @NonNull String packageName) { + } +} diff --git a/framework/src/android/net/CaptivePortalData.java b/framework/src/android/net/CaptivePortalData.java new file mode 100644 index 0000000000000000000000000000000000000000..53aa1b92edcadc953143c122ff0d720567740e82 --- /dev/null +++ b/framework/src/android/net/CaptivePortalData.java @@ -0,0 +1,379 @@ +/* + * 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Metadata sent by captive portals, see https://www.ietf.org/id/draft-ietf-capport-api-03.txt. + * @hide + */ +@SystemApi +public final class CaptivePortalData implements Parcelable { + private final long mRefreshTimeMillis; + @Nullable + private final Uri mUserPortalUrl; + @Nullable + private final Uri mVenueInfoUrl; + private final boolean mIsSessionExtendable; + private final long mByteLimit; + private final long mExpiryTimeMillis; + private final boolean mCaptive; + private final String mVenueFriendlyName; + private final int mVenueInfoUrlSource; + private final int mUserPortalUrlSource; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"CAPTIVE_PORTAL_DATA_SOURCE_"}, value = { + CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT}) + public @interface CaptivePortalDataSource {} + + /** + * Source of information: Other (default) + */ + public static final int CAPTIVE_PORTAL_DATA_SOURCE_OTHER = 0; + + /** + * Source of information: Wi-Fi Passpoint + */ + public static final int CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT = 1; + + private CaptivePortalData(long refreshTimeMillis, Uri userPortalUrl, Uri venueInfoUrl, + boolean isSessionExtendable, long byteLimit, long expiryTimeMillis, boolean captive, + CharSequence venueFriendlyName, int venueInfoUrlSource, int userPortalUrlSource) { + mRefreshTimeMillis = refreshTimeMillis; + mUserPortalUrl = userPortalUrl; + mVenueInfoUrl = venueInfoUrl; + mIsSessionExtendable = isSessionExtendable; + mByteLimit = byteLimit; + mExpiryTimeMillis = expiryTimeMillis; + mCaptive = captive; + mVenueFriendlyName = venueFriendlyName == null ? null : venueFriendlyName.toString(); + mVenueInfoUrlSource = venueInfoUrlSource; + mUserPortalUrlSource = userPortalUrlSource; + } + + private CaptivePortalData(Parcel p) { + this(p.readLong(), p.readParcelable(null), p.readParcelable(null), p.readBoolean(), + p.readLong(), p.readLong(), p.readBoolean(), p.readString(), p.readInt(), + p.readInt()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mRefreshTimeMillis); + dest.writeParcelable(mUserPortalUrl, 0); + dest.writeParcelable(mVenueInfoUrl, 0); + dest.writeBoolean(mIsSessionExtendable); + dest.writeLong(mByteLimit); + dest.writeLong(mExpiryTimeMillis); + dest.writeBoolean(mCaptive); + dest.writeString(mVenueFriendlyName); + dest.writeInt(mVenueInfoUrlSource); + dest.writeInt(mUserPortalUrlSource); + } + + /** + * A builder to create new {@link CaptivePortalData}. + */ + public static class Builder { + private long mRefreshTime; + private Uri mUserPortalUrl; + private Uri mVenueInfoUrl; + private boolean mIsSessionExtendable; + private long mBytesRemaining = -1; + private long mExpiryTime = -1; + private boolean mCaptive; + private CharSequence mVenueFriendlyName; + private @CaptivePortalDataSource int mVenueInfoUrlSource = CAPTIVE_PORTAL_DATA_SOURCE_OTHER; + private @CaptivePortalDataSource int mUserPortalUrlSource = + CAPTIVE_PORTAL_DATA_SOURCE_OTHER; + + /** + * Create an empty builder. + */ + public Builder() {} + + /** + * Create a builder copying all data from existing {@link CaptivePortalData}. + */ + public Builder(@Nullable CaptivePortalData data) { + if (data == null) return; + setRefreshTime(data.mRefreshTimeMillis) + .setUserPortalUrl(data.mUserPortalUrl, data.mUserPortalUrlSource) + .setVenueInfoUrl(data.mVenueInfoUrl, data.mVenueInfoUrlSource) + .setSessionExtendable(data.mIsSessionExtendable) + .setBytesRemaining(data.mByteLimit) + .setExpiryTime(data.mExpiryTimeMillis) + .setCaptive(data.mCaptive) + .setVenueFriendlyName(data.mVenueFriendlyName); + } + + /** + * Set the time at which data was last refreshed, as per {@link System#currentTimeMillis()}. + */ + @NonNull + public Builder setRefreshTime(long refreshTime) { + mRefreshTime = refreshTime; + return this; + } + + /** + * Set the URL to be used for users to login to the portal, if captive. + */ + @NonNull + public Builder setUserPortalUrl(@Nullable Uri userPortalUrl) { + return setUserPortalUrl(userPortalUrl, CAPTIVE_PORTAL_DATA_SOURCE_OTHER); + } + + /** + * Set the URL to be used for users to login to the portal, if captive, and the source of + * the data, see {@link CaptivePortalDataSource} + */ + @NonNull + public Builder setUserPortalUrl(@Nullable Uri userPortalUrl, + @CaptivePortalDataSource int source) { + mUserPortalUrl = userPortalUrl; + mUserPortalUrlSource = source; + return this; + } + + /** + * Set the URL that can be used by users to view information about the network venue. + */ + @NonNull + public Builder setVenueInfoUrl(@Nullable Uri venueInfoUrl) { + return setVenueInfoUrl(venueInfoUrl, CAPTIVE_PORTAL_DATA_SOURCE_OTHER); + } + + /** + * Set the URL that can be used by users to view information about the network venue, and + * the source of the data, see {@link CaptivePortalDataSource} + */ + @NonNull + public Builder setVenueInfoUrl(@Nullable Uri venueInfoUrl, + @CaptivePortalDataSource int source) { + mVenueInfoUrl = venueInfoUrl; + mVenueInfoUrlSource = source; + return this; + } + + /** + * Set whether the portal supports extending a user session on the portal URL page. + */ + @NonNull + public Builder setSessionExtendable(boolean sessionExtendable) { + mIsSessionExtendable = sessionExtendable; + return this; + } + + /** + * Set the number of bytes remaining on the network before the portal closes. + */ + @NonNull + public Builder setBytesRemaining(long bytesRemaining) { + mBytesRemaining = bytesRemaining; + return this; + } + + /** + * Set the time at the session will expire, as per {@link System#currentTimeMillis()}. + */ + @NonNull + public Builder setExpiryTime(long expiryTime) { + mExpiryTime = expiryTime; + return this; + } + + /** + * Set whether the network is captive (portal closed). + */ + @NonNull + public Builder setCaptive(boolean captive) { + mCaptive = captive; + return this; + } + + /** + * Set the venue friendly name. + */ + @NonNull + public Builder setVenueFriendlyName(@Nullable CharSequence venueFriendlyName) { + mVenueFriendlyName = venueFriendlyName; + return this; + } + + /** + * Create a new {@link CaptivePortalData}. + */ + @NonNull + public CaptivePortalData build() { + return new CaptivePortalData(mRefreshTime, mUserPortalUrl, mVenueInfoUrl, + mIsSessionExtendable, mBytesRemaining, mExpiryTime, mCaptive, + mVenueFriendlyName, mVenueInfoUrlSource, + mUserPortalUrlSource); + } + } + + /** + * Get the time at which data was last refreshed, as per {@link System#currentTimeMillis()}. + */ + public long getRefreshTimeMillis() { + return mRefreshTimeMillis; + } + + /** + * Get the URL to be used for users to login to the portal, or extend their session if + * {@link #isSessionExtendable()} is true. + */ + @Nullable + public Uri getUserPortalUrl() { + return mUserPortalUrl; + } + + /** + * Get the URL that can be used by users to view information about the network venue. + */ + @Nullable + public Uri getVenueInfoUrl() { + return mVenueInfoUrl; + } + + /** + * Indicates whether the user portal URL can be used to extend sessions, when the user is logged + * in and the session has a time or byte limit. + */ + public boolean isSessionExtendable() { + return mIsSessionExtendable; + } + + /** + * Get the remaining bytes on the captive portal session, at the time {@link CaptivePortalData} + * was refreshed. This may be different from the limit currently enforced by the portal. + * @return The byte limit, or -1 if not set. + */ + public long getByteLimit() { + return mByteLimit; + } + + /** + * Get the time at the session will expire, as per {@link System#currentTimeMillis()}. + * @return The expiry time, or -1 if unset. + */ + public long getExpiryTimeMillis() { + return mExpiryTimeMillis; + } + + /** + * Get whether the network is captive (portal closed). + */ + public boolean isCaptive() { + return mCaptive; + } + + /** + * Get the information source of the Venue URL + * @return The source that the Venue URL was obtained from + */ + public @CaptivePortalDataSource int getVenueInfoUrlSource() { + return mVenueInfoUrlSource; + } + + /** + * Get the information source of the user portal URL + * @return The source that the user portal URL was obtained from + */ + public @CaptivePortalDataSource int getUserPortalUrlSource() { + return mUserPortalUrlSource; + } + + /** + * Get the venue friendly name + */ + @Nullable + public CharSequence getVenueFriendlyName() { + return mVenueFriendlyName; + } + + @NonNull + public static final Creator<CaptivePortalData> CREATOR = new Creator<CaptivePortalData>() { + @Override + public CaptivePortalData createFromParcel(Parcel source) { + return new CaptivePortalData(source); + } + + @Override + public CaptivePortalData[] newArray(int size) { + return new CaptivePortalData[size]; + } + }; + + @Override + public int hashCode() { + return Objects.hash(mRefreshTimeMillis, mUserPortalUrl, mVenueInfoUrl, + mIsSessionExtendable, mByteLimit, mExpiryTimeMillis, mCaptive, mVenueFriendlyName, + mVenueInfoUrlSource, mUserPortalUrlSource); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof CaptivePortalData)) return false; + final CaptivePortalData other = (CaptivePortalData) obj; + return mRefreshTimeMillis == other.mRefreshTimeMillis + && Objects.equals(mUserPortalUrl, other.mUserPortalUrl) + && Objects.equals(mVenueInfoUrl, other.mVenueInfoUrl) + && mIsSessionExtendable == other.mIsSessionExtendable + && mByteLimit == other.mByteLimit + && mExpiryTimeMillis == other.mExpiryTimeMillis + && mCaptive == other.mCaptive + && Objects.equals(mVenueFriendlyName, other.mVenueFriendlyName) + && mVenueInfoUrlSource == other.mVenueInfoUrlSource + && mUserPortalUrlSource == other.mUserPortalUrlSource; + } + + @Override + public String toString() { + return "CaptivePortalData {" + + "refreshTime: " + mRefreshTimeMillis + + ", userPortalUrl: " + mUserPortalUrl + + ", venueInfoUrl: " + mVenueInfoUrl + + ", isSessionExtendable: " + mIsSessionExtendable + + ", byteLimit: " + mByteLimit + + ", expiryTime: " + mExpiryTimeMillis + + ", captive: " + mCaptive + + ", venueFriendlyName: " + mVenueFriendlyName + + ", venueInfoUrlSource: " + mVenueInfoUrlSource + + ", userPortalUrlSource: " + mUserPortalUrlSource + + "}"; + } +} diff --git a/framework/src/android/net/ConnectionInfo.aidl b/framework/src/android/net/ConnectionInfo.aidl new file mode 100644 index 0000000000000000000000000000000000000000..07faf8bbbed84eb8c714a1921093b10b5d2aa6e4 --- /dev/null +++ b/framework/src/android/net/ConnectionInfo.aidl @@ -0,0 +1,20 @@ +/* +** +** 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; + +parcelable ConnectionInfo; diff --git a/framework/src/android/net/ConnectionInfo.java b/framework/src/android/net/ConnectionInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..4514a8484d96ded59afc2ac78a26cac9bb373908 --- /dev/null +++ b/framework/src/android/net/ConnectionInfo.java @@ -0,0 +1,83 @@ +/* + * 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; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +/** + * Describe a network connection including local and remote address/port of a connection and the + * transport protocol. + * + * @hide + */ +public final class ConnectionInfo implements Parcelable { + public final int protocol; + public final InetSocketAddress local; + public final InetSocketAddress remote; + + @Override + public int describeContents() { + return 0; + } + + public ConnectionInfo(int protocol, InetSocketAddress local, InetSocketAddress remote) { + this.protocol = protocol; + this.local = local; + this.remote = remote; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(protocol); + out.writeByteArray(local.getAddress().getAddress()); + out.writeInt(local.getPort()); + out.writeByteArray(remote.getAddress().getAddress()); + out.writeInt(remote.getPort()); + } + + public static final @android.annotation.NonNull Creator<ConnectionInfo> CREATOR = new Creator<ConnectionInfo>() { + public ConnectionInfo createFromParcel(Parcel in) { + int protocol = in.readInt(); + InetAddress localAddress; + try { + localAddress = InetAddress.getByAddress(in.createByteArray()); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Invalid InetAddress"); + } + int localPort = in.readInt(); + InetAddress remoteAddress; + try { + remoteAddress = InetAddress.getByAddress(in.createByteArray()); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Invalid InetAddress"); + } + int remotePort = in.readInt(); + InetSocketAddress local = new InetSocketAddress(localAddress, localPort); + InetSocketAddress remote = new InetSocketAddress(remoteAddress, remotePort); + return new ConnectionInfo(protocol, local, remote); + } + + public ConnectionInfo[] newArray(int size) { + return new ConnectionInfo[size]; + } + }; +} diff --git a/framework/src/android/net/ConnectivityAnnotations.java b/framework/src/android/net/ConnectivityAnnotations.java new file mode 100644 index 0000000000000000000000000000000000000000..eb1faa0aa25c30c44b128f1cc704d7875836fe07 --- /dev/null +++ b/framework/src/android/net/ConnectivityAnnotations.java @@ -0,0 +1,51 @@ +/* + * 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.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Type annotations for constants used in the connectivity API surface. + * + * The annotations are maintained in a separate class so that it can be built as + * a separate library that other modules can build against, as Typedef should not + * be exposed as SystemApi. + * + * @hide + */ +public final class ConnectivityAnnotations { + private ConnectivityAnnotations() {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { + ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER, + ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY, + ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE, + }) + public @interface MultipathPreference {} + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, value = { + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED, + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED, + ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED, + }) + public @interface RestrictBackgroundStatus {} +} diff --git a/framework/src/android/net/ConnectivityDiagnosticsManager.java b/framework/src/android/net/ConnectivityDiagnosticsManager.java new file mode 100644 index 0000000000000000000000000000000000000000..dcc8a5eacd1386e03688035e54f68465ef25fe0b --- /dev/null +++ b/framework/src/android/net/ConnectivityDiagnosticsManager.java @@ -0,0 +1,778 @@ +/* + * 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.content.Context; +import android.os.Binder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.RemoteException; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; + +/** + * Class that provides utilities for collecting network connectivity diagnostics information. + * Connectivity information is made available through triggerable diagnostics tools and by listening + * to System validations. Some diagnostics information may be permissions-restricted. + * + * <p>ConnectivityDiagnosticsManager is intended for use by applications offering network + * connectivity on a user device. These tools will provide several mechanisms for these applications + * to be alerted to network conditions as well as diagnose potential network issues themselves. + * + * <p>The primary responsibilities of this class are to: + * + * <ul> + * <li>Allow permissioned applications to register and unregister callbacks for network event + * notifications + * <li>Invoke callbacks for network event notifications, including: + * <ul> + * <li>Network validations + * <li>Data stalls + * <li>Connectivity reports from applications + * </ul> + * </ul> + */ +public class ConnectivityDiagnosticsManager { + /** @hide */ + @VisibleForTesting + public static final Map<ConnectivityDiagnosticsCallback, ConnectivityDiagnosticsBinder> + sCallbacks = new ConcurrentHashMap<>(); + + private final Context mContext; + private final IConnectivityManager mService; + + /** @hide */ + public ConnectivityDiagnosticsManager(Context context, IConnectivityManager service) { + mContext = Objects.requireNonNull(context, "missing context"); + mService = Objects.requireNonNull(service, "missing IConnectivityManager"); + } + + /** @hide */ + @VisibleForTesting + public static boolean persistableBundleEquals( + @Nullable PersistableBundle a, @Nullable PersistableBundle b) { + if (a == b) return true; + if (a == null || b == null) return false; + if (!Objects.equals(a.keySet(), b.keySet())) return false; + for (String key : a.keySet()) { + if (!Objects.equals(a.get(key), b.get(key))) return false; + } + return true; + } + + /** Class that includes connectivity information for a specific Network at a specific time. */ + public static final class ConnectivityReport implements Parcelable { + /** + * The overall status of the network is that it is invalid; it neither provides + * connectivity nor has been exempted from validation. + */ + public static final int NETWORK_VALIDATION_RESULT_INVALID = 0; + + /** + * The overall status of the network is that it is valid, this may be because it provides + * full Internet access (all probes succeeded), or because other properties of the network + * caused probes not to be run. + */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID + public static final int NETWORK_VALIDATION_RESULT_VALID = 1; + + /** + * The overall status of the network is that it provides partial connectivity; some + * probed services succeeded but others failed. + */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL; + public static final int NETWORK_VALIDATION_RESULT_PARTIALLY_VALID = 2; + + /** + * Due to the properties of the network, validation was not performed. + */ + public static final int NETWORK_VALIDATION_RESULT_SKIPPED = 3; + + /** @hide */ + @IntDef( + prefix = {"NETWORK_VALIDATION_RESULT_"}, + value = { + NETWORK_VALIDATION_RESULT_INVALID, + NETWORK_VALIDATION_RESULT_VALID, + NETWORK_VALIDATION_RESULT_PARTIALLY_VALID, + NETWORK_VALIDATION_RESULT_SKIPPED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NetworkValidationResult {} + + /** + * The overall validation result for the Network being reported on. + * + * <p>The possible values for this key are: + * {@link #NETWORK_VALIDATION_RESULT_INVALID}, + * {@link #NETWORK_VALIDATION_RESULT_VALID}, + * {@link #NETWORK_VALIDATION_RESULT_PARTIALLY_VALID}, + * {@link #NETWORK_VALIDATION_RESULT_SKIPPED}. + * + * @see android.net.NetworkCapabilities#NET_CAPABILITY_VALIDATED + */ + @NetworkValidationResult + public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult"; + + /** DNS probe. */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS + public static final int NETWORK_PROBE_DNS = 0x04; + + /** HTTP probe. */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP + public static final int NETWORK_PROBE_HTTP = 0x08; + + /** HTTPS probe. */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS; + public static final int NETWORK_PROBE_HTTPS = 0x10; + + /** Captive portal fallback probe. */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_FALLBACK + public static final int NETWORK_PROBE_FALLBACK = 0x20; + + /** Private DNS (DNS over TLS) probd. */ + // TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS + public static final int NETWORK_PROBE_PRIVATE_DNS = 0x40; + + /** @hide */ + @IntDef( + prefix = {"NETWORK_PROBE_"}, + value = { + NETWORK_PROBE_DNS, + NETWORK_PROBE_HTTP, + NETWORK_PROBE_HTTPS, + NETWORK_PROBE_FALLBACK, + NETWORK_PROBE_PRIVATE_DNS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface NetworkProbe {} + + /** + * A bitmask of network validation probes that succeeded. + * + * <p>The possible bits values reported by this key are: + * {@link #NETWORK_PROBE_DNS}, + * {@link #NETWORK_PROBE_HTTP}, + * {@link #NETWORK_PROBE_HTTPS}, + * {@link #NETWORK_PROBE_FALLBACK}, + * {@link #NETWORK_PROBE_PRIVATE_DNS}. + */ + @NetworkProbe + public static final String KEY_NETWORK_PROBES_SUCCEEDED_BITMASK = + "networkProbesSucceeded"; + + /** + * A bitmask of network validation probes that were attempted. + * + * <p>These probes may have failed or may be incomplete at the time of this report. + * + * <p>The possible bits values reported by this key are: + * {@link #NETWORK_PROBE_DNS}, + * {@link #NETWORK_PROBE_HTTP}, + * {@link #NETWORK_PROBE_HTTPS}, + * {@link #NETWORK_PROBE_FALLBACK}, + * {@link #NETWORK_PROBE_PRIVATE_DNS}. + */ + @NetworkProbe + public static final String KEY_NETWORK_PROBES_ATTEMPTED_BITMASK = + "networkProbesAttempted"; + + /** @hide */ + @StringDef(prefix = {"KEY_"}, value = { + KEY_NETWORK_VALIDATION_RESULT, KEY_NETWORK_PROBES_SUCCEEDED_BITMASK, + KEY_NETWORK_PROBES_ATTEMPTED_BITMASK}) + @Retention(RetentionPolicy.SOURCE) + public @interface ConnectivityReportBundleKeys {} + + /** The Network for which this ConnectivityReport applied */ + @NonNull private final Network mNetwork; + + /** + * The timestamp for the report. The timestamp is taken from {@link + * System#currentTimeMillis}. + */ + private final long mReportTimestamp; + + /** LinkProperties available on the Network at the reported timestamp */ + @NonNull private final LinkProperties mLinkProperties; + + /** NetworkCapabilities available on the Network at the reported timestamp */ + @NonNull private final NetworkCapabilities mNetworkCapabilities; + + /** PersistableBundle that may contain additional info about the report */ + @NonNull private final PersistableBundle mAdditionalInfo; + + /** + * Constructor for ConnectivityReport. + * + * <p>Apps should obtain instances through {@link + * ConnectivityDiagnosticsCallback#onConnectivityReportAvailable} instead of instantiating + * their own instances (unless for testing purposes). + * + * @param network The Network for which this ConnectivityReport applies + * @param reportTimestamp The timestamp for the report + * @param linkProperties The LinkProperties available on network at reportTimestamp + * @param networkCapabilities The NetworkCapabilities available on network at + * reportTimestamp + * @param additionalInfo A PersistableBundle that may contain additional info about the + * report + */ + public ConnectivityReport( + @NonNull Network network, + long reportTimestamp, + @NonNull LinkProperties linkProperties, + @NonNull NetworkCapabilities networkCapabilities, + @NonNull PersistableBundle additionalInfo) { + mNetwork = network; + mReportTimestamp = reportTimestamp; + mLinkProperties = new LinkProperties(linkProperties); + mNetworkCapabilities = new NetworkCapabilities(networkCapabilities); + mAdditionalInfo = additionalInfo; + } + + /** + * Returns the Network for this ConnectivityReport. + * + * @return The Network for which this ConnectivityReport applied + */ + @NonNull + public Network getNetwork() { + return mNetwork; + } + + /** + * Returns the epoch timestamp (milliseconds) for when this report was taken. + * + * @return The timestamp for the report. Taken from {@link System#currentTimeMillis}. + */ + public long getReportTimestamp() { + return mReportTimestamp; + } + + /** + * Returns the LinkProperties available when this report was taken. + * + * @return LinkProperties available on the Network at the reported timestamp + */ + @NonNull + public LinkProperties getLinkProperties() { + return new LinkProperties(mLinkProperties); + } + + /** + * Returns the NetworkCapabilities when this report was taken. + * + * @return NetworkCapabilities available on the Network at the reported timestamp + */ + @NonNull + public NetworkCapabilities getNetworkCapabilities() { + return new NetworkCapabilities(mNetworkCapabilities); + } + + /** + * Returns a PersistableBundle with additional info for this report. + * + * @return PersistableBundle that may contain additional info about the report + */ + @NonNull + public PersistableBundle getAdditionalInfo() { + return new PersistableBundle(mAdditionalInfo); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof ConnectivityReport)) return false; + final ConnectivityReport that = (ConnectivityReport) o; + + // PersistableBundle is optimized to avoid unparcelling data unless fields are + // referenced. Because of this, use {@link ConnectivityDiagnosticsManager#equals} over + // {@link PersistableBundle#kindofEquals}. + return mReportTimestamp == that.mReportTimestamp + && mNetwork.equals(that.mNetwork) + && mLinkProperties.equals(that.mLinkProperties) + && mNetworkCapabilities.equals(that.mNetworkCapabilities) + && persistableBundleEquals(mAdditionalInfo, that.mAdditionalInfo); + } + + @Override + public int hashCode() { + return Objects.hash( + mNetwork, + mReportTimestamp, + mLinkProperties, + mNetworkCapabilities, + mAdditionalInfo); + } + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mNetwork, flags); + dest.writeLong(mReportTimestamp); + dest.writeParcelable(mLinkProperties, flags); + dest.writeParcelable(mNetworkCapabilities, flags); + dest.writeParcelable(mAdditionalInfo, flags); + } + + /** Implement the Parcelable interface */ + public static final @NonNull Creator<ConnectivityReport> CREATOR = + new Creator<ConnectivityReport>() { + public ConnectivityReport createFromParcel(Parcel in) { + return new ConnectivityReport( + in.readParcelable(null), + in.readLong(), + in.readParcelable(null), + in.readParcelable(null), + in.readParcelable(null)); + } + + public ConnectivityReport[] newArray(int size) { + return new ConnectivityReport[size]; + } + }; + } + + /** Class that includes information for a suspected data stall on a specific Network */ + public static final class DataStallReport implements Parcelable { + /** + * Indicates that the Data Stall was detected using DNS events. + */ + public static final int DETECTION_METHOD_DNS_EVENTS = 1; + + /** + * Indicates that the Data Stall was detected using TCP metrics. + */ + public static final int DETECTION_METHOD_TCP_METRICS = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"DETECTION_METHOD_"}, + value = {DETECTION_METHOD_DNS_EVENTS, DETECTION_METHOD_TCP_METRICS}) + public @interface DetectionMethod {} + + /** + * This key represents the period in milliseconds over which other included TCP metrics + * were measured. + * + * <p>This key will be included if the data stall detection method is + * {@link #DETECTION_METHOD_TCP_METRICS}. + * + * <p>This value is an int. + */ + public static final String KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS = + "tcpMetricsCollectionPeriodMillis"; + + /** + * This key represents the fail rate of TCP packets when the suspected data stall was + * detected. + * + * <p>This key will be included if the data stall detection method is + * {@link #DETECTION_METHOD_TCP_METRICS}. + * + * <p>This value is an int percentage between 0 and 100. + */ + public static final String KEY_TCP_PACKET_FAIL_RATE = "tcpPacketFailRate"; + + /** + * This key represents the consecutive number of DNS timeouts that have occurred. + * + * <p>The consecutive count will be reset any time a DNS response is received. + * + * <p>This key will be included if the data stall detection method is + * {@link #DETECTION_METHOD_DNS_EVENTS}. + * + * <p>This value is an int. + */ + public static final String KEY_DNS_CONSECUTIVE_TIMEOUTS = "dnsConsecutiveTimeouts"; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef(prefix = {"KEY_"}, value = { + KEY_TCP_PACKET_FAIL_RATE, + KEY_DNS_CONSECUTIVE_TIMEOUTS + }) + public @interface DataStallReportBundleKeys {} + + /** The Network for which this DataStallReport applied */ + @NonNull private final Network mNetwork; + + /** + * The timestamp for the report. The timestamp is taken from {@link + * System#currentTimeMillis}. + */ + private long mReportTimestamp; + + /** A bitmask of the detection methods used to identify the suspected data stall */ + @DetectionMethod private final int mDetectionMethod; + + /** LinkProperties available on the Network at the reported timestamp */ + @NonNull private final LinkProperties mLinkProperties; + + /** NetworkCapabilities available on the Network at the reported timestamp */ + @NonNull private final NetworkCapabilities mNetworkCapabilities; + + /** PersistableBundle that may contain additional information on the suspected data stall */ + @NonNull private final PersistableBundle mStallDetails; + + /** + * Constructor for DataStallReport. + * + * <p>Apps should obtain instances through {@link + * ConnectivityDiagnosticsCallback#onDataStallSuspected} instead of instantiating their own + * instances (unless for testing purposes). + * + * @param network The Network for which this DataStallReport applies + * @param reportTimestamp The timestamp for the report + * @param detectionMethod The detection method used to identify this data stall + * @param linkProperties The LinkProperties available on network at reportTimestamp + * @param networkCapabilities The NetworkCapabilities available on network at + * reportTimestamp + * @param stallDetails A PersistableBundle that may contain additional info about the report + */ + public DataStallReport( + @NonNull Network network, + long reportTimestamp, + @DetectionMethod int detectionMethod, + @NonNull LinkProperties linkProperties, + @NonNull NetworkCapabilities networkCapabilities, + @NonNull PersistableBundle stallDetails) { + mNetwork = network; + mReportTimestamp = reportTimestamp; + mDetectionMethod = detectionMethod; + mLinkProperties = new LinkProperties(linkProperties); + mNetworkCapabilities = new NetworkCapabilities(networkCapabilities); + mStallDetails = stallDetails; + } + + /** + * Returns the Network for this DataStallReport. + * + * @return The Network for which this DataStallReport applied + */ + @NonNull + public Network getNetwork() { + return mNetwork; + } + + /** + * Returns the epoch timestamp (milliseconds) for when this report was taken. + * + * @return The timestamp for the report. Taken from {@link System#currentTimeMillis}. + */ + public long getReportTimestamp() { + return mReportTimestamp; + } + + /** + * Returns the bitmask of detection methods used to identify this suspected data stall. + * + * @return The bitmask of detection methods used to identify the suspected data stall + */ + public int getDetectionMethod() { + return mDetectionMethod; + } + + /** + * Returns the LinkProperties available when this report was taken. + * + * @return LinkProperties available on the Network at the reported timestamp + */ + @NonNull + public LinkProperties getLinkProperties() { + return new LinkProperties(mLinkProperties); + } + + /** + * Returns the NetworkCapabilities when this report was taken. + * + * @return NetworkCapabilities available on the Network at the reported timestamp + */ + @NonNull + public NetworkCapabilities getNetworkCapabilities() { + return new NetworkCapabilities(mNetworkCapabilities); + } + + /** + * Returns a PersistableBundle with additional info for this report. + * + * <p>Gets a bundle with details about the suspected data stall including information + * specific to the monitoring method that detected the data stall. + * + * @return PersistableBundle that may contain additional information on the suspected data + * stall + */ + @NonNull + public PersistableBundle getStallDetails() { + return new PersistableBundle(mStallDetails); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof DataStallReport)) return false; + final DataStallReport that = (DataStallReport) o; + + // PersistableBundle is optimized to avoid unparcelling data unless fields are + // referenced. Because of this, use {@link ConnectivityDiagnosticsManager#equals} over + // {@link PersistableBundle#kindofEquals}. + return mReportTimestamp == that.mReportTimestamp + && mDetectionMethod == that.mDetectionMethod + && mNetwork.equals(that.mNetwork) + && mLinkProperties.equals(that.mLinkProperties) + && mNetworkCapabilities.equals(that.mNetworkCapabilities) + && persistableBundleEquals(mStallDetails, that.mStallDetails); + } + + @Override + public int hashCode() { + return Objects.hash( + mNetwork, + mReportTimestamp, + mDetectionMethod, + mLinkProperties, + mNetworkCapabilities, + mStallDetails); + } + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mNetwork, flags); + dest.writeLong(mReportTimestamp); + dest.writeInt(mDetectionMethod); + dest.writeParcelable(mLinkProperties, flags); + dest.writeParcelable(mNetworkCapabilities, flags); + dest.writeParcelable(mStallDetails, flags); + } + + /** Implement the Parcelable interface */ + public static final @NonNull Creator<DataStallReport> CREATOR = + new Creator<DataStallReport>() { + public DataStallReport createFromParcel(Parcel in) { + return new DataStallReport( + in.readParcelable(null), + in.readLong(), + in.readInt(), + in.readParcelable(null), + in.readParcelable(null), + in.readParcelable(null)); + } + + public DataStallReport[] newArray(int size) { + return new DataStallReport[size]; + } + }; + } + + /** @hide */ + @VisibleForTesting + public static class ConnectivityDiagnosticsBinder + extends IConnectivityDiagnosticsCallback.Stub { + @NonNull private final ConnectivityDiagnosticsCallback mCb; + @NonNull private final Executor mExecutor; + + /** @hide */ + @VisibleForTesting + public ConnectivityDiagnosticsBinder( + @NonNull ConnectivityDiagnosticsCallback cb, @NonNull Executor executor) { + this.mCb = cb; + this.mExecutor = executor; + } + + /** @hide */ + @VisibleForTesting + public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mCb.onConnectivityReportAvailable(report); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** @hide */ + @VisibleForTesting + public void onDataStallSuspected(@NonNull DataStallReport report) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mCb.onDataStallSuspected(report); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** @hide */ + @VisibleForTesting + public void onNetworkConnectivityReported( + @NonNull Network network, boolean hasConnectivity) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mCb.onNetworkConnectivityReported(network, hasConnectivity); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + } + + /** + * Abstract base class for Connectivity Diagnostics callbacks. Used for notifications about + * network connectivity events. Must be extended by applications wanting notifications. + */ + public abstract static class ConnectivityDiagnosticsCallback { + /** + * Called when the platform completes a data connectivity check. This will also be invoked + * immediately upon registration for each network matching the request with the latest + * report, if a report has already been generated for that network. + * + * <p>The Network specified in the ConnectivityReport may not be active any more when this + * method is invoked. + * + * @param report The ConnectivityReport containing information about a connectivity check + */ + public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) {} + + /** + * Called when the platform suspects a data stall on some Network. + * + * <p>The Network specified in the DataStallReport may not be active any more when this + * method is invoked. + * + * @param report The DataStallReport containing information about the suspected data stall + */ + public void onDataStallSuspected(@NonNull DataStallReport report) {} + + /** + * Called when any app reports connectivity to the System. + * + * @param network The Network for which connectivity has been reported + * @param hasConnectivity The connectivity reported to the System + */ + public void onNetworkConnectivityReported( + @NonNull Network network, boolean hasConnectivity) {} + } + + /** + * Registers a ConnectivityDiagnosticsCallback with the System. + * + * <p>Only apps that offer network connectivity to the user should be registering callbacks. + * These are the only apps whose callbacks will be invoked by the system. Apps considered to + * meet these conditions include: + * + * <ul> + * <li>Carrier apps with active subscriptions + * <li>Active VPNs + * <li>WiFi Suggesters + * </ul> + * + * <p>Callbacks registered by apps not meeting the above criteria will not be invoked. + * + * <p>If a registering app loses its relevant permissions, any callbacks it registered will + * silently stop receiving callbacks. Note that registering apps must also have location + * permissions to receive callbacks as some Networks may be location-bound (such as WiFi + * networks). + * + * <p>Each register() call <b>MUST</b> use a ConnectivityDiagnosticsCallback instance that is + * not currently registered. If a ConnectivityDiagnosticsCallback instance is registered with + * multiple NetworkRequests, an IllegalArgumentException will be thrown. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * callbacks in {@link ConnectivityManager}. Registering a callback with this method will count + * toward this limit. If this limit is exceeded, an exception will be thrown. To avoid hitting + * this issue and to conserve resources, make sure to unregister the callbacks with + * {@link #unregisterConnectivityDiagnosticsCallback}. + * + * @param request The NetworkRequest that will be used to match with Networks for which + * callbacks will be fired + * @param e The Executor to be used for running the callback method invocations + * @param callback The ConnectivityDiagnosticsCallback that the caller wants registered with the + * System + * @throws IllegalArgumentException if the same callback instance is registered with multiple + * NetworkRequests + * @throws RuntimeException if the app already has too many callbacks registered. + */ + public void registerConnectivityDiagnosticsCallback( + @NonNull NetworkRequest request, + @NonNull Executor e, + @NonNull ConnectivityDiagnosticsCallback callback) { + final ConnectivityDiagnosticsBinder binder = new ConnectivityDiagnosticsBinder(callback, e); + if (sCallbacks.putIfAbsent(callback, binder) != null) { + throw new IllegalArgumentException("Callback is currently registered"); + } + + try { + mService.registerConnectivityDiagnosticsCallback( + binder, request, mContext.getOpPackageName()); + } catch (RemoteException exception) { + exception.rethrowFromSystemServer(); + } + } + + /** + * Unregisters a ConnectivityDiagnosticsCallback with the System. + * + * <p>If the given callback is not currently registered with the System, this operation will be + * a no-op. + * + * @param callback The ConnectivityDiagnosticsCallback to be unregistered from the System. + */ + public void unregisterConnectivityDiagnosticsCallback( + @NonNull ConnectivityDiagnosticsCallback callback) { + // unconditionally removing from sCallbacks prevents race conditions here, since remove() is + // atomic. + final ConnectivityDiagnosticsBinder binder = sCallbacks.remove(callback); + if (binder == null) return; + + try { + mService.unregisterConnectivityDiagnosticsCallback(binder); + } catch (RemoteException exception) { + exception.rethrowFromSystemServer(); + } + } +} diff --git a/framework/src/android/net/ConnectivityFrameworkInitializer.java b/framework/src/android/net/ConnectivityFrameworkInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..a2e218dcbb4bc5a3e4f670029fafabcbc7aad256 --- /dev/null +++ b/framework/src/android/net/ConnectivityFrameworkInitializer.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.content.Context; + +/** + * Class for performing registration for all core connectivity services. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class ConnectivityFrameworkInitializer { + private ConnectivityFrameworkInitializer() {} + + /** + * Called by {@link SystemServiceRegistry}'s static initializer and registers all core + * connectivity services to {@link Context}, so that {@link Context#getSystemService} can + * return them. + * + * @throws IllegalStateException if this is called anywhere besides + * {@link SystemServiceRegistry}. + */ + public static void registerServiceWrappers() { + // registerContextAwareService will throw if this is called outside of SystemServiceRegistry + // initialization. + SystemServiceRegistry.registerContextAwareService( + Context.CONNECTIVITY_SERVICE, + ConnectivityManager.class, + (context, serviceBinder) -> { + IConnectivityManager icm = IConnectivityManager.Stub.asInterface(serviceBinder); + return new ConnectivityManager(context, icm); + } + ); + + SystemServiceRegistry.registerContextAwareService( + Context.CONNECTIVITY_DIAGNOSTICS_SERVICE, + ConnectivityDiagnosticsManager.class, + (context) -> { + final ConnectivityManager cm = context.getSystemService( + ConnectivityManager.class); + return cm.createDiagnosticsManager(); + } + ); + + SystemServiceRegistry.registerContextAwareService( + Context.TEST_NETWORK_SERVICE, + TestNetworkManager.class, + context -> { + final ConnectivityManager cm = context.getSystemService( + ConnectivityManager.class); + return cm.startOrGetTestNetworkManager(); + } + ); + + SystemServiceRegistry.registerContextAwareService( + DnsResolverServiceManager.DNS_RESOLVER_SERVICE, + DnsResolverServiceManager.class, + (context, serviceBinder) -> new DnsResolverServiceManager(serviceBinder) + ); + } +} diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java new file mode 100644 index 0000000000000000000000000000000000000000..93455bc80087762464e669476839a076eda5fce1 --- /dev/null +++ b/framework/src/android/net/ConnectivityManager.java @@ -0,0 +1,5459 @@ +/* + * 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; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST; +import static android.net.NetworkRequest.Type.LISTEN; +import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST; +import static android.net.NetworkRequest.Type.REQUEST; +import static android.net.NetworkRequest.Type.TRACK_DEFAULT; +import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT; +import static android.net.QosCallback.QosCallbackRegistrationException; + +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.app.PendingIntent; +import android.app.admin.DevicePolicyManager; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityAnnotations.MultipathPreference; +import android.net.ConnectivityAnnotations.RestrictBackgroundStatus; +import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.SocketKeepalive.Callback; +import android.net.TetheringManager.StartTetheringCallback; +import android.net.TetheringManager.TetheringEventCallback; +import android.net.TetheringManager.TetheringRequest; +import android.net.wifi.WifiNetworkSuggestion; +import android.os.Binder; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceSpecificException; +import android.os.UserHandle; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Range; +import android.util.SparseIntArray; + +import com.android.internal.annotations.GuardedBy; + +import libcore.net.event.NetworkEventDispatcher; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; + +/** + * Class that answers queries about the state of network connectivity. It also + * notifies applications when network connectivity changes. + * <p> + * The primary responsibilities of this class are to: + * <ol> + * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li> + * <li>Send broadcast intents when network connectivity changes</li> + * <li>Attempt to "fail over" to another network when connectivity to a network + * is lost</li> + * <li>Provide an API that allows applications to query the coarse-grained or fine-grained + * state of the available networks</li> + * <li>Provide an API that allows applications to request and select networks for their data + * traffic</li> + * </ol> + */ +@SystemService(Context.CONNECTIVITY_SERVICE) +public class ConnectivityManager { + private static final String TAG = "ConnectivityManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * A change in network connectivity has occurred. A default connection has either + * been established or lost. The NetworkInfo for the affected network is + * sent as an extra; it should be consulted to see what kind of + * connectivity event occurred. + * <p/> + * Apps targeting Android 7.0 (API level 24) and higher do not receive this + * broadcast if they declare the broadcast receiver in their manifest. Apps + * will still receive broadcasts if they register their + * {@link android.content.BroadcastReceiver} with + * {@link android.content.Context#registerReceiver Context.registerReceiver()} + * and that context is still valid. + * <p/> + * If this is a connection that was the result of failing over from a + * disconnected network, then the FAILOVER_CONNECTION boolean extra is + * set to true. + * <p/> + * For a loss of connectivity, if the connectivity manager is attempting + * to connect (or has already connected) to another network, the + * NetworkInfo for the new network is also passed as an extra. This lets + * any receivers of the broadcast know that they should not necessarily + * tell the user that no data traffic will be possible. Instead, the + * receiver should expect another broadcast soon, indicating either that + * the failover attempt succeeded (and so there is still overall data + * connectivity), or that the failover attempt failed, meaning that all + * connectivity has been lost. + * <p/> + * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY + * is set to {@code true} if there are no connected networks at all. + * + * @deprecated apps should use the more versatile {@link #requestNetwork}, + * {@link #registerNetworkCallback} or {@link #registerDefaultNetworkCallback} + * functions instead for faster and more detailed updates about the network + * changes they care about. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @Deprecated + public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; + + /** + * The device has connected to a network that has presented a captive + * portal, which is blocking Internet connectivity. The user was presented + * with a notification that network sign in is required, + * and the user invoked the notification's action indicating they + * desire to sign in to the network. Apps handling this activity should + * facilitate signing in to the network. This action includes a + * {@link Network} typed extra called {@link #EXTRA_NETWORK} that represents + * the network presenting the captive portal; all communication with the + * captive portal must be done using this {@code Network} object. + * <p/> + * This activity includes a {@link CaptivePortal} extra named + * {@link #EXTRA_CAPTIVE_PORTAL} that can be used to indicate different + * outcomes of the captive portal sign in to the system: + * <ul> + * <li> When the app handling this action believes the user has signed in to + * the network and the captive portal has been dismissed, the app should + * call {@link CaptivePortal#reportCaptivePortalDismissed} so the system can + * reevaluate the network. If reevaluation finds the network no longer + * subject to a captive portal, the network may become the default active + * data network.</li> + * <li> When the app handling this action believes the user explicitly wants + * to ignore the captive portal and the network, the app should call + * {@link CaptivePortal#ignoreNetwork}. </li> + * </ul> + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL"; + + /** + * The lookup key for a {@link NetworkInfo} object. Retrieve with + * {@link android.content.Intent#getParcelableExtra(String)}. + * + * @deprecated The {@link NetworkInfo} object is deprecated, as many of its properties + * can't accurately represent modern network characteristics. + * Please obtain information about networks from the {@link NetworkCapabilities} + * or {@link LinkProperties} objects instead. + */ + @Deprecated + public static final String EXTRA_NETWORK_INFO = "networkInfo"; + + /** + * Network type which triggered a {@link #CONNECTIVITY_ACTION} broadcast. + * + * @see android.content.Intent#getIntExtra(String, int) + * @deprecated The network type is not rich enough to represent the characteristics + * of modern networks. Please use {@link NetworkCapabilities} instead, + * in particular the transports. + */ + @Deprecated + public static final String EXTRA_NETWORK_TYPE = "networkType"; + + /** + * The lookup key for a boolean that indicates whether a connect event + * is for a network to which the connectivity manager was failing over + * following a disconnect on another network. + * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}. + * + * @deprecated See {@link NetworkInfo}. + */ + @Deprecated + public static final String EXTRA_IS_FAILOVER = "isFailover"; + /** + * The lookup key for a {@link NetworkInfo} object. This is supplied when + * there is another network that it may be possible to connect to. Retrieve with + * {@link android.content.Intent#getParcelableExtra(String)}. + * + * @deprecated See {@link NetworkInfo}. + */ + @Deprecated + public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork"; + /** + * The lookup key for a boolean that indicates whether there is a + * complete lack of connectivity, i.e., no network is available. + * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}. + */ + public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity"; + /** + * The lookup key for a string that indicates why an attempt to connect + * to a network failed. The string has no particular structure. It is + * intended to be used in notifications presented to users. Retrieve + * it with {@link android.content.Intent#getStringExtra(String)}. + */ + public static final String EXTRA_REASON = "reason"; + /** + * The lookup key for a string that provides optionally supplied + * extra information about the network state. The information + * may be passed up from the lower networking layers, and its + * meaning may be specific to a particular network type. Retrieve + * it with {@link android.content.Intent#getStringExtra(String)}. + * + * @deprecated See {@link NetworkInfo#getExtraInfo()}. + */ + @Deprecated + public static final String EXTRA_EXTRA_INFO = "extraInfo"; + /** + * The lookup key for an int that provides information about + * our connection to the internet at large. 0 indicates no connection, + * 100 indicates a great connection. Retrieve it with + * {@link android.content.Intent#getIntExtra(String, int)}. + * {@hide} + */ + public static final String EXTRA_INET_CONDITION = "inetCondition"; + /** + * The lookup key for a {@link CaptivePortal} object included with the + * {@link #ACTION_CAPTIVE_PORTAL_SIGN_IN} intent. The {@code CaptivePortal} + * object can be used to either indicate to the system that the captive + * portal has been dismissed or that the user does not want to pursue + * signing in to captive portal. Retrieve it with + * {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL"; + + /** + * Key for passing a URL to the captive portal login activity. + */ + public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL"; + + /** + * Key for passing a {@link android.net.captiveportal.CaptivePortalProbeSpec} to the captive + * portal login activity. + * {@hide} + */ + @SystemApi + public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC = + "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC"; + + /** + * Key for passing a user agent string to the captive portal login activity. + * {@hide} + */ + @SystemApi + public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT = + "android.net.extra.CAPTIVE_PORTAL_USER_AGENT"; + + /** + * Broadcast action to indicate the change of data activity status + * (idle or active) on a network in a recent period. + * The network becomes active when data transmission is started, or + * idle if there is no data transmission for a period of time. + * {@hide} + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DATA_ACTIVITY_CHANGE = + "android.net.conn.DATA_ACTIVITY_CHANGE"; + /** + * The lookup key for an enum that indicates the network device type on which this data activity + * change happens. + * {@hide} + */ + public static final String EXTRA_DEVICE_TYPE = "deviceType"; + /** + * The lookup key for a boolean that indicates the device is active or not. {@code true} means + * it is actively sending or receiving data and {@code false} means it is idle. + * {@hide} + */ + public static final String EXTRA_IS_ACTIVE = "isActive"; + /** + * The lookup key for a long that contains the timestamp (nanos) of the radio state change. + * {@hide} + */ + public static final String EXTRA_REALTIME_NS = "tsNanos"; + + /** + * Broadcast Action: The setting for background data usage has changed + * values. Use {@link #getBackgroundDataSetting()} to get the current value. + * <p> + * If an application uses the network in the background, it should listen + * for this broadcast and stop using the background data if the value is + * {@code false}. + * <p> + * + * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability + * of background data depends on several combined factors, and + * this broadcast is no longer sent. Instead, when background + * data is unavailable, {@link #getActiveNetworkInfo()} will now + * appear disconnected. During first boot after a platform + * upgrade, this broadcast will be sent once if + * {@link #getBackgroundDataSetting()} was {@code false} before + * the upgrade. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @Deprecated + public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = + "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"; + + /** + * Broadcast Action: The network connection may not be good + * uses {@code ConnectivityManager.EXTRA_INET_CONDITION} and + * {@code ConnectivityManager.EXTRA_NETWORK_INFO} to specify + * the network and it's condition. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @UnsupportedAppUsage + public static final String INET_CONDITION_ACTION = + "android.net.conn.INET_CONDITION_ACTION"; + + /** + * Broadcast Action: A tetherable connection has come or gone. + * Uses {@code ConnectivityManager.EXTRA_AVAILABLE_TETHER}, + * {@code ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY}, + * {@code ConnectivityManager.EXTRA_ACTIVE_TETHER}, and + * {@code ConnectivityManager.EXTRA_ERRORED_TETHER} to indicate + * the current state of tethering. Each include a list of + * interface names in that state (may be empty). + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static final String ACTION_TETHER_STATE_CHANGED = + TetheringManager.ACTION_TETHER_STATE_CHANGED; + + /** + * @hide + * gives a String[] listing all the interfaces configured for + * tethering and currently available for tethering. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static final String EXTRA_AVAILABLE_TETHER = TetheringManager.EXTRA_AVAILABLE_TETHER; + + /** + * @hide + * 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 = TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY; + + /** + * @hide + * gives a String[] listing all the interfaces currently tethered + * (ie, has DHCPv4 support and packets potentially forwarded/NATed) + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static final String EXTRA_ACTIVE_TETHER = TetheringManager.EXTRA_ACTIVE_TETHER; + + /** + * @hide + * 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. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static final String EXTRA_ERRORED_TETHER = TetheringManager.EXTRA_ERRORED_TETHER; + + /** + * Broadcast Action: The captive portal tracker has finished its test. + * Sent only while running Setup Wizard, in lieu of showing a user + * notification. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CAPTIVE_PORTAL_TEST_COMPLETED = + "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED"; + /** + * The lookup key for a boolean that indicates whether a captive portal was detected. + * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}. + * @hide + */ + public static final String EXTRA_IS_CAPTIVE_PORTAL = "captivePortal"; + + /** + * Action used to display a dialog that asks the user whether to connect to a network that is + * not validated. This intent is used to start the dialog in settings via startActivity. + * + * This action includes a {@link Network} typed extra which is called + * {@link ConnectivityManager#EXTRA_NETWORK} that represents the network which is unvalidated. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final String ACTION_PROMPT_UNVALIDATED = "android.net.action.PROMPT_UNVALIDATED"; + + /** + * Action used to display a dialog that asks the user whether to avoid a network that is no + * longer validated. This intent is used to start the dialog in settings via startActivity. + * + * This action includes a {@link Network} typed extra which is called + * {@link ConnectivityManager#EXTRA_NETWORK} that represents the network which is no longer + * validated. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final String ACTION_PROMPT_LOST_VALIDATION = + "android.net.action.PROMPT_LOST_VALIDATION"; + + /** + * Action used to display a dialog that asks the user whether to stay connected to a network + * that has not validated. This intent is used to start the dialog in settings via + * startActivity. + * + * This action includes a {@link Network} typed extra which is called + * {@link ConnectivityManager#EXTRA_NETWORK} that represents the network which has partial + * connectivity. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY = + "android.net.action.PROMPT_PARTIAL_CONNECTIVITY"; + + /** + * Clear DNS Cache Action: This is broadcast when networks have changed and old + * DNS entries should be cleared. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final String ACTION_CLEAR_DNS_CACHE = "android.net.action.CLEAR_DNS_CACHE"; + + /** + * Invalid tethering type. + * @see #startTethering(int, boolean, OnStartTetheringCallback) + * @hide + */ + public static final int TETHERING_INVALID = TetheringManager.TETHERING_INVALID; + + /** + * Wifi tethering type. + * @see #startTethering(int, boolean, OnStartTetheringCallback) + * @hide + */ + @SystemApi + public static final int TETHERING_WIFI = 0; + + /** + * USB tethering type. + * @see #startTethering(int, boolean, OnStartTetheringCallback) + * @hide + */ + @SystemApi + public static final int TETHERING_USB = 1; + + /** + * Bluetooth tethering type. + * @see #startTethering(int, boolean, OnStartTetheringCallback) + * @hide + */ + @SystemApi + 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(int, boolean, OnStartTetheringCallback). + * @hide + */ + public static final int TETHERING_WIFI_P2P = TetheringManager.TETHERING_WIFI_P2P; + + /** + * Extra used for communicating with the TetherService. Includes the type of tethering to + * enable if any. + * @hide + */ + public static final String EXTRA_ADD_TETHER_TYPE = TetheringConstants.EXTRA_ADD_TETHER_TYPE; + + /** + * Extra used for communicating with the TetherService. Includes the type of tethering for + * which to cancel provisioning. + * @hide + */ + public static final String EXTRA_REM_TETHER_TYPE = TetheringConstants.EXTRA_REM_TETHER_TYPE; + + /** + * Extra used for communicating with the TetherService. True to schedule a recheck of tether + * provisioning. + * @hide + */ + public static final String EXTRA_SET_ALARM = TetheringConstants.EXTRA_SET_ALARM; + + /** + * Tells the TetherService to run a provision check now. + * @hide + */ + public static final String EXTRA_RUN_PROVISION = TetheringConstants.EXTRA_RUN_PROVISION; + + /** + * Extra used for communicating with the TetherService. Contains the {@link ResultReceiver} + * which will receive provisioning results. Can be left empty. + * @hide + */ + public static final String EXTRA_PROVISION_CALLBACK = + TetheringConstants.EXTRA_PROVISION_CALLBACK; + + /** + * The absence of a connection type. + * @hide + */ + @SystemApi + public static final int TYPE_NONE = -1; + + /** + * A Mobile data connection. Devices may support more than one. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an + * appropriate network. {@see NetworkCapabilities} for supported transports. + */ + @Deprecated + public static final int TYPE_MOBILE = 0; + + /** + * A WIFI data connection. Devices may support more than one. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an + * appropriate network. {@see NetworkCapabilities} for supported transports. + */ + @Deprecated + public static final int TYPE_WIFI = 1; + + /** + * An MMS-specific Mobile data connection. This network type may use the + * same network interface as {@link #TYPE_MOBILE} or it may use a different + * one. This is used by applications needing to talk to the carrier's + * Multimedia Messaging Service servers. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that + * provides the {@link NetworkCapabilities#NET_CAPABILITY_MMS} capability. + */ + @Deprecated + public static final int TYPE_MOBILE_MMS = 2; + + /** + * A SUPL-specific Mobile data connection. This network type may use the + * same network interface as {@link #TYPE_MOBILE} or it may use a different + * one. This is used by applications needing to talk to the carrier's + * Secure User Plane Location servers for help locating the device. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that + * provides the {@link NetworkCapabilities#NET_CAPABILITY_SUPL} capability. + */ + @Deprecated + public static final int TYPE_MOBILE_SUPL = 3; + + /** + * A DUN-specific Mobile data connection. This network type may use the + * same network interface as {@link #TYPE_MOBILE} or it may use a different + * one. This is sometimes by the system when setting up an upstream connection + * for tethering so that the carrier is aware of DUN traffic. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that + * provides the {@link NetworkCapabilities#NET_CAPABILITY_DUN} capability. + */ + @Deprecated + public static final int TYPE_MOBILE_DUN = 4; + + /** + * A High Priority Mobile data connection. This network type uses the + * same network interface as {@link #TYPE_MOBILE} but the routing setup + * is different. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an + * appropriate network. {@see NetworkCapabilities} for supported transports. + */ + @Deprecated + public static final int TYPE_MOBILE_HIPRI = 5; + + /** + * A WiMAX data connection. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an + * appropriate network. {@see NetworkCapabilities} for supported transports. + */ + @Deprecated + public static final int TYPE_WIMAX = 6; + + /** + * A Bluetooth data connection. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an + * appropriate network. {@see NetworkCapabilities} for supported transports. + */ + @Deprecated + public static final int TYPE_BLUETOOTH = 7; + + /** + * Fake data connection. This should not be used on shipping devices. + * @deprecated This is not used any more. + */ + @Deprecated + public static final int TYPE_DUMMY = 8; + + /** + * An Ethernet data connection. + * + * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an + * appropriate network. {@see NetworkCapabilities} for supported transports. + */ + @Deprecated + public static final int TYPE_ETHERNET = 9; + + /** + * Over the air Administration. + * @deprecated Use {@link NetworkCapabilities} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + public static final int TYPE_MOBILE_FOTA = 10; + + /** + * IP Multimedia Subsystem. + * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_IMS} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage + public static final int TYPE_MOBILE_IMS = 11; + + /** + * Carrier Branded Services. + * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_CBS} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + public static final int TYPE_MOBILE_CBS = 12; + + /** + * A Wi-Fi p2p connection. Only requesting processes will have access to + * the peers connected. + * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_WIFI_P2P} instead. + * {@hide} + */ + @Deprecated + @SystemApi + public static final int TYPE_WIFI_P2P = 13; + + /** + * The network to use for initially attaching to the network + * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_IA} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage + public static final int TYPE_MOBILE_IA = 14; + + /** + * Emergency PDN connection for emergency services. This + * may include IMS and MMS in emergency situations. + * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_EIMS} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + public static final int TYPE_MOBILE_EMERGENCY = 15; + + /** + * The network that uses proxy to achieve connectivity. + * @deprecated Use {@link NetworkCapabilities} instead. + * {@hide} + */ + @Deprecated + @SystemApi + public static final int TYPE_PROXY = 16; + + /** + * A virtual network using one or more native bearers. + * It may or may not be providing security services. + * @deprecated Applications should use {@link NetworkCapabilities#TRANSPORT_VPN} instead. + */ + @Deprecated + public static final int TYPE_VPN = 17; + + /** + * A network that is exclusively meant to be used for testing + * + * @deprecated Use {@link NetworkCapabilities} instead. + * @hide + */ + @Deprecated + public static final int TYPE_TEST = 18; // TODO: Remove this once NetworkTypes are unused. + + /** + * @deprecated Use {@link NetworkCapabilities} instead. + * @hide + */ + @Deprecated + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_NONE, + TYPE_MOBILE, + TYPE_WIFI, + TYPE_MOBILE_MMS, + TYPE_MOBILE_SUPL, + TYPE_MOBILE_DUN, + TYPE_MOBILE_HIPRI, + TYPE_WIMAX, + TYPE_BLUETOOTH, + TYPE_DUMMY, + TYPE_ETHERNET, + TYPE_MOBILE_FOTA, + TYPE_MOBILE_IMS, + TYPE_MOBILE_CBS, + TYPE_WIFI_P2P, + TYPE_MOBILE_IA, + TYPE_MOBILE_EMERGENCY, + TYPE_PROXY, + TYPE_VPN, + TYPE_TEST + }) + public @interface LegacyNetworkType {} + + // Deprecated constants for return values of startUsingNetworkFeature. They used to live + // in com.android.internal.telephony.PhoneConstants until they were made inaccessible. + private static final int DEPRECATED_PHONE_CONSTANT_APN_ALREADY_ACTIVE = 0; + private static final int DEPRECATED_PHONE_CONSTANT_APN_REQUEST_STARTED = 1; + private static final int DEPRECATED_PHONE_CONSTANT_APN_REQUEST_FAILED = 3; + + /** {@hide} */ + public static final int MAX_RADIO_TYPE = TYPE_TEST; + + /** {@hide} */ + public static final int MAX_NETWORK_TYPE = TYPE_TEST; + + private static final int MIN_NETWORK_TYPE = TYPE_MOBILE; + + /** + * If you want to set the default network preference,you can directly + * change the networkAttributes array in framework's config.xml. + * + * @deprecated Since we support so many more networks now, the single + * network default network preference can't really express + * the hierarchy. Instead, the default is defined by the + * networkAttributes in config.xml. You can determine + * the current value by calling {@link #getNetworkPreference()} + * from an App. + */ + @Deprecated + public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI; + + /** + * @hide + */ + public static final int REQUEST_ID_UNSET = 0; + + /** + * Static unique request used as a tombstone for NetworkCallbacks that have been unregistered. + * This allows to distinguish when unregistering NetworkCallbacks those that were never + * registered from those that were already unregistered. + * @hide + */ + private static final NetworkRequest ALREADY_UNREGISTERED = + new NetworkRequest.Builder().clearCapabilities().build(); + + /** + * A NetID indicating no Network is selected. + * Keep in sync with bionic/libc/dns/include/resolv_netid.h + * @hide + */ + public static final int NETID_UNSET = 0; + + /** + * Flag to indicate that an app is not subject to any restrictions that could result in its + * network access blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_NONE = 0; + + /** + * Flag to indicate that an app is subject to Battery saver restrictions that would + * result in its network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_BATTERY_SAVER = 1 << 0; + + /** + * Flag to indicate that an app is subject to Doze restrictions that would + * result in its network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_DOZE = 1 << 1; + + /** + * Flag to indicate that an app is subject to App Standby restrictions that would + * result in its network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_APP_STANDBY = 1 << 2; + + /** + * Flag to indicate that an app is subject to Restricted mode restrictions that would + * result in its network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_RESTRICTED_MODE = 1 << 3; + + /** + * Flag to indicate that an app is blocked because it is subject to an always-on VPN but the VPN + * is not currently connected. + * + * @see DevicePolicyManager#setAlwaysOnVpnPackage(ComponentName, String, boolean) + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_REASON_LOCKDOWN_VPN = 1 << 4; + + /** + * Flag to indicate that an app is subject to Data saver restrictions that would + * result in its metered network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_METERED_REASON_DATA_SAVER = 1 << 16; + + /** + * Flag to indicate that an app is subject to user restrictions that would + * result in its metered network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_METERED_REASON_USER_RESTRICTED = 1 << 17; + + /** + * Flag to indicate that an app is subject to Device admin restrictions that would + * result in its metered network access being blocked. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_METERED_REASON_ADMIN_DISABLED = 1 << 18; + + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = {"BLOCKED_"}, value = { + BLOCKED_REASON_NONE, + BLOCKED_REASON_BATTERY_SAVER, + BLOCKED_REASON_DOZE, + BLOCKED_REASON_APP_STANDBY, + BLOCKED_REASON_RESTRICTED_MODE, + BLOCKED_REASON_LOCKDOWN_VPN, + BLOCKED_METERED_REASON_DATA_SAVER, + BLOCKED_METERED_REASON_USER_RESTRICTED, + BLOCKED_METERED_REASON_ADMIN_DISABLED, + }) + public @interface BlockedReason {} + + /** + * Set of blocked reasons that are only applicable on metered networks. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int BLOCKED_METERED_REASON_MASK = 0xffff0000; + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + private final IConnectivityManager mService; + + /** + * A kludge to facilitate static access where a Context pointer isn't available, like in the + * case of the static set/getProcessDefaultNetwork methods and from the Network class. + * TODO: Remove this after deprecating the static methods in favor of non-static methods or + * methods that take a Context argument. + */ + private static ConnectivityManager sInstance; + + private final Context mContext; + + private final TetheringManager mTetheringManager; + + /** + * Tests if a given integer represents a valid network type. + * @param networkType the type to be tested + * @return a boolean. {@code true} if the type is valid, else {@code false} + * @deprecated All APIs accepting a network type are deprecated. There should be no need to + * validate a network type. + */ + @Deprecated + public static boolean isNetworkTypeValid(int networkType) { + return MIN_NETWORK_TYPE <= networkType && networkType <= MAX_NETWORK_TYPE; + } + + /** + * Returns a non-localized string representing a given network type. + * ONLY used for debugging output. + * @param type the type needing naming + * @return a String for the given type, or a string version of the type ("87") + * if no name is known. + * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static String getNetworkTypeName(int type) { + switch (type) { + case TYPE_NONE: + return "NONE"; + case TYPE_MOBILE: + return "MOBILE"; + case TYPE_WIFI: + return "WIFI"; + case TYPE_MOBILE_MMS: + return "MOBILE_MMS"; + case TYPE_MOBILE_SUPL: + return "MOBILE_SUPL"; + case TYPE_MOBILE_DUN: + return "MOBILE_DUN"; + case TYPE_MOBILE_HIPRI: + return "MOBILE_HIPRI"; + case TYPE_WIMAX: + return "WIMAX"; + case TYPE_BLUETOOTH: + return "BLUETOOTH"; + case TYPE_DUMMY: + return "DUMMY"; + case TYPE_ETHERNET: + return "ETHERNET"; + case TYPE_MOBILE_FOTA: + return "MOBILE_FOTA"; + case TYPE_MOBILE_IMS: + return "MOBILE_IMS"; + case TYPE_MOBILE_CBS: + return "MOBILE_CBS"; + case TYPE_WIFI_P2P: + return "WIFI_P2P"; + case TYPE_MOBILE_IA: + return "MOBILE_IA"; + case TYPE_MOBILE_EMERGENCY: + return "MOBILE_EMERGENCY"; + case TYPE_PROXY: + return "PROXY"; + case TYPE_VPN: + return "VPN"; + default: + return Integer.toString(type); + } + } + + /** + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public void systemReady() { + try { + mService.systemReady(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Checks if a given type uses the cellular data connection. + * This should be replaced in the future by a network property. + * @param networkType the type to check + * @return a boolean - {@code true} if uses cellular network, else {@code false} + * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead. + * {@hide} + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + public static boolean isNetworkTypeMobile(int networkType) { + switch (networkType) { + case TYPE_MOBILE: + case TYPE_MOBILE_MMS: + case TYPE_MOBILE_SUPL: + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + case TYPE_MOBILE_FOTA: + case TYPE_MOBILE_IMS: + case TYPE_MOBILE_CBS: + case TYPE_MOBILE_IA: + case TYPE_MOBILE_EMERGENCY: + return true; + default: + return false; + } + } + + /** + * Checks if the given network type is backed by a Wi-Fi radio. + * + * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead. + * @hide + */ + @Deprecated + public static boolean isNetworkTypeWifi(int networkType) { + switch (networkType) { + case TYPE_WIFI: + case TYPE_WIFI_P2P: + return true; + default: + return false; + } + } + + /** + * Preference for {@link #setNetworkPreferenceForUser(UserHandle, int, Executor, Runnable)}. + * Specify that the traffic for this user should by follow the default rules. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int PROFILE_NETWORK_PREFERENCE_DEFAULT = 0; + + /** + * Preference for {@link #setNetworkPreferenceForUser(UserHandle, int, Executor, Runnable)}. + * Specify that the traffic for this user should by default go on a network with + * {@link NetworkCapabilities#NET_CAPABILITY_ENTERPRISE}, and on the system default network + * if no such network is available. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public static final int PROFILE_NETWORK_PREFERENCE_ENTERPRISE = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + PROFILE_NETWORK_PREFERENCE_DEFAULT, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE + }) + public @interface ProfileNetworkPreference { + } + + /** + * Specifies the preferred network type. When the device has more + * than one type available the preferred network type will be used. + * + * @param preference the network type to prefer over all others. It is + * unspecified what happens to the old preferred network in the + * overall ordering. + * @deprecated Functionality has been removed as it no longer makes sense, + * with many more than two networks - we'd need an array to express + * preference. Instead we use dynamic network properties of + * the networks to describe their precedence. + */ + @Deprecated + public void setNetworkPreference(int preference) { + } + + /** + * Retrieves the current preferred network type. + * + * @return an integer representing the preferred network type + * + * @deprecated Functionality has been removed as it no longer makes sense, + * with many more than two networks - we'd need an array to express + * preference. Instead we use dynamic network properties of + * the networks to describe their precedence. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public int getNetworkPreference() { + return TYPE_NONE; + } + + /** + * Returns details about the currently active default data network. When + * connected, this network is the default route for outgoing connections. + * You should always check {@link NetworkInfo#isConnected()} before initiating + * network traffic. This may return {@code null} when there is no default + * network. + * Note that if the default network is a VPN, this method will return the + * NetworkInfo for one of its underlying networks instead, or null if the + * VPN agent did not specify any. Apps interested in learning about VPNs + * should use {@link #getNetworkInfo(android.net.Network)} instead. + * + * @return a {@link NetworkInfo} object for the current default network + * or {@code null} if no default network is currently active + * @deprecated See {@link NetworkInfo}. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @Nullable + public NetworkInfo getActiveNetworkInfo() { + try { + return mService.getActiveNetworkInfo(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns a {@link Network} object corresponding to the currently active + * default data network. In the event that the current active default data + * network disconnects, the returned {@code Network} object will no longer + * be usable. This will return {@code null} when there is no default + * network. + * + * @return a {@link Network} object for the current default network or + * {@code null} if no default network is currently active + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @Nullable + public Network getActiveNetwork() { + try { + return mService.getActiveNetwork(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns a {@link Network} object corresponding to the currently active + * default data network for a specific UID. In the event that the default data + * network disconnects, the returned {@code Network} object will no longer + * be usable. This will return {@code null} when there is no default + * network for the UID. + * + * @return a {@link Network} object for the current default network for the + * given UID or {@code null} if no default network is currently active + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) + @Nullable + public Network getActiveNetworkForUid(int uid) { + return getActiveNetworkForUid(uid, false); + } + + /** {@hide} */ + public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) { + try { + return mService.getActiveNetworkForUid(uid, ignoreBlocked); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Adds or removes a requirement for given UID ranges to use the VPN. + * + * If set to {@code true}, informs the system that the UIDs in the specified ranges must not + * have any connectivity except if a VPN is connected and applies to the UIDs, or if the UIDs + * otherwise have permission to bypass the VPN (e.g., because they have the + * {@link android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS} permission, or when + * using a socket protected by a method such as {@link VpnService#protect(DatagramSocket)}. If + * set to {@code false}, a previously-added restriction is removed. + * <p> + * Each of the UID ranges specified by this method is added and removed as is, and no processing + * is performed on the ranges to de-duplicate, merge, split, or intersect them. In order to + * remove a previously-added range, the exact range must be removed as is. + * <p> + * The changes are applied asynchronously and may not have been applied by the time the method + * returns. Apps will be notified about any changes that apply to them via + * {@link NetworkCallback#onBlockedStatusChanged} callbacks called after the changes take + * effect. + * <p> + * This method should be called only by the VPN code. + * + * @param ranges the UID ranges to restrict + * @param requireVpn whether the specified UID ranges must use a VPN + * + * @hide + */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + @SystemApi(client = MODULE_LIBRARIES) + public void setRequireVpnForUids(boolean requireVpn, + @NonNull Collection<Range<Integer>> ranges) { + Objects.requireNonNull(ranges); + // The Range class is not parcelable. Convert to UidRange, which is what is used internally. + // This method is not necessarily expected to be used outside the system server, so + // parceling may not be necessary, but it could be used out-of-process, e.g., by the network + // stack process, or by tests. + UidRange[] rangesArray = new UidRange[ranges.size()]; + int index = 0; + for (Range<Integer> range : ranges) { + rangesArray[index++] = new UidRange(range.getLower(), range.getUpper()); + } + try { + mService.setRequireVpnForUids(requireVpn, rangesArray); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Informs ConnectivityService of whether the legacy lockdown VPN, as implemented by + * LockdownVpnTracker, is in use. This is deprecated for new devices starting from Android 12 + * but is still supported for backwards compatibility. + * <p> + * This type of VPN is assumed always to use the system default network, and must always declare + * exactly one underlying network, which is the network that was the default when the VPN + * connected. + * <p> + * Calling this method with {@code true} enables legacy behaviour, specifically: + * <ul> + * <li>Any VPN that applies to userId 0 behaves specially with respect to deprecated + * {@link #CONNECTIVITY_ACTION} broadcasts. Any such broadcasts will have the state in the + * {@link #EXTRA_NETWORK_INFO} replaced by state of the VPN network. Also, any time the VPN + * connects, a {@link #CONNECTIVITY_ACTION} broadcast will be sent for the network + * underlying the VPN.</li> + * <li>Deprecated APIs that return {@link NetworkInfo} objects will have their state + * similarly replaced by the VPN network state.</li> + * <li>Information on current network interfaces passed to NetworkStatsService will not + * include any VPN interfaces.</li> + * </ul> + * + * @param enabled whether legacy lockdown VPN is enabled or disabled + * + * @hide + */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + @SystemApi(client = MODULE_LIBRARIES) + public void setLegacyLockdownVpnEnabled(boolean enabled) { + try { + mService.setLegacyLockdownVpnEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns details about the currently active default data network + * for a given uid. This is for internal use only to avoid spying + * other apps. + * + * @return a {@link NetworkInfo} object for the current default network + * for the given uid or {@code null} if no default network is + * available for the specified uid. + * + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public NetworkInfo getActiveNetworkInfoForUid(int uid) { + return getActiveNetworkInfoForUid(uid, false); + } + + /** {@hide} */ + public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) { + try { + return mService.getActiveNetworkInfoForUid(uid, ignoreBlocked); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns connection status information about a particular + * network type. + * + * @param networkType integer specifying which networkType in + * which you're interested. + * @return a {@link NetworkInfo} object for the requested + * network type or {@code null} if the type is not + * supported by the device. If {@code networkType} is + * TYPE_VPN and a VPN is active for the calling app, + * then this method will try to return one of the + * underlying networks for the VPN or null if the + * VPN agent didn't specify any. + * + * @deprecated This method does not support multiple connected networks + * of the same type. Use {@link #getAllNetworks} and + * {@link #getNetworkInfo(android.net.Network)} instead. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @Nullable + public NetworkInfo getNetworkInfo(int networkType) { + try { + return mService.getNetworkInfo(networkType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns connection status information about a particular + * Network. + * + * @param network {@link Network} specifying which network + * in which you're interested. + * @return a {@link NetworkInfo} object for the requested + * network or {@code null} if the {@code Network} + * is not valid. + * @deprecated See {@link NetworkInfo}. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @Nullable + public NetworkInfo getNetworkInfo(@Nullable Network network) { + return getNetworkInfoForUid(network, Process.myUid(), false); + } + + /** {@hide} */ + public NetworkInfo getNetworkInfoForUid(Network network, int uid, boolean ignoreBlocked) { + try { + return mService.getNetworkInfoForUid(network, uid, ignoreBlocked); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns connection status information about all network + * types supported by the device. + * + * @return an array of {@link NetworkInfo} objects. Check each + * {@link NetworkInfo#getType} for which type each applies. + * + * @deprecated This method does not support multiple connected networks + * of the same type. Use {@link #getAllNetworks} and + * {@link #getNetworkInfo(android.net.Network)} instead. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @NonNull + public NetworkInfo[] getAllNetworkInfo() { + try { + return mService.getAllNetworkInfo(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return a list of {@link NetworkStateSnapshot}s, one for each network that is currently + * connected. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + @NonNull + public List<NetworkStateSnapshot> getAllNetworkStateSnapshots() { + try { + return mService.getAllNetworkStateSnapshots(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the {@link Network} object currently serving a given type, or + * null if the given type is not connected. + * + * @hide + * @deprecated This method does not support multiple connected networks + * of the same type. Use {@link #getAllNetworks} and + * {@link #getNetworkInfo(android.net.Network)} instead. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + public Network getNetworkForType(int networkType) { + try { + return mService.getNetworkForType(networkType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns an array of all {@link Network} currently tracked by the + * framework. + * + * @deprecated This method does not provide any notification of network state changes, forcing + * apps to call it repeatedly. This is inefficient and prone to race conditions. + * Apps should use methods such as + * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} instead. + * Apps that desire to obtain information about networks that do not apply to them + * can use {@link NetworkRequest.Builder#setIncludeOtherUidNetworks}. + * + * @return an array of {@link Network} objects. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @NonNull + @Deprecated + public Network[] getAllNetworks() { + try { + return mService.getAllNetworks(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns an array of {@link NetworkCapabilities} objects, representing + * the Networks that applications run by the given user will use by default. + * @hide + */ + @UnsupportedAppUsage + public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) { + try { + return mService.getDefaultNetworkCapabilitiesForUser( + userId, mContext.getOpPackageName(), getAttributionTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the IP information for the current default network. + * + * @return a {@link LinkProperties} object describing the IP info + * for the current default network, or {@code null} if there + * is no current default network. + * + * {@hide} + * @deprecated please use {@link #getLinkProperties(Network)} on the return + * value of {@link #getActiveNetwork()} instead. In particular, + * this method will return non-null LinkProperties even if the + * app is blocked by policy from using this network. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 109783091) + public LinkProperties getActiveLinkProperties() { + try { + return mService.getActiveLinkProperties(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the IP information for a given network type. + * + * @param networkType the network type of interest. + * @return a {@link LinkProperties} object describing the IP info + * for the given networkType, or {@code null} if there is + * no current default network. + * + * {@hide} + * @deprecated This method does not support multiple connected networks + * of the same type. Use {@link #getAllNetworks}, + * {@link #getNetworkInfo(android.net.Network)}, and + * {@link #getLinkProperties(android.net.Network)} instead. + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + public LinkProperties getLinkProperties(int networkType) { + try { + return mService.getLinkPropertiesForType(networkType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the {@link LinkProperties} for the given {@link Network}. This + * will return {@code null} if the network is unknown. + * + * @param network The {@link Network} object identifying the network in question. + * @return The {@link LinkProperties} for the network, or {@code null}. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @Nullable + public LinkProperties getLinkProperties(@Nullable Network network) { + try { + return mService.getLinkProperties(network); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the {@link NetworkCapabilities} for the given {@link Network}. This + * will return {@code null} if the network is unknown or if the |network| argument is null. + * + * This will remove any location sensitive data in {@link TransportInfo} embedded in + * {@link NetworkCapabilities#getTransportInfo()}. Some transport info instances like + * {@link android.net.wifi.WifiInfo} contain location sensitive information. Retrieving + * this location sensitive information (subject to app's location permissions) will be + * noted by system. To include any location sensitive data in {@link TransportInfo}, + * use a {@link NetworkCallback} with + * {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} flag. + * + * @param network The {@link Network} object identifying the network in question. + * @return The {@link NetworkCapabilities} for the network, or {@code null}. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @Nullable + public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) { + try { + return mService.getNetworkCapabilities( + network, mContext.getOpPackageName(), getAttributionTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Gets a URL that can be used for resolving whether a captive portal is present. + * 1. This URL should respond with a 204 response to a GET request to indicate no captive + * portal is present. + * 2. This URL must be HTTP as redirect responses are used to find captive portal + * sign-in pages. Captive portals cannot respond to HTTPS requests with redirects. + * + * The system network validation may be using different strategies to detect captive portals, + * so this method does not necessarily return a URL used by the system. It only returns a URL + * that may be relevant for other components trying to detect captive portals. + * + * @hide + * @deprecated This API returns URL which is not guaranteed to be one of the URLs used by the + * system. + */ + @Deprecated + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) + public String getCaptivePortalServerUrl() { + try { + return mService.getCaptivePortalServerUrl(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Tells the underlying networking system that the caller wants to + * begin using the named feature. The interpretation of {@code feature} + * is completely up to each networking implementation. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * @param networkType specifies which network the request pertains to + * @param feature the name of the feature to be used + * @return an integer value representing the outcome of the request. + * The interpretation of this value is specific to each networking + * implementation+feature combination, except that the value {@code -1} + * always indicates failure. + * + * @deprecated Deprecated in favor of the cleaner + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} API. + * In {@link VERSION_CODES#M}, and above, this method is unsupported and will + * throw {@code UnsupportedOperationException} if called. + * @removed + */ + @Deprecated + public int startUsingNetworkFeature(int networkType, String feature) { + checkLegacyRoutingApiAccess(); + NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature); + if (netCap == null) { + Log.d(TAG, "Can't satisfy startUsingNetworkFeature for " + networkType + ", " + + feature); + return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_FAILED; + } + + NetworkRequest request = null; + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.get(netCap); + if (l != null) { + Log.d(TAG, "renewing startUsingNetworkFeature request " + l.networkRequest); + renewRequestLocked(l); + if (l.currentNetwork != null) { + return DEPRECATED_PHONE_CONSTANT_APN_ALREADY_ACTIVE; + } else { + return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_STARTED; + } + } + + request = requestNetworkForFeatureLocked(netCap); + } + if (request != null) { + Log.d(TAG, "starting startUsingNetworkFeature for request " + request); + return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_STARTED; + } else { + Log.d(TAG, " request Failed"); + return DEPRECATED_PHONE_CONSTANT_APN_REQUEST_FAILED; + } + } + + /** + * Tells the underlying networking system that the caller is finished + * using the named feature. The interpretation of {@code feature} + * is completely up to each networking implementation. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * @param networkType specifies which network the request pertains to + * @param feature the name of the feature that is no longer needed + * @return an integer value representing the outcome of the request. + * The interpretation of this value is specific to each networking + * implementation+feature combination, except that the value {@code -1} + * always indicates failure. + * + * @deprecated Deprecated in favor of the cleaner + * {@link #unregisterNetworkCallback(NetworkCallback)} API. + * In {@link VERSION_CODES#M}, and above, this method is unsupported and will + * throw {@code UnsupportedOperationException} if called. + * @removed + */ + @Deprecated + public int stopUsingNetworkFeature(int networkType, String feature) { + checkLegacyRoutingApiAccess(); + NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature); + if (netCap == null) { + Log.d(TAG, "Can't satisfy stopUsingNetworkFeature for " + networkType + ", " + + feature); + return -1; + } + + if (removeRequestForFeature(netCap)) { + Log.d(TAG, "stopUsingNetworkFeature for " + networkType + ", " + feature); + } + return 1; + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private NetworkCapabilities networkCapabilitiesForFeature(int networkType, String feature) { + if (networkType == TYPE_MOBILE) { + switch (feature) { + case "enableCBS": + return networkCapabilitiesForType(TYPE_MOBILE_CBS); + case "enableDUN": + case "enableDUNAlways": + return networkCapabilitiesForType(TYPE_MOBILE_DUN); + case "enableFOTA": + return networkCapabilitiesForType(TYPE_MOBILE_FOTA); + case "enableHIPRI": + return networkCapabilitiesForType(TYPE_MOBILE_HIPRI); + case "enableIMS": + return networkCapabilitiesForType(TYPE_MOBILE_IMS); + case "enableMMS": + return networkCapabilitiesForType(TYPE_MOBILE_MMS); + case "enableSUPL": + return networkCapabilitiesForType(TYPE_MOBILE_SUPL); + default: + return null; + } + } else if (networkType == TYPE_WIFI && "p2p".equals(feature)) { + return networkCapabilitiesForType(TYPE_WIFI_P2P); + } + return null; + } + + private int legacyTypeForNetworkCapabilities(NetworkCapabilities netCap) { + if (netCap == null) return TYPE_NONE; + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) { + return TYPE_MOBILE_CBS; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) { + return TYPE_MOBILE_IMS; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) { + return TYPE_MOBILE_FOTA; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) { + return TYPE_MOBILE_DUN; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) { + return TYPE_MOBILE_SUPL; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) { + return TYPE_MOBILE_MMS; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + return TYPE_MOBILE_HIPRI; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P)) { + return TYPE_WIFI_P2P; + } + return TYPE_NONE; + } + + private static class LegacyRequest { + NetworkCapabilities networkCapabilities; + NetworkRequest networkRequest; + int expireSequenceNumber; + Network currentNetwork; + int delay = -1; + + private void clearDnsBinding() { + if (currentNetwork != null) { + currentNetwork = null; + setProcessDefaultNetworkForHostResolution(null); + } + } + + NetworkCallback networkCallback = new NetworkCallback() { + @Override + public void onAvailable(Network network) { + currentNetwork = network; + Log.d(TAG, "startUsingNetworkFeature got Network:" + network); + setProcessDefaultNetworkForHostResolution(network); + } + @Override + public void onLost(Network network) { + if (network.equals(currentNetwork)) clearDnsBinding(); + Log.d(TAG, "startUsingNetworkFeature lost Network:" + network); + } + }; + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private static final HashMap<NetworkCapabilities, LegacyRequest> sLegacyRequests = + new HashMap<>(); + + private NetworkRequest findRequestForFeature(NetworkCapabilities netCap) { + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.get(netCap); + if (l != null) return l.networkRequest; + } + return null; + } + + private void renewRequestLocked(LegacyRequest l) { + l.expireSequenceNumber++; + Log.d(TAG, "renewing request to seqNum " + l.expireSequenceNumber); + sendExpireMsgForFeature(l.networkCapabilities, l.expireSequenceNumber, l.delay); + } + + private void expireRequest(NetworkCapabilities netCap, int sequenceNum) { + int ourSeqNum = -1; + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.get(netCap); + if (l == null) return; + ourSeqNum = l.expireSequenceNumber; + if (l.expireSequenceNumber == sequenceNum) removeRequestForFeature(netCap); + } + Log.d(TAG, "expireRequest with " + ourSeqNum + ", " + sequenceNum); + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private NetworkRequest requestNetworkForFeatureLocked(NetworkCapabilities netCap) { + int delay = -1; + int type = legacyTypeForNetworkCapabilities(netCap); + try { + delay = mService.getRestoreDefaultNetworkDelay(type); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + LegacyRequest l = new LegacyRequest(); + l.networkCapabilities = netCap; + l.delay = delay; + l.expireSequenceNumber = 0; + l.networkRequest = sendRequestForNetwork( + netCap, l.networkCallback, 0, REQUEST, type, getDefaultHandler()); + if (l.networkRequest == null) return null; + sLegacyRequests.put(netCap, l); + sendExpireMsgForFeature(netCap, l.expireSequenceNumber, delay); + return l.networkRequest; + } + + private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) { + if (delay >= 0) { + Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay); + CallbackHandler handler = getDefaultHandler(); + Message msg = handler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap); + handler.sendMessageDelayed(msg, delay); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private boolean removeRequestForFeature(NetworkCapabilities netCap) { + final LegacyRequest l; + synchronized (sLegacyRequests) { + l = sLegacyRequests.remove(netCap); + } + if (l == null) return false; + unregisterNetworkCallback(l.networkCallback); + l.clearDnsBinding(); + return true; + } + + private static final SparseIntArray sLegacyTypeToTransport = new SparseIntArray(); + static { + sLegacyTypeToTransport.put(TYPE_MOBILE, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_CBS, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_DUN, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_FOTA, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_HIPRI, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_IMS, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_MMS, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_SUPL, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_WIFI, NetworkCapabilities.TRANSPORT_WIFI); + sLegacyTypeToTransport.put(TYPE_WIFI_P2P, NetworkCapabilities.TRANSPORT_WIFI); + sLegacyTypeToTransport.put(TYPE_BLUETOOTH, NetworkCapabilities.TRANSPORT_BLUETOOTH); + sLegacyTypeToTransport.put(TYPE_ETHERNET, NetworkCapabilities.TRANSPORT_ETHERNET); + } + + private static final SparseIntArray sLegacyTypeToCapability = new SparseIntArray(); + static { + sLegacyTypeToCapability.put(TYPE_MOBILE_CBS, NetworkCapabilities.NET_CAPABILITY_CBS); + sLegacyTypeToCapability.put(TYPE_MOBILE_DUN, NetworkCapabilities.NET_CAPABILITY_DUN); + sLegacyTypeToCapability.put(TYPE_MOBILE_FOTA, NetworkCapabilities.NET_CAPABILITY_FOTA); + sLegacyTypeToCapability.put(TYPE_MOBILE_IMS, NetworkCapabilities.NET_CAPABILITY_IMS); + sLegacyTypeToCapability.put(TYPE_MOBILE_MMS, NetworkCapabilities.NET_CAPABILITY_MMS); + sLegacyTypeToCapability.put(TYPE_MOBILE_SUPL, NetworkCapabilities.NET_CAPABILITY_SUPL); + sLegacyTypeToCapability.put(TYPE_WIFI_P2P, NetworkCapabilities.NET_CAPABILITY_WIFI_P2P); + } + + /** + * Given a legacy type (TYPE_WIFI, ...) returns a NetworkCapabilities + * instance suitable for registering a request or callback. Throws an + * IllegalArgumentException if no mapping from the legacy type to + * NetworkCapabilities is known. + * + * @deprecated Types are deprecated. Use {@link NetworkCallback} or {@link NetworkRequest} + * to find the network instead. + * @hide + */ + public static NetworkCapabilities networkCapabilitiesForType(int type) { + final NetworkCapabilities nc = new NetworkCapabilities(); + + // Map from type to transports. + final int NOT_FOUND = -1; + final int transport = sLegacyTypeToTransport.get(type, NOT_FOUND); + if (transport == NOT_FOUND) { + throw new IllegalArgumentException("unknown legacy type: " + type); + } + nc.addTransportType(transport); + + // Map from type to capabilities. + nc.addCapability(sLegacyTypeToCapability.get( + type, NetworkCapabilities.NET_CAPABILITY_INTERNET)); + nc.maybeMarkCapabilitiesRestricted(); + return nc; + } + + /** @hide */ + public static class PacketKeepaliveCallback { + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public PacketKeepaliveCallback() { + } + /** The requested keepalive was successfully started. */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void onStarted() {} + /** The keepalive was successfully stopped. */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void onStopped() {} + /** An error occurred. */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void onError(int error) {} + } + + /** + * Allows applications to request that the system periodically send specific packets on their + * behalf, using hardware offload to save battery power. + * + * To request that the system send keepalives, call one of the methods that return a + * {@link ConnectivityManager.PacketKeepalive} object, such as {@link #startNattKeepalive}, + * passing in a non-null callback. If the callback is successfully started, the callback's + * {@code onStarted} method will be called. If an error occurs, {@code onError} will be called, + * specifying one of the {@code ERROR_*} constants in this class. + * + * To stop an existing keepalive, call {@link PacketKeepalive#stop}. The system will call + * {@link PacketKeepaliveCallback#onStopped} if the operation was successful or + * {@link PacketKeepaliveCallback#onError} if an error occurred. + * + * @deprecated Use {@link SocketKeepalive} instead. + * + * @hide + */ + public class PacketKeepalive { + + private static final String TAG = "PacketKeepalive"; + + /** @hide */ + public static final int SUCCESS = 0; + + /** @hide */ + public static final int NO_KEEPALIVE = -1; + + /** @hide */ + public static final int BINDER_DIED = -10; + + /** The specified {@code Network} is not connected. */ + public static final int ERROR_INVALID_NETWORK = -20; + /** The specified IP addresses are invalid. For example, the specified source IP address is + * not configured on the specified {@code Network}. */ + public static final int ERROR_INVALID_IP_ADDRESS = -21; + /** The requested port is invalid. */ + public static final int ERROR_INVALID_PORT = -22; + /** The packet length is invalid (e.g., too long). */ + public static final int ERROR_INVALID_LENGTH = -23; + /** The packet transmission interval is invalid (e.g., too short). */ + public static final int ERROR_INVALID_INTERVAL = -24; + + /** The hardware does not support this request. */ + public static final int ERROR_HARDWARE_UNSUPPORTED = -30; + /** The hardware returned an error. */ + public static final int ERROR_HARDWARE_ERROR = -31; + + /** The NAT-T destination port for IPsec */ + public static final int NATT_PORT = 4500; + + /** The minimum interval in seconds between keepalive packet transmissions */ + public static final int MIN_INTERVAL = 10; + + private final Network mNetwork; + private final ISocketKeepaliveCallback mCallback; + private final ExecutorService mExecutor; + + private volatile Integer mSlot; + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void stop() { + try { + mExecutor.execute(() -> { + try { + if (mSlot != null) { + mService.stopKeepalive(mNetwork, mSlot); + } + } catch (RemoteException e) { + Log.e(TAG, "Error stopping packet keepalive: ", e); + throw e.rethrowFromSystemServer(); + } + }); + } catch (RejectedExecutionException e) { + // The internal executor has already stopped due to previous event. + } + } + + private PacketKeepalive(Network network, PacketKeepaliveCallback callback) { + Objects.requireNonNull(network, "network cannot be null"); + Objects.requireNonNull(callback, "callback cannot be null"); + mNetwork = network; + mExecutor = Executors.newSingleThreadExecutor(); + mCallback = new ISocketKeepaliveCallback.Stub() { + @Override + public void onStarted(int slot) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mSlot = slot; + callback.onStarted(); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onStopped() { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mSlot = null; + callback.onStopped(); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + mExecutor.shutdown(); + } + + @Override + public void onError(int error) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mSlot = null; + callback.onError(error); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + mExecutor.shutdown(); + } + + @Override + public void onDataReceived() { + // PacketKeepalive is only used for Nat-T keepalive and as such does not invoke + // this callback when data is received. + } + }; + } + } + + /** + * Starts an IPsec NAT-T keepalive packet with the specified parameters. + * + * @deprecated Use {@link #createSocketKeepalive} instead. + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public PacketKeepalive startNattKeepalive( + Network network, int intervalSeconds, PacketKeepaliveCallback callback, + InetAddress srcAddr, int srcPort, InetAddress dstAddr) { + final PacketKeepalive k = new PacketKeepalive(network, callback); + try { + mService.startNattKeepalive(network, intervalSeconds, k.mCallback, + srcAddr.getHostAddress(), srcPort, dstAddr.getHostAddress()); + } catch (RemoteException e) { + Log.e(TAG, "Error starting packet keepalive: ", e); + throw e.rethrowFromSystemServer(); + } + return k; + } + + // Construct an invalid fd. + private ParcelFileDescriptor createInvalidFd() { + final int invalidFd = -1; + return ParcelFileDescriptor.adoptFd(invalidFd); + } + + /** + * Request that keepalives be started on a IPsec NAT-T socket. + * + * @param network The {@link Network} the socket is on. + * @param socket The socket that needs to be kept alive. + * @param source The source address of the {@link UdpEncapsulationSocket}. + * @param destination The destination address of the {@link UdpEncapsulationSocket}. + * @param executor The executor on which callback will be invoked. The provided {@link Executor} + * must run callback sequentially, otherwise the order of callbacks cannot be + * guaranteed. + * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive + * changes. Must be extended by applications that use this API. + * + * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the + * given socket. + **/ + public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network, + @NonNull UdpEncapsulationSocket socket, + @NonNull InetAddress source, + @NonNull InetAddress destination, + @NonNull @CallbackExecutor Executor executor, + @NonNull Callback callback) { + ParcelFileDescriptor dup; + try { + // Dup is needed here as the pfd inside the socket is owned by the IpSecService, + // which cannot be obtained by the app process. + dup = ParcelFileDescriptor.dup(socket.getFileDescriptor()); + } catch (IOException ignored) { + // Construct an invalid fd, so that if the user later calls start(), it will fail with + // ERROR_INVALID_SOCKET. + dup = createInvalidFd(); + } + return new NattSocketKeepalive(mService, network, dup, socket.getResourceId(), source, + destination, executor, callback); + } + + /** + * Request that keepalives be started on a IPsec NAT-T socket file descriptor. Directly called + * by system apps which don't use IpSecService to create {@link UdpEncapsulationSocket}. + * + * @param network The {@link Network} the socket is on. + * @param pfd The {@link ParcelFileDescriptor} that needs to be kept alive. The provided + * {@link ParcelFileDescriptor} must be bound to a port and the keepalives will be sent + * from that port. + * @param source The source address of the {@link UdpEncapsulationSocket}. + * @param destination The destination address of the {@link UdpEncapsulationSocket}. The + * keepalive packets will always be sent to port 4500 of the given {@code destination}. + * @param executor The executor on which callback will be invoked. The provided {@link Executor} + * must run callback sequentially, otherwise the order of callbacks cannot be + * guaranteed. + * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive + * changes. Must be extended by applications that use this API. + * + * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the + * given socket. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) + public @NonNull SocketKeepalive createNattKeepalive(@NonNull Network network, + @NonNull ParcelFileDescriptor pfd, + @NonNull InetAddress source, + @NonNull InetAddress destination, + @NonNull @CallbackExecutor Executor executor, + @NonNull Callback callback) { + ParcelFileDescriptor dup; + try { + // TODO: Consider remove unnecessary dup. + dup = pfd.dup(); + } catch (IOException ignored) { + // Construct an invalid fd, so that if the user later calls start(), it will fail with + // ERROR_INVALID_SOCKET. + dup = createInvalidFd(); + } + return new NattSocketKeepalive(mService, network, dup, + -1 /* Unused */, source, destination, executor, callback); + } + + /** + * Request that keepalives be started on a TCP socket. + * The socket must be established. + * + * @param network The {@link Network} the socket is on. + * @param socket The socket that needs to be kept alive. + * @param executor The executor on which callback will be invoked. This implementation assumes + * the provided {@link Executor} runs the callbacks in sequence with no + * concurrency. Failing this, no guarantee of correctness can be made. It is + * the responsibility of the caller to ensure the executor provides this + * guarantee. A simple way of creating such an executor is with the standard + * tool {@code Executors.newSingleThreadExecutor}. + * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive + * changes. Must be extended by applications that use this API. + * + * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the + * given socket. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD) + public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network, + @NonNull Socket socket, + @NonNull Executor executor, + @NonNull Callback callback) { + ParcelFileDescriptor dup; + try { + dup = ParcelFileDescriptor.fromSocket(socket); + } catch (UncheckedIOException ignored) { + // Construct an invalid fd, so that if the user later calls start(), it will fail with + // ERROR_INVALID_SOCKET. + dup = createInvalidFd(); + } + return new TcpSocketKeepalive(mService, network, dup, executor, callback); + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. An attempt to add a route that + * already exists is ignored, but treated as successful. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * @param networkType the type of the network over which traffic to the specified + * host is to be routed + * @param hostAddress the IP address of the host to which the route is desired + * @return {@code true} on success, {@code false} on failure + * + * @deprecated Deprecated in favor of the + * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, + * {@link #bindProcessToNetwork} and {@link Network#getSocketFactory} API. + * In {@link VERSION_CODES#M}, and above, this method is unsupported and will + * throw {@code UnsupportedOperationException} if called. + * @removed + */ + @Deprecated + public boolean requestRouteToHost(int networkType, int hostAddress) { + return requestRouteToHostAddress(networkType, NetworkUtils.intToInetAddress(hostAddress)); + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. An attempt to add a route that + * already exists is ignored, but treated as successful. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * @param networkType the type of the network over which traffic to the specified + * host is to be routed + * @param hostAddress the IP address of the host to which the route is desired + * @return {@code true} on success, {@code false} on failure + * @hide + * @deprecated Deprecated in favor of the {@link #requestNetwork} and + * {@link #bindProcessToNetwork} API. + */ + @Deprecated + @UnsupportedAppUsage + @SystemApi(client = MODULE_LIBRARIES) + public boolean requestRouteToHostAddress(int networkType, InetAddress hostAddress) { + checkLegacyRoutingApiAccess(); + try { + return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress(), + mContext.getOpPackageName(), getAttributionTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @return the context's attribution tag + */ + // TODO: Remove method and replace with direct call once R code is pushed to AOSP + private @Nullable String getAttributionTag() { + return mContext.getAttributionTag(); + } + + /** + * Returns the value of the setting for background data usage. If false, + * applications should not use the network if the application is not in the + * foreground. Developers should respect this setting, and check the value + * of this before performing any background data operations. + * <p> + * All applications that have background services that use the network + * should listen to {@link #ACTION_BACKGROUND_DATA_SETTING_CHANGED}. + * <p> + * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability of + * background data depends on several combined factors, and this method will + * always return {@code true}. Instead, when background data is unavailable, + * {@link #getActiveNetworkInfo()} will now appear disconnected. + * + * @return Whether background data usage is allowed. + */ + @Deprecated + public boolean getBackgroundDataSetting() { + // assume that background data is allowed; final authority is + // NetworkInfo which may be blocked. + return true; + } + + /** + * Sets the value of the setting for background data usage. + * + * @param allowBackgroundData Whether an application should use data while + * it is in the background. + * + * @attr ref android.Manifest.permission#CHANGE_BACKGROUND_DATA_SETTING + * @see #getBackgroundDataSetting() + * @hide + */ + @Deprecated + @UnsupportedAppUsage + public void setBackgroundDataSetting(boolean allowBackgroundData) { + // ignored + } + + /** + * @hide + * @deprecated Talk to TelephonyManager directly + */ + @Deprecated + @UnsupportedAppUsage + public boolean getMobileDataEnabled() { + TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); + if (tm != null) { + int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + Log.d("ConnectivityManager", "getMobileDataEnabled()+ subId=" + subId); + boolean retVal = tm.createForSubscriptionId(subId).isDataEnabled(); + Log.d("ConnectivityManager", "getMobileDataEnabled()- subId=" + subId + + " retVal=" + retVal); + return retVal; + } + Log.d("ConnectivityManager", "getMobileDataEnabled()- remote exception retVal=false"); + return false; + } + + /** + * Callback for use with {@link ConnectivityManager#addDefaultNetworkActiveListener} + * to find out when the system default network has gone in to a high power state. + */ + public interface OnNetworkActiveListener { + /** + * Called on the main thread of the process to report that the current data network + * has become active, and it is now a good time to perform any pending network + * operations. Note that this listener only tells you when the network becomes + * active; if at any other time you want to know whether it is active (and thus okay + * to initiate network traffic), you can retrieve its instantaneous state with + * {@link ConnectivityManager#isDefaultNetworkActive}. + */ + void onNetworkActive(); + } + + private final ArrayMap<OnNetworkActiveListener, INetworkActivityListener> + mNetworkActivityListeners = new ArrayMap<>(); + + /** + * Start listening to reports when the system's default data network is active, meaning it is + * a good time to perform network traffic. Use {@link #isDefaultNetworkActive()} + * to determine the current state of the system's default network after registering the + * listener. + * <p> + * If the process default network has been set with + * {@link ConnectivityManager#bindProcessToNetwork} this function will not + * reflect the process's default, but the system default. + * + * @param l The listener to be told when the network is active. + */ + public void addDefaultNetworkActiveListener(final OnNetworkActiveListener l) { + INetworkActivityListener rl = new INetworkActivityListener.Stub() { + @Override + public void onNetworkActive() throws RemoteException { + l.onNetworkActive(); + } + }; + + try { + mService.registerNetworkActivityListener(rl); + mNetworkActivityListeners.put(l, rl); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove network active listener previously registered with + * {@link #addDefaultNetworkActiveListener}. + * + * @param l Previously registered listener. + */ + public void removeDefaultNetworkActiveListener(@NonNull OnNetworkActiveListener l) { + INetworkActivityListener rl = mNetworkActivityListeners.get(l); + if (rl == null) { + throw new IllegalArgumentException("Listener was not registered."); + } + try { + mService.registerNetworkActivityListener(rl); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return whether the data network is currently active. An active network means that + * it is currently in a high power state for performing data transmission. On some + * types of networks, it may be expensive to move and stay in such a state, so it is + * more power efficient to batch network traffic together when the radio is already in + * this state. This method tells you whether right now is currently a good time to + * initiate network traffic, as the network is already active. + */ + public boolean isDefaultNetworkActive() { + try { + return mService.isDefaultNetworkActive(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * {@hide} + */ + public ConnectivityManager(Context context, IConnectivityManager service) { + mContext = Objects.requireNonNull(context, "missing context"); + mService = Objects.requireNonNull(service, "missing IConnectivityManager"); + mTetheringManager = (TetheringManager) mContext.getSystemService(Context.TETHERING_SERVICE); + sInstance = this; + } + + /** {@hide} */ + @UnsupportedAppUsage + public static ConnectivityManager from(Context context) { + return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + /** @hide */ + public NetworkRequest getDefaultRequest() { + try { + // This is not racy as the default request is final in ConnectivityService. + return mService.getDefaultRequest(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * 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. + */ + // TODO: Remove method and replace with direct call once R code is pushed to AOSP + private static boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid, + @NonNull String callingPackage, @Nullable String callingAttributionTag, + boolean throwException) { + return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage, + callingAttributionTag, throwException); + } + + /** + * @deprecated - use getSystemService. This is a kludge to support static access in certain + * situations where a Context pointer is unavailable. + * @hide + */ + @Deprecated + static ConnectivityManager getInstanceOrNull() { + return sInstance; + } + + /** + * @deprecated - use getSystemService. This is a kludge to support static access in certain + * situations where a Context pointer is unavailable. + * @hide + */ + @Deprecated + @UnsupportedAppUsage + private static ConnectivityManager getInstance() { + if (getInstanceOrNull() == null) { + throw new IllegalStateException("No ConnectivityManager yet constructed"); + } + return getInstanceOrNull(); + } + + /** + * 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. + * + * @deprecated Use {@link TetheringEventCallback#onTetherableInterfacesChanged(List)} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + @Deprecated + public String[] getTetherableIfaces() { + return mTetheringManager.getTetherableIfaces(); + } + + /** + * Get the set of tethered interfaces. + * + * @return an array of 0 or more String of currently tethered interface names. + * + * @deprecated Use {@link TetheringEventCallback#onTetherableInterfacesChanged(List)} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + @Deprecated + public String[] getTetheredIfaces() { + return mTetheringManager.getTetheredIfaces(); + } + + /** + * 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 ConnectivityManager#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. + * + * @deprecated Use {@link TetheringEventCallback#onError(String, int)} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + @Deprecated + public String[] getTetheringErroredIfaces() { + return mTetheringManager.getTetheringErroredIfaces(); + } + + /** + * Get the set of tethered dhcp ranges. + * + * @deprecated This method is not supported. + * TODO: remove this function when all of clients are removed. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS) + @Deprecated + public String[] getTetheredDhcpRanges() { + throw new UnsupportedOperationException("getTetheredDhcpRanges is not supported"); + } + + /** + * Attempt to tether the named interface. This will setup a dhcp server + * on the interface, forward and NAT IP 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. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * <p>WARNING: New clients should not use this function. The only usages should be in PanService + * and WifiStateMachine which need direct access. All other clients should use + * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning + * logic.</p> + * + * @param iface the interface name to tether. + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * @deprecated Use {@link TetheringManager#startTethering} instead + * + * {@hide} + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Deprecated + public int tether(String iface) { + return mTetheringManager.tether(iface); + } + + /** + * Stop tethering the named interface. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * <p>WARNING: New clients should not use this function. The only usages should be in PanService + * and WifiStateMachine which need direct access. All other clients should use + * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning + * logic.</p> + * + * @param iface the interface name to untether. + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * + * {@hide} + */ + @UnsupportedAppUsage + @Deprecated + public int untether(String iface) { + return mTetheringManager.untether(iface); + } + + /** + * 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. + * + * <p>If this app does not have permission to use this API, it will always + * return false rather than throw an exception.</p> + * + * <p>If the device has a hotspot provisioning app, the caller is required to hold the + * {@link android.Manifest.permission.TETHER_PRIVILEGED} permission.</p> + * + * <p>Otherwise, this method requires the caller to hold the ability to modify system + * settings as determined by {@link android.provider.Settings.System#canWrite}.</p> + * + * @return a boolean - {@code true} indicating Tethering is supported. + * + * @deprecated Use {@link TetheringEventCallback#onTetheringSupported(boolean)} instead. + * {@hide} + */ + @SystemApi + @RequiresPermission(anyOf = {android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS}) + public boolean isTetheringSupported() { + return mTetheringManager.isTetheringSupported(); + } + + /** + * Callback for use with {@link #startTethering} to find out whether tethering succeeded. + * + * @deprecated Use {@link TetheringManager.StartTetheringCallback} instead. + * @hide + */ + @SystemApi + @Deprecated + public static abstract class OnStartTetheringCallback { + /** + * Called when tethering has been successfully started. + */ + public void onTetheringStarted() {} + + /** + * Called when starting tethering failed. + */ + public void onTetheringFailed() {} + } + + /** + * Convenient overload for + * {@link #startTethering(int, boolean, OnStartTetheringCallback, Handler)} which passes a null + * handler to run on the current thread's {@link Looper}. + * + * @deprecated Use {@link TetheringManager#startTethering} instead. + * @hide + */ + @SystemApi + @Deprecated + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + public void startTethering(int type, boolean showProvisioningUi, + final OnStartTetheringCallback callback) { + startTethering(type, showProvisioningUi, callback, null); + } + + /** + * Runs tether provisioning for the given type if needed and then starts tethering if + * the check succeeds. If no carrier provisioning is required for tethering, tethering is + * enabled immediately. If provisioning fails, tethering will not be enabled. It also + * schedules tether provisioning re-checks if appropriate. + * + * @param type The type of tethering to start. Must be one of + * {@link ConnectivityManager.TETHERING_WIFI}, + * {@link ConnectivityManager.TETHERING_USB}, or + * {@link ConnectivityManager.TETHERING_BLUETOOTH}. + * @param showProvisioningUi a boolean indicating to show the provisioning app UI if there + * is one. This should be true the first time this function is called and also any time + * the user can see this UI. It gives users information from their carrier about the + * check failing and how they can sign up for tethering if possible. + * @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller + * of the result of trying to tether. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * + * @deprecated Use {@link TetheringManager#startTethering} instead. + * @hide + */ + @SystemApi + @Deprecated + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + public void startTethering(int type, boolean showProvisioningUi, + final OnStartTetheringCallback callback, Handler handler) { + Objects.requireNonNull(callback, "OnStartTetheringCallback cannot be null."); + + final Executor executor = new Executor() { + @Override + public void execute(Runnable command) { + if (handler == null) { + command.run(); + } else { + handler.post(command); + } + } + }; + + final StartTetheringCallback tetheringCallback = new StartTetheringCallback() { + @Override + public void onTetheringStarted() { + callback.onTetheringStarted(); + } + + @Override + public void onTetheringFailed(final int error) { + callback.onTetheringFailed(); + } + }; + + final TetheringRequest request = new TetheringRequest.Builder(type) + .setShouldShowEntitlementUi(showProvisioningUi).build(); + + mTetheringManager.startTethering(request, executor, tetheringCallback); + } + + /** + * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if + * applicable. + * + * @param type The type of tethering to stop. Must be one of + * {@link ConnectivityManager.TETHERING_WIFI}, + * {@link ConnectivityManager.TETHERING_USB}, or + * {@link ConnectivityManager.TETHERING_BLUETOOTH}. + * + * @deprecated Use {@link TetheringManager#stopTethering} instead. + * @hide + */ + @SystemApi + @Deprecated + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + public void stopTethering(int type) { + mTetheringManager.stopTethering(type); + } + + /** + * Callback for use with {@link registerTetheringEventCallback} to find out tethering + * upstream status. + * + * @deprecated Use {@link TetheringManager#OnTetheringEventCallback} instead. + * @hide + */ + @SystemApi + @Deprecated + public abstract static class OnTetheringEventCallback { + + /** + * Called when tethering upstream changed. This can be called multiple times and can be + * called any time. + * + * @param network the {@link Network} of tethering upstream. Null means tethering doesn't + * have any upstream. + */ + public void onUpstreamChanged(@Nullable Network network) {} + } + + @GuardedBy("mTetheringEventCallbacks") + private final ArrayMap<OnTetheringEventCallback, TetheringEventCallback> + mTetheringEventCallbacks = new ArrayMap<>(); + + /** + * Start listening to tethering change events. Any new added callback will receive the last + * tethering status right away. If callback is registered when tethering has no upstream or + * disabled, {@link OnTetheringEventCallback#onUpstreamChanged} will immediately be called + * with a null argument. 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. + * + * @deprecated Use {@link TetheringManager#registerTetheringEventCallback} instead. + * @hide + */ + @SystemApi + @Deprecated + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + public void registerTetheringEventCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull final OnTetheringEventCallback callback) { + Objects.requireNonNull(callback, "OnTetheringEventCallback cannot be null."); + + final TetheringEventCallback tetherCallback = + new TetheringEventCallback() { + @Override + public void onUpstreamChanged(@Nullable Network network) { + callback.onUpstreamChanged(network); + } + }; + + synchronized (mTetheringEventCallbacks) { + mTetheringEventCallbacks.put(callback, tetherCallback); + mTetheringManager.registerTetheringEventCallback(executor, tetherCallback); + } + } + + /** + * Remove tethering event callback previously registered with + * {@link #registerTetheringEventCallback}. + * + * @param callback previously registered callback. + * + * @deprecated Use {@link TetheringManager#unregisterTetheringEventCallback} instead. + * @hide + */ + @SystemApi + @Deprecated + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + public void unregisterTetheringEventCallback( + @NonNull final OnTetheringEventCallback callback) { + Objects.requireNonNull(callback, "The callback must be non-null"); + synchronized (mTetheringEventCallbacks) { + final TetheringEventCallback tetherCallback = + mTetheringEventCallbacks.remove(callback); + mTetheringManager.unregisterTetheringEventCallback(tetherCallback); + } + } + + + /** + * 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. + * + * @deprecated Use {@link TetheringEventCallback#onTetherableInterfaceRegexpsChanged} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + @Deprecated + public String[] getTetherableUsbRegexs() { + return mTetheringManager.getTetherableUsbRegexs(); + } + + /** + * 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. + * + * @deprecated Use {@link TetheringEventCallback#onTetherableInterfaceRegexpsChanged} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + @Deprecated + public String[] getTetherableWifiRegexs() { + return mTetheringManager.getTetherableWifiRegexs(); + } + + /** + * 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. + * + * @deprecated Use {@link TetheringEventCallback#onTetherableInterfaceRegexpsChanged( + *TetheringManager.TetheringInterfaceRegexps)} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage + @Deprecated + public String[] getTetherableBluetoothRegexs() { + return mTetheringManager.getTetherableBluetoothRegexs(); + } + + /** + * Attempt to both alter the mode of USB and Tethering of USB. A + * utility method to deal with some of the complexity of USB - will + * attempt to switch to Rndis and subsequently tether the resulting + * interface on {@code true} or turn off tethering and switch off + * Rndis on {@code false}. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * @param enable a boolean - {@code true} to enable tethering + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * @deprecated Use {@link TetheringManager#startTethering} instead + * + * {@hide} + */ + @UnsupportedAppUsage + @Deprecated + public int setUsbTethering(boolean enable) { + return mTetheringManager.setUsbTethering(enable); + } + + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_NO_ERROR}. + * {@hide} + */ + @SystemApi + @Deprecated + public static final int TETHER_ERROR_NO_ERROR = 0; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNKNOWN_IFACE}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_UNKNOWN_IFACE = + TetheringManager.TETHER_ERROR_UNKNOWN_IFACE; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_SERVICE_UNAVAIL}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_SERVICE_UNAVAIL = + TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNSUPPORTED}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_UNSUPPORTED = TetheringManager.TETHER_ERROR_UNSUPPORTED; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNAVAIL_IFACE}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_UNAVAIL_IFACE = + TetheringManager.TETHER_ERROR_UNAVAIL_IFACE; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_INTERNAL_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_MASTER_ERROR = + TetheringManager.TETHER_ERROR_INTERNAL_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_TETHER_IFACE_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_TETHER_IFACE_ERROR = + TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_UNTETHER_IFACE_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = + TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_ENABLE_FORWARDING_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_ENABLE_NAT_ERROR = + TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_DISABLE_FORWARDING_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_DISABLE_NAT_ERROR = + TetheringManager.TETHER_ERROR_DISABLE_FORWARDING_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_IFACE_CFG_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_IFACE_CFG_ERROR = + TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_PROVISIONING_FAILED}. + * {@hide} + */ + @SystemApi + @Deprecated + public static final int TETHER_ERROR_PROVISION_FAILED = 11; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_DHCPSERVER_ERROR}. + * {@hide} + */ + @Deprecated + public static final int TETHER_ERROR_DHCPSERVER_ERROR = + TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR; + /** + * @deprecated Use {@link TetheringManager#TETHER_ERROR_ENTITLEMENT_UNKNOWN}. + * {@hide} + */ + @SystemApi + @Deprecated + public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN = 13; + + /** + * 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 + * + * @deprecated Use {@link TetheringEventCallback#onError(String, int)} instead. + * {@hide} + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Deprecated + public int getLastTetherError(String iface) { + int error = mTetheringManager.getLastTetherError(iface); + if (error == TetheringManager.TETHER_ERROR_UNKNOWN_TYPE) { + // TETHER_ERROR_UNKNOWN_TYPE was introduced with TetheringManager and has never been + // returned by ConnectivityManager. Convert it to the legacy TETHER_ERROR_UNKNOWN_IFACE + // instead. + error = TetheringManager.TETHER_ERROR_UNKNOWN_IFACE; + } + return error; + } + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TETHER_ERROR_NO_ERROR, + TETHER_ERROR_PROVISION_FAILED, + TETHER_ERROR_ENTITLEMENT_UNKONWN, + }) + public @interface EntitlementResultCode { + } + + /** + * Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether + * entitlement succeeded. + * + * @deprecated Use {@link TetheringManager#OnTetheringEntitlementResultListener} instead. + * @hide + */ + @SystemApi + @Deprecated + 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_PROVISION_FAILED}, or + * {@link #TETHER_ERROR_ENTITLEMENT_UNKONWN}. + */ + void onTetheringEntitlementResult(@EntitlementResultCode int resultCode); + } + + /** + * Get the last value of the entitlement check on this downstream. If the cached value is + * {@link #TETHER_ERROR_NO_ERROR} or showEntitlementUi argument is false, it just return the + * cached value. Otherwise, a UI-based entitlement check would be performed. It is not + * guaranteed that the UI-based entitlement check will complete in any specific time period + * and may in fact never complete. Any successful entitlement check the platform performs for + * any reason will update the cached value. + * + * @param type the downstream type of tethering. Must be one of + * {@link #TETHERING_WIFI}, + * {@link #TETHERING_USB}, or + * {@link #TETHERING_BLUETOOTH}. + * @param showEntitlementUi a boolean indicating whether to run UI-based 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. + * @deprecated Use {@link TetheringManager#requestLatestTetheringEntitlementResult} instead. + * {@hide} + */ + @SystemApi + @Deprecated + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + public void getLatestTetheringEntitlementResult(int type, boolean showEntitlementUi, + @NonNull @CallbackExecutor Executor executor, + @NonNull final OnTetheringEntitlementResultListener listener) { + Objects.requireNonNull(listener, "TetheringEntitlementResultListener cannot be null."); + ResultReceiver wrappedListener = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + final long token = Binder.clearCallingIdentity(); + try { + executor.execute(() -> { + listener.onTetheringEntitlementResult(resultCode); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + }; + + mTetheringManager.requestLatestTetheringEntitlementResult(type, wrappedListener, + showEntitlementUi); + } + + /** + * Report network connectivity status. This is currently used only + * to alter status bar UI. + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#STATUS_BAR}. + * + * @param networkType The type of network you want to report on + * @param percentage The quality of the connection 0 is bad, 100 is good + * @deprecated Types are deprecated. Use {@link #reportNetworkConnectivity} instead. + * {@hide} + */ + public void reportInetCondition(int networkType, int percentage) { + printStackTrace(); + try { + mService.reportInetCondition(networkType, percentage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report a problem network to the framework. This provides a hint to the system + * that there might be connectivity problems on this network and may cause + * the framework to re-evaluate network connectivity and/or switch to another + * network. + * + * @param network The {@link Network} the application was attempting to use + * or {@code null} to indicate the current default network. + * @deprecated Use {@link #reportNetworkConnectivity} which allows reporting both + * working and non-working connectivity. + */ + @Deprecated + public void reportBadNetwork(@Nullable Network network) { + printStackTrace(); + try { + // One of these will be ignored because it matches system's current state. + // The other will trigger the necessary reevaluation. + mService.reportNetworkConnectivity(network, true); + mService.reportNetworkConnectivity(network, false); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Report to the framework whether a network has working connectivity. + * This provides a hint to the system that a particular network is providing + * working connectivity or not. In response the framework may re-evaluate + * the network's connectivity and might take further action thereafter. + * + * @param network The {@link Network} the application was attempting to use + * or {@code null} to indicate the current default network. + * @param hasConnectivity {@code true} if the application was able to successfully access the + * Internet using {@code network} or {@code false} if not. + */ + public void reportNetworkConnectivity(@Nullable Network network, boolean hasConnectivity) { + printStackTrace(); + try { + mService.reportNetworkConnectivity(network, hasConnectivity); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set a network-independent global HTTP proxy. + * + * This sets an HTTP proxy that applies to all networks and overrides any network-specific + * proxy. If set, HTTP libraries that are proxy-aware will use this global proxy when + * accessing any network, regardless of what the settings for that network are. + * + * Note that HTTP proxies are by nature typically network-dependent, and setting a global + * proxy is likely to break networking on multiple networks. This method is only meant + * for device policy clients looking to do general internal filtering or similar use cases. + * + * {@see #getGlobalProxy} + * {@see LinkProperties#getHttpProxy} + * + * @param p A {@link ProxyInfo} object defining the new global HTTP proxy. Calling this + * method with a {@code null} value will clear the global HTTP proxy. + * @hide + */ + // Used by Device Policy Manager to set the global proxy. + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) + public void setGlobalProxy(@Nullable final ProxyInfo p) { + try { + mService.setGlobalProxy(p); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Retrieve any network-independent global HTTP proxy. + * + * @return {@link ProxyInfo} for the current global HTTP proxy or {@code null} + * if no global HTTP proxy is set. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @Nullable + public ProxyInfo getGlobalProxy() { + try { + return mService.getGlobalProxy(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Retrieve the global HTTP proxy, or if no global HTTP proxy is set, a + * network-specific HTTP proxy. If {@code network} is null, the + * network-specific proxy returned is the proxy of the default active + * network. + * + * @return {@link ProxyInfo} for the current global HTTP proxy, or if no + * global HTTP proxy is set, {@code ProxyInfo} for {@code network}, + * or when {@code network} is {@code null}, + * the {@code ProxyInfo} for the default active network. Returns + * {@code null} when no proxy applies or the caller doesn't have + * permission to use {@code network}. + * @hide + */ + public ProxyInfo getProxyForNetwork(Network network) { + try { + return mService.getProxyForNetwork(network); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Get the current default HTTP proxy settings. If a global proxy is set it will be returned, + * otherwise if this process is bound to a {@link Network} using + * {@link #bindProcessToNetwork} then that {@code Network}'s proxy is returned, otherwise + * the default network's proxy is returned. + * + * @return the {@link ProxyInfo} for the current HTTP proxy, or {@code null} if no + * HTTP proxy is active. + */ + @Nullable + public ProxyInfo getDefaultProxy() { + return getProxyForNetwork(getBoundNetworkForProcess()); + } + + /** + * Returns true if the hardware supports the given network type + * else it returns false. This doesn't indicate we have coverage + * or are authorized onto a network, just whether or not the + * hardware supports it. For example a GSM phone without a SIM + * should still return {@code true} for mobile data, but a wifi only + * tablet would return {@code false}. + * + * @param networkType The network type we'd like to check + * @return {@code true} if supported, else {@code false} + * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead. + * @hide + */ + @Deprecated + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562) + public boolean isNetworkSupported(int networkType) { + try { + return mService.isNetworkSupported(networkType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns if the currently active data network is metered. A network is + * classified as metered when the user is sensitive to heavy data usage on + * that connection due to monetary costs, data limitations or + * battery/performance issues. You should check this before doing large + * data transfers, and warn the user or delay the operation until another + * network is available. + * + * @return {@code true} if large transfers should be avoided, otherwise + * {@code false}. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public boolean isActiveNetworkMetered() { + try { + return mService.isActiveNetworkMetered(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set sign in error notification to visible or invisible + * + * @hide + * @deprecated Doesn't properly deal with multiple connected networks of the same type. + */ + @Deprecated + public void setProvisioningNotificationVisible(boolean visible, int networkType, + String action) { + try { + mService.setProvisioningNotificationVisible(visible, networkType, action); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the value for enabling/disabling airplane mode + * + * @param enable whether to enable airplane mode or not + * + * @hide + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_AIRPLANE_MODE, + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_SETUP_WIZARD, + android.Manifest.permission.NETWORK_STACK}) + @SystemApi + public void setAirplaneMode(boolean enable) { + try { + mService.setAirplaneMode(enable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers the specified {@link NetworkProvider}. + * Each listener must only be registered once. The listener can be unregistered with + * {@link #unregisterNetworkProvider}. + * + * @param provider the provider to register + * @return the ID of the provider. This ID must be used by the provider when registering + * {@link android.net.NetworkAgent}s. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_FACTORY}) + public int registerNetworkProvider(@NonNull NetworkProvider provider) { + if (provider.getProviderId() != NetworkProvider.ID_NONE) { + throw new IllegalStateException("NetworkProviders can only be registered once"); + } + + try { + int providerId = mService.registerNetworkProvider(provider.getMessenger(), + provider.getName()); + provider.setProviderId(providerId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + return provider.getProviderId(); + } + + /** + * Unregisters the specified NetworkProvider. + * + * @param provider the provider to unregister + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_FACTORY}) + public void unregisterNetworkProvider(@NonNull NetworkProvider provider) { + try { + mService.unregisterNetworkProvider(provider.getMessenger()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + provider.setProviderId(NetworkProvider.ID_NONE); + } + + /** + * Register or update a network offer with ConnectivityService. + * + * ConnectivityService keeps track of offers made by the various providers and matches + * them to networking requests made by apps or the system. A callback identifies an offer + * uniquely, and later calls with the same callback update the offer. The provider supplies a + * score and the capabilities of the network it might be able to bring up ; these act as + * filters used by ConnectivityService to only send those requests that can be fulfilled by the + * provider. + * + * The provider is under no obligation to be able to bring up the network it offers at any + * given time. Instead, this mechanism is meant to limit requests received by providers + * to those they actually have a chance to fulfill, as providers don't have a way to compare + * the quality of the network satisfying a given request to their own offer. + * + * An offer can be updated by calling this again with the same callback object. This is + * similar to calling unofferNetwork and offerNetwork again, but will only update the + * provider with the changes caused by the changes in the offer. + * + * @param provider The provider making this offer. + * @param score The prospective score of the network. + * @param caps The prospective capabilities of the network. + * @param callback The callback to call when this offer is needed or unneeded. + * @hide exposed via the NetworkProvider class. + */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_FACTORY}) + public void offerNetwork(@NonNull final int providerId, + @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps, + @NonNull final INetworkOfferCallback callback) { + try { + mService.offerNetwork(providerId, + Objects.requireNonNull(score, "null score"), + Objects.requireNonNull(caps, "null caps"), + Objects.requireNonNull(callback, "null callback")); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Withdraw a network offer made with {@link #offerNetwork}. + * + * @param callback The callback passed at registration time. This must be the same object + * that was passed to {@link #offerNetwork} + * @hide exposed via the NetworkProvider class. + */ + public void unofferNetwork(@NonNull final INetworkOfferCallback callback) { + try { + mService.unofferNetwork(Objects.requireNonNull(callback)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** @hide exposed via the NetworkProvider class. */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_FACTORY}) + public void declareNetworkRequestUnfulfillable(@NonNull NetworkRequest request) { + try { + mService.declareNetworkRequestUnfulfillable(request); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Register a NetworkAgent with ConnectivityService. + * @return Network corresponding to NetworkAgent. + */ + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_FACTORY}) + public Network registerNetworkAgent(INetworkAgent na, NetworkInfo ni, LinkProperties lp, + NetworkCapabilities nc, @NonNull NetworkScore score, NetworkAgentConfig config, + int providerId) { + try { + return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Base class for {@code NetworkRequest} callbacks. Used for notifications about network + * changes. Should be extended by applications wanting notifications. + * + * A {@code NetworkCallback} is registered by calling + * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, + * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)}, + * or {@link #registerDefaultNetworkCallback(NetworkCallback)}. A {@code NetworkCallback} is + * unregistered by calling {@link #unregisterNetworkCallback(NetworkCallback)}. + * A {@code NetworkCallback} should be registered at most once at any time. + * A {@code NetworkCallback} that has been unregistered can be registered again. + */ + public static class NetworkCallback { + /** + * No flags associated with this callback. + * @hide + */ + public static final int FLAG_NONE = 0; + /** + * Use this flag to include any location sensitive data in {@link NetworkCapabilities} sent + * via {@link #onCapabilitiesChanged(Network, NetworkCapabilities)}. + * <p> + * These include: + * <li> Some transport info instances (retrieved via + * {@link NetworkCapabilities#getTransportInfo()}) like {@link android.net.wifi.WifiInfo} + * contain location sensitive information. + * <li> OwnerUid (retrieved via {@link NetworkCapabilities#getOwnerUid()} is location + * sensitive for wifi suggestor apps (i.e using {@link WifiNetworkSuggestion}).</li> + * </p> + * <p> + * Note: + * <li> Retrieving this location sensitive information (subject to app's location + * permissions) will be noted by system. </li> + * <li> Without this flag any {@link NetworkCapabilities} provided via the callback does + * not include location sensitive info. + * </p> + */ + // Note: Some existing fields which are location sensitive may still be included without + // this flag if the app targets SDK < S (to maintain backwards compatibility). + public static final int FLAG_INCLUDE_LOCATION_INFO = 1 << 0; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = "FLAG_", value = { + FLAG_NONE, + FLAG_INCLUDE_LOCATION_INFO + }) + public @interface Flag { } + + /** + * All the valid flags for error checking. + */ + private static final int VALID_FLAGS = FLAG_INCLUDE_LOCATION_INFO; + + public NetworkCallback() { + this(FLAG_NONE); + } + + public NetworkCallback(@Flag int flags) { + if ((flags & VALID_FLAGS) != flags) { + throw new IllegalArgumentException("Invalid flags"); + } + mFlags = flags; + } + + /** + * Called when the framework connects to a new network to evaluate whether it satisfies this + * request. If evaluation succeeds, this callback may be followed by an {@link #onAvailable} + * callback. There is no guarantee that this new network will satisfy any requests, or that + * the network will stay connected for longer than the time necessary to evaluate it. + * <p> + * Most applications <b>should not</b> act on this callback, and should instead use + * {@link #onAvailable}. This callback is intended for use by applications that can assist + * the framework in properly evaluating the network — for example, an application that + * can automatically log in to a captive portal without user intervention. + * + * @param network The {@link Network} of the network that is being evaluated. + * + * @hide + */ + public void onPreCheck(@NonNull Network network) {} + + /** + * Called when the framework connects and has declared a new network ready for use. + * This callback may be called more than once if the {@link Network} that is + * satisfying the request changes. + * + * @param network The {@link Network} of the satisfying network. + * @param networkCapabilities The {@link NetworkCapabilities} of the satisfying network. + * @param linkProperties The {@link LinkProperties} of the satisfying network. + * @param blocked Whether access to the {@link Network} is blocked due to system policy. + * @hide + */ + public final void onAvailable(@NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties, @BlockedReason int blocked) { + // Internally only this method is called when a new network is available, and + // it calls the callback in the same way and order that older versions used + // to call so as not to change the behavior. + onAvailable(network, networkCapabilities, linkProperties, blocked != 0); + onBlockedStatusChanged(network, blocked); + } + + /** + * Legacy variant of onAvailable that takes a boolean blocked reason. + * + * This method has never been public API, but it's not final, so there may be apps that + * implemented it and rely on it being called. Do our best not to break them. + * Note: such apps will also get a second call to onBlockedStatusChanged immediately after + * this method is called. There does not seem to be a way to avoid this. + * TODO: add a compat check to move apps off this method, and eventually stop calling it. + * + * @hide + */ + public void onAvailable(@NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties, boolean blocked) { + onAvailable(network); + if (!networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) { + onNetworkSuspended(network); + } + onCapabilitiesChanged(network, networkCapabilities); + onLinkPropertiesChanged(network, linkProperties); + // No call to onBlockedStatusChanged here. That is done by the caller. + } + + /** + * Called when the framework connects and has declared a new network ready for use. + * + * <p>For callbacks registered with {@link #registerNetworkCallback}, multiple networks may + * be available at the same time, and onAvailable will be called for each of these as they + * appear. + * + * <p>For callbacks registered with {@link #requestNetwork} and + * {@link #registerDefaultNetworkCallback}, this means the network passed as an argument + * is the new best network for this request and is now tracked by this callback ; this + * callback will no longer receive method calls about other networks that may have been + * passed to this method previously. The previously-best network may have disconnected, or + * it may still be around and the newly-best network may simply be better. + * + * <p>Starting with {@link android.os.Build.VERSION_CODES#O}, this will always immediately + * be followed by a call to {@link #onCapabilitiesChanged(Network, NetworkCapabilities)} + * then by a call to {@link #onLinkPropertiesChanged(Network, LinkProperties)}, and a call + * to {@link #onBlockedStatusChanged(Network, boolean)}. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions (there is no guarantee the objects + * returned by these methods will be current). Instead, wait for a call to + * {@link #onCapabilitiesChanged(Network, NetworkCapabilities)} and + * {@link #onLinkPropertiesChanged(Network, LinkProperties)} whose arguments are guaranteed + * to be well-ordered with respect to other callbacks. + * + * @param network The {@link Network} of the satisfying network. + */ + public void onAvailable(@NonNull Network network) {} + + /** + * Called when the network is about to be lost, typically because there are no outstanding + * requests left for it. This may be paired with a {@link NetworkCallback#onAvailable} call + * with the new replacement network for graceful handover. This method is not guaranteed + * to be called before {@link NetworkCallback#onLost} is called, for example in case a + * network is suddenly disconnected. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions ; calling these methods while in a + * callback may return an outdated or even a null object. + * + * @param network The {@link Network} that is about to be lost. + * @param maxMsToLive The time in milliseconds the system intends to keep the network + * connected for graceful handover; note that the network may still + * suffer a hard loss at any time. + */ + public void onLosing(@NonNull Network network, int maxMsToLive) {} + + /** + * Called when a network disconnects or otherwise no longer satisfies this request or + * callback. + * + * <p>If the callback was registered with requestNetwork() or + * registerDefaultNetworkCallback(), it will only be invoked against the last network + * returned by onAvailable() when that network is lost and no other network satisfies + * the criteria of the request. + * + * <p>If the callback was registered with registerNetworkCallback() it will be called for + * each network which no longer satisfies the criteria of the callback. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions ; calling these methods while in a + * callback may return an outdated or even a null object. + * + * @param network The {@link Network} lost. + */ + public void onLost(@NonNull Network network) {} + + /** + * Called if no network is found within the timeout time specified in + * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the + * requested network request cannot be fulfilled (whether or not a timeout was + * specified). When this callback is invoked the associated + * {@link NetworkRequest} will have already been removed and released, as if + * {@link #unregisterNetworkCallback(NetworkCallback)} had been called. + */ + public void onUnavailable() {} + + /** + * Called when the network corresponding to this request changes capabilities but still + * satisfies the requested criteria. + * + * <p>Starting with {@link android.os.Build.VERSION_CODES#O} this method is guaranteed + * to be called immediately after {@link #onAvailable}. + * + * <p>Do NOT call {@link #getLinkProperties(Network)} or other synchronous + * ConnectivityManager methods in this callback as this is prone to race conditions : + * calling these methods while in a callback may return an outdated or even a null object. + * + * @param network The {@link Network} whose capabilities have changed. + * @param networkCapabilities The new {@link NetworkCapabilities} for this + * network. + */ + public void onCapabilitiesChanged(@NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities) {} + + /** + * Called when the network corresponding to this request changes {@link LinkProperties}. + * + * <p>Starting with {@link android.os.Build.VERSION_CODES#O} this method is guaranteed + * to be called immediately after {@link #onAvailable}. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or other synchronous + * ConnectivityManager methods in this callback as this is prone to race conditions : + * calling these methods while in a callback may return an outdated or even a null object. + * + * @param network The {@link Network} whose link properties have changed. + * @param linkProperties The new {@link LinkProperties} for this network. + */ + public void onLinkPropertiesChanged(@NonNull Network network, + @NonNull LinkProperties linkProperties) {} + + /** + * Called when the network the framework connected to for this request suspends data + * transmission temporarily. + * + * <p>This generally means that while the TCP connections are still live temporarily + * network data fails to transfer. To give a specific example, this is used on cellular + * networks to mask temporary outages when driving through a tunnel, etc. In general this + * means read operations on sockets on this network will block once the buffers are + * drained, and write operations will block once the buffers are full. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions (there is no guarantee the objects + * returned by these methods will be current). + * + * @hide + */ + public void onNetworkSuspended(@NonNull Network network) {} + + /** + * Called when the network the framework connected to for this request + * returns from a {@link NetworkInfo.State#SUSPENDED} state. This should always be + * preceded by a matching {@link NetworkCallback#onNetworkSuspended} call. + + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions : calling these methods while in a + * callback may return an outdated or even a null object. + * + * @hide + */ + public void onNetworkResumed(@NonNull Network network) {} + + /** + * Called when access to the specified network is blocked or unblocked. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions : calling these methods while in a + * callback may return an outdated or even a null object. + * + * @param network The {@link Network} whose blocked status has changed. + * @param blocked The blocked status of this {@link Network}. + */ + public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {} + + /** + * Called when access to the specified network is blocked or unblocked, or the reason for + * access being blocked changes. + * + * If a NetworkCallback object implements this method, + * {@link #onBlockedStatusChanged(Network, boolean)} will not be called. + * + * <p>Do NOT call {@link #getNetworkCapabilities(Network)} or + * {@link #getLinkProperties(Network)} or other synchronous ConnectivityManager methods in + * this callback as this is prone to race conditions : calling these methods while in a + * callback may return an outdated or even a null object. + * + * @param network The {@link Network} whose blocked status has changed. + * @param blocked The blocked status of this {@link Network}. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public void onBlockedStatusChanged(@NonNull Network network, @BlockedReason int blocked) { + onBlockedStatusChanged(network, blocked != 0); + } + + private NetworkRequest networkRequest; + private final int mFlags; + } + + /** + * Constant error codes used by ConnectivityService to communicate about failures and errors + * across a Binder boundary. + * @hide + */ + public interface Errors { + int TOO_MANY_REQUESTS = 1; + } + + /** @hide */ + public static class TooManyRequestsException extends RuntimeException {} + + private static RuntimeException convertServiceException(ServiceSpecificException e) { + switch (e.errorCode) { + case Errors.TOO_MANY_REQUESTS: + return new TooManyRequestsException(); + default: + Log.w(TAG, "Unknown service error code " + e.errorCode); + return new RuntimeException(e); + } + } + + /** @hide */ + public static final int CALLBACK_PRECHECK = 1; + /** @hide */ + public static final int CALLBACK_AVAILABLE = 2; + /** @hide arg1 = TTL */ + public static final int CALLBACK_LOSING = 3; + /** @hide */ + public static final int CALLBACK_LOST = 4; + /** @hide */ + public static final int CALLBACK_UNAVAIL = 5; + /** @hide */ + public static final int CALLBACK_CAP_CHANGED = 6; + /** @hide */ + public static final int CALLBACK_IP_CHANGED = 7; + /** @hide obj = NetworkCapabilities, arg1 = seq number */ + private static final int EXPIRE_LEGACY_REQUEST = 8; + /** @hide */ + public static final int CALLBACK_SUSPENDED = 9; + /** @hide */ + public static final int CALLBACK_RESUMED = 10; + /** @hide */ + public static final int CALLBACK_BLK_CHANGED = 11; + + /** @hide */ + public static String getCallbackName(int whichCallback) { + switch (whichCallback) { + case CALLBACK_PRECHECK: return "CALLBACK_PRECHECK"; + case CALLBACK_AVAILABLE: return "CALLBACK_AVAILABLE"; + case CALLBACK_LOSING: return "CALLBACK_LOSING"; + case CALLBACK_LOST: return "CALLBACK_LOST"; + case CALLBACK_UNAVAIL: return "CALLBACK_UNAVAIL"; + case CALLBACK_CAP_CHANGED: return "CALLBACK_CAP_CHANGED"; + case CALLBACK_IP_CHANGED: return "CALLBACK_IP_CHANGED"; + case EXPIRE_LEGACY_REQUEST: return "EXPIRE_LEGACY_REQUEST"; + case CALLBACK_SUSPENDED: return "CALLBACK_SUSPENDED"; + case CALLBACK_RESUMED: return "CALLBACK_RESUMED"; + case CALLBACK_BLK_CHANGED: return "CALLBACK_BLK_CHANGED"; + default: + return Integer.toString(whichCallback); + } + } + + private class CallbackHandler extends Handler { + private static final String TAG = "ConnectivityManager.CallbackHandler"; + private static final boolean DBG = false; + + CallbackHandler(Looper looper) { + super(looper); + } + + CallbackHandler(Handler handler) { + this(Objects.requireNonNull(handler, "Handler cannot be null.").getLooper()); + } + + @Override + public void handleMessage(Message message) { + if (message.what == EXPIRE_LEGACY_REQUEST) { + expireRequest((NetworkCapabilities) message.obj, message.arg1); + return; + } + + final NetworkRequest request = getObject(message, NetworkRequest.class); + final Network network = getObject(message, Network.class); + final NetworkCallback callback; + synchronized (sCallbacks) { + callback = sCallbacks.get(request); + if (callback == null) { + Log.w(TAG, + "callback not found for " + getCallbackName(message.what) + " message"); + return; + } + if (message.what == CALLBACK_UNAVAIL) { + sCallbacks.remove(request); + callback.networkRequest = ALREADY_UNREGISTERED; + } + } + if (DBG) { + Log.d(TAG, getCallbackName(message.what) + " for network " + network); + } + + switch (message.what) { + case CALLBACK_PRECHECK: { + callback.onPreCheck(network); + break; + } + case CALLBACK_AVAILABLE: { + NetworkCapabilities cap = getObject(message, NetworkCapabilities.class); + LinkProperties lp = getObject(message, LinkProperties.class); + callback.onAvailable(network, cap, lp, message.arg1); + break; + } + case CALLBACK_LOSING: { + callback.onLosing(network, message.arg1); + break; + } + case CALLBACK_LOST: { + callback.onLost(network); + break; + } + case CALLBACK_UNAVAIL: { + callback.onUnavailable(); + break; + } + case CALLBACK_CAP_CHANGED: { + NetworkCapabilities cap = getObject(message, NetworkCapabilities.class); + callback.onCapabilitiesChanged(network, cap); + break; + } + case CALLBACK_IP_CHANGED: { + LinkProperties lp = getObject(message, LinkProperties.class); + callback.onLinkPropertiesChanged(network, lp); + break; + } + case CALLBACK_SUSPENDED: { + callback.onNetworkSuspended(network); + break; + } + case CALLBACK_RESUMED: { + callback.onNetworkResumed(network); + break; + } + case CALLBACK_BLK_CHANGED: { + callback.onBlockedStatusChanged(network, message.arg1); + } + } + } + + private <T> T getObject(Message msg, Class<T> c) { + return (T) msg.getData().getParcelable(c.getSimpleName()); + } + } + + private CallbackHandler getDefaultHandler() { + synchronized (sCallbacks) { + if (sCallbackHandler == null) { + sCallbackHandler = new CallbackHandler(ConnectivityThread.getInstanceLooper()); + } + return sCallbackHandler; + } + } + + private static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>(); + private static CallbackHandler sCallbackHandler; + + private NetworkRequest sendRequestForNetwork(int asUid, NetworkCapabilities need, + NetworkCallback callback, int timeoutMs, NetworkRequest.Type reqType, int legacyType, + CallbackHandler handler) { + printStackTrace(); + checkCallbackNotNull(callback); + if (reqType != TRACK_DEFAULT && reqType != TRACK_SYSTEM_DEFAULT && need == null) { + throw new IllegalArgumentException("null NetworkCapabilities"); + } + final NetworkRequest request; + final String callingPackageName = mContext.getOpPackageName(); + try { + synchronized(sCallbacks) { + if (callback.networkRequest != null + && callback.networkRequest != ALREADY_UNREGISTERED) { + // TODO: throw exception instead and enforce 1:1 mapping of callbacks + // and requests (http://b/20701525). + Log.e(TAG, "NetworkCallback was already registered"); + } + Messenger messenger = new Messenger(handler); + Binder binder = new Binder(); + final int callbackFlags = callback.mFlags; + if (reqType == LISTEN) { + request = mService.listenForNetwork( + need, messenger, binder, callbackFlags, callingPackageName, + getAttributionTag()); + } else { + request = mService.requestNetwork( + asUid, need, reqType.ordinal(), messenger, timeoutMs, binder, + legacyType, callbackFlags, callingPackageName, getAttributionTag()); + } + if (request != null) { + sCallbacks.put(request, callback); + } + callback.networkRequest = request; + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw convertServiceException(e); + } + return request; + } + + private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback, + int timeoutMs, NetworkRequest.Type reqType, int legacyType, CallbackHandler handler) { + return sendRequestForNetwork(Process.INVALID_UID, need, callback, timeoutMs, reqType, + legacyType, handler); + } + + /** + * Helper function to request a network with a particular legacy type. + * + * This API is only for use in internal system code that requests networks with legacy type and + * relies on CONNECTIVITY_ACTION broadcasts instead of NetworkCallbacks. New caller should use + * {@link #requestNetwork(NetworkRequest, NetworkCallback, Handler)} instead. + * + * @param request {@link NetworkRequest} describing this request. + * @param timeoutMs The time in milliseconds to attempt looking for a suitable network + * before {@link NetworkCallback#onUnavailable()} is called. The timeout must + * be a positive value (i.e. >0). + * @param legacyType to specify the network type(#TYPE_*). + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + * + * @hide + */ + @SystemApi + @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + public void requestNetwork(@NonNull NetworkRequest request, + int timeoutMs, int legacyType, @NonNull Handler handler, + @NonNull NetworkCallback networkCallback) { + if (legacyType == TYPE_NONE) { + throw new IllegalArgumentException("TYPE_NONE is meaningless legacy type"); + } + CallbackHandler cbHandler = new CallbackHandler(handler); + NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, legacyType, cbHandler); + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}. + * + * <p>This method will attempt to find the best network that matches the passed + * {@link NetworkRequest}, and to bring up one that does if none currently satisfies the + * criteria. The platform will evaluate which network is the best at its own discretion. + * Throughput, latency, cost per byte, policy, user preference and other considerations + * may be factored in the decision of what is considered the best network. + * + * <p>As long as this request is outstanding, the platform will try to maintain the best network + * matching this request, while always attempting to match the request to a better network if + * possible. If a better match is found, the platform will switch this request to the now-best + * network and inform the app of the newly best network by invoking + * {@link NetworkCallback#onAvailable(Network)} on the provided callback. Note that the platform + * will not try to maintain any other network than the best one currently matching the request: + * a network not matching any network request may be disconnected at any time. + * + * <p>For example, an application could use this method to obtain a connected cellular network + * even if the device currently has a data connection over Ethernet. This may cause the cellular + * radio to consume additional power. Or, an application could inform the system that it wants + * a network supporting sending MMSes and have the system let it know about the currently best + * MMS-supporting network through the provided {@link NetworkCallback}. + * + * <p>The status of the request can be followed by listening to the various callbacks described + * in {@link NetworkCallback}. The {@link Network} object passed to the callback methods can be + * used to direct traffic to the network (although accessing some networks may be subject to + * holding specific permissions). Callers will learn about the specific characteristics of the + * network through + * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)} and + * {@link NetworkCallback#onLinkPropertiesChanged(Network, LinkProperties)}. The methods of the + * provided {@link NetworkCallback} will only be invoked due to changes in the best network + * matching the request at any given time; therefore when a better network matching the request + * becomes available, the {@link NetworkCallback#onAvailable(Network)} method is called + * with the new network after which no further updates are given about the previously-best + * network, unless it becomes the best again at some later time. All callbacks are invoked + * in order on the same thread, which by default is a thread created by the framework running + * in the app. + * {@see #requestNetwork(NetworkRequest, NetworkCallback, Handler)} to change where the + * callbacks are invoked. + * + * <p>This{@link NetworkRequest} will live until released via + * {@link #unregisterNetworkCallback(NetworkCallback)} or the calling application exits, at + * which point the system may let go of the network at any time. + * + * <p>A version of this method which takes a timeout is + * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)}, that an app can use to only + * wait for a limited amount of time for the network to become unavailable. + * + * <p>It is presently unsupported to request a network with mutable + * {@link NetworkCapabilities} such as + * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or + * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL} + * as these {@code NetworkCapabilities} represent states that a particular + * network may never attain, and whether a network will attain these states + * is unknown prior to bringing up the network so the framework does not + * know how to go about satisfying a request with these capabilities. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #registerNetworkCallback} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + * The callback is invoked on the default internal Handler. + * @throws IllegalArgumentException if {@code request} contains invalid network capabilities. + * @throws SecurityException if missing the appropriate permissions. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + public void requestNetwork(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback) { + requestNetwork(request, networkCallback, getDefaultHandler()); + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}. + * + * This method behaves identically to {@link #requestNetwork(NetworkRequest, NetworkCallback)} + * but runs all the callbacks on the passed Handler. + * + * <p>This method has the same permission requirements as + * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations, + * and throws the same exceptions in the same conditions. + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + */ + public void requestNetwork(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback, @NonNull Handler handler) { + CallbackHandler cbHandler = new CallbackHandler(handler); + NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, 0, REQUEST, TYPE_NONE, cbHandler); + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}, limited + * by a timeout. + * + * This function behaves identically to the non-timed-out version + * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, but if a suitable network + * is not found within the given time (in milliseconds) the + * {@link NetworkCallback#onUnavailable()} callback is called. The request can still be + * released normally by calling {@link #unregisterNetworkCallback(NetworkCallback)} but does + * not have to be released if timed-out (it is automatically released). Unregistering a + * request that timed out is not an error. + * + * <p>Do not use this method to poll for the existence of specific networks (e.g. with a small + * timeout) - {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} is provided + * for that purpose. Calling this method will attempt to bring up the requested network. + * + * <p>This method has the same permission requirements as + * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations, + * and throws the same exceptions in the same conditions. + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + * @param timeoutMs The time in milliseconds to attempt looking for a suitable network + * before {@link NetworkCallback#onUnavailable()} is called. The timeout must + * be a positive value (i.e. >0). + */ + public void requestNetwork(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback, int timeoutMs) { + checkTimeout(timeoutMs); + NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, TYPE_NONE, + getDefaultHandler()); + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}, limited + * by a timeout. + * + * This method behaves identically to + * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} but runs all the callbacks + * on the passed Handler. + * + * <p>This method has the same permission requirements as + * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations, + * and throws the same exceptions in the same conditions. + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @param timeoutMs The time in milliseconds to attempt looking for a suitable network + * before {@link NetworkCallback#onUnavailable} is called. + */ + public void requestNetwork(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback, @NonNull Handler handler, int timeoutMs) { + checkTimeout(timeoutMs); + CallbackHandler cbHandler = new CallbackHandler(handler); + NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, TYPE_NONE, cbHandler); + } + + /** + * The lookup key for a {@link Network} object included with the intent after + * successfully finding a network for the applications request. Retrieve it with + * {@link android.content.Intent#getParcelableExtra(String)}. + * <p> + * Note that if you intend to invoke {@link Network#openConnection(java.net.URL)} + * then you must get a ConnectivityManager instance before doing so. + */ + public static final String EXTRA_NETWORK = "android.net.extra.NETWORK"; + + /** + * The lookup key for a {@link NetworkRequest} object included with the intent after + * successfully finding a network for the applications request. Retrieve it with + * {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST"; + + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}. + * + * This function behaves identically to the version that takes a NetworkCallback, but instead + * of {@link NetworkCallback} a {@link PendingIntent} is used. This means + * the request may outlive the calling application and get called back when a suitable + * network is found. + * <p> + * The operation is an Intent broadcast that goes to a broadcast receiver that + * you registered with {@link Context#registerReceiver} or through the + * <receiver> tag in an AndroidManifest.xml file + * <p> + * The operation Intent is delivered with two extras, a {@link Network} typed + * extra called {@link #EXTRA_NETWORK} and a {@link NetworkRequest} + * typed extra called {@link #EXTRA_NETWORK_REQUEST} containing + * the original requests parameters. It is important to create a new, + * {@link NetworkCallback} based request before completing the processing of the + * Intent to reserve the network or it will be released shortly after the Intent + * is processed. + * <p> + * If there is already a request for this Intent registered (with the equality of + * two Intents defined by {@link Intent#filterEquals}), then it will be removed and + * replaced by this one, effectively releasing the previous {@link NetworkRequest}. + * <p> + * The request may be released normally by calling + * {@link #releaseNetworkRequest(android.app.PendingIntent)}. + * <p>It is presently unsupported to request a network with either + * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or + * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL} + * as these {@code NetworkCapabilities} represent states that a particular + * network may never attain, and whether a network will attain these states + * is unknown prior to bringing up the network so the framework does not + * know how to go about satisfying a request with these capabilities. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #registerNetworkCallback} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with {@link #unregisterNetworkCallback(PendingIntent)} + * or {@link #releaseNetworkRequest(PendingIntent)}. + * + * <p>This method requires the caller to hold either the + * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission + * or the ability to modify system settings as determined by + * {@link android.provider.Settings.System#canWrite}.</p> + * + * @param request {@link NetworkRequest} describing this request. + * @param operation Action to perform when the network is available (corresponds + * to the {@link NetworkCallback#onAvailable} call. Typically + * comes from {@link PendingIntent#getBroadcast}. Cannot be null. + * @throws IllegalArgumentException if {@code request} contains invalid network capabilities. + * @throws SecurityException if missing the appropriate permissions. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + public void requestNetwork(@NonNull NetworkRequest request, + @NonNull PendingIntent operation) { + printStackTrace(); + checkPendingIntentNotNull(operation); + try { + mService.pendingRequestForNetwork( + request.networkCapabilities, operation, mContext.getOpPackageName(), + getAttributionTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw convertServiceException(e); + } + } + + /** + * Removes a request made via {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)} + * <p> + * This method has the same behavior as + * {@link #unregisterNetworkCallback(android.app.PendingIntent)} with respect to + * releasing network resources and disconnecting. + * + * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the + * PendingIntent passed to + * {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)} with the + * corresponding NetworkRequest you'd like to remove. Cannot be null. + */ + public void releaseNetworkRequest(@NonNull PendingIntent operation) { + printStackTrace(); + checkPendingIntentNotNull(operation); + try { + mService.releasePendingNetworkRequest(operation); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static void checkPendingIntentNotNull(PendingIntent intent) { + Objects.requireNonNull(intent, "PendingIntent cannot be null."); + } + + private static void checkCallbackNotNull(NetworkCallback callback) { + Objects.requireNonNull(callback, "null NetworkCallback"); + } + + private static void checkTimeout(int timeoutMs) { + if (timeoutMs <= 0) { + throw new IllegalArgumentException("timeoutMs must be strictly positive."); + } + } + + /** + * Registers to receive notifications about all networks which satisfy the given + * {@link NetworkRequest}. The callbacks will continue to be called until + * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is + * called. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} that the system will call as suitable + * networks change state. + * The callback is invoked on the default internal Handler. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public void registerNetworkCallback(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback) { + registerNetworkCallback(request, networkCallback, getDefaultHandler()); + } + + /** + * Registers to receive notifications about all networks which satisfy the given + * {@link NetworkRequest}. The callbacks will continue to be called until + * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is + * called. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} that the system will call as suitable + * networks change state. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public void registerNetworkCallback(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback, @NonNull Handler handler) { + CallbackHandler cbHandler = new CallbackHandler(handler); + NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, 0, LISTEN, TYPE_NONE, cbHandler); + } + + /** + * Registers a PendingIntent to be sent when a network is available which satisfies the given + * {@link NetworkRequest}. + * + * This function behaves identically to the version that takes a NetworkCallback, but instead + * of {@link NetworkCallback} a {@link PendingIntent} is used. This means + * the request may outlive the calling application and get called back when a suitable + * network is found. + * <p> + * The operation is an Intent broadcast that goes to a broadcast receiver that + * you registered with {@link Context#registerReceiver} or through the + * <receiver> tag in an AndroidManifest.xml file + * <p> + * The operation Intent is delivered with two extras, a {@link Network} typed + * extra called {@link #EXTRA_NETWORK} and a {@link NetworkRequest} + * typed extra called {@link #EXTRA_NETWORK_REQUEST} containing + * the original requests parameters. + * <p> + * If there is already a request for this Intent registered (with the equality of + * two Intents defined by {@link Intent#filterEquals}), then it will be removed and + * replaced by this one, effectively releasing the previous {@link NetworkRequest}. + * <p> + * The request may be released normally by calling + * {@link #unregisterNetworkCallback(android.app.PendingIntent)}. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with {@link #unregisterNetworkCallback(PendingIntent)} + * or {@link #releaseNetworkRequest(PendingIntent)}. + * + * @param request {@link NetworkRequest} describing this request. + * @param operation Action to perform when the network is available (corresponds + * to the {@link NetworkCallback#onAvailable} call. Typically + * comes from {@link PendingIntent#getBroadcast}. Cannot be null. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public void registerNetworkCallback(@NonNull NetworkRequest request, + @NonNull PendingIntent operation) { + printStackTrace(); + checkPendingIntentNotNull(operation); + try { + mService.pendingListenForNetwork( + request.networkCapabilities, operation, mContext.getOpPackageName(), + getAttributionTag()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (ServiceSpecificException e) { + throw convertServiceException(e); + } + } + + /** + * Registers to receive notifications about changes in the application's default network. This + * may be a physical network or a virtual network, such as a VPN that applies to the + * application. The callbacks will continue to be called until either the application exits or + * {@link #unregisterNetworkCallback(NetworkCallback)} is called. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param networkCallback The {@link NetworkCallback} that the system will call as the + * application's default network changes. + * The callback is invoked on the default internal Handler. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback) { + registerDefaultNetworkCallback(networkCallback, getDefaultHandler()); + } + + /** + * Registers to receive notifications about changes in the application's default network. This + * may be a physical network or a virtual network, such as a VPN that applies to the + * application. The callbacks will continue to be called until either the application exits or + * {@link #unregisterNetworkCallback(NetworkCallback)} is called. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param networkCallback The {@link NetworkCallback} that the system will call as the + * application's default network changes. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback, + @NonNull Handler handler) { + registerDefaultNetworkCallbackForUid(Process.INVALID_UID, networkCallback, handler); + } + + /** + * Registers to receive notifications about changes in the default network for the specified + * UID. This may be a physical network or a virtual network, such as a VPN that applies to the + * UID. The callbacks will continue to be called until either the application exits or + * {@link #unregisterNetworkCallback(NetworkCallback)} is called. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param uid the UID for which to track default network changes. + * @param networkCallback The {@link NetworkCallback} that the system will call as the + * UID's default network changes. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @throws RuntimeException if the app already has too many callbacks registered. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @SuppressLint({"ExecutorRegistration", "PairedRegistration"}) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + public void registerDefaultNetworkCallbackForUid(int uid, + @NonNull NetworkCallback networkCallback, @NonNull Handler handler) { + CallbackHandler cbHandler = new CallbackHandler(handler); + sendRequestForNetwork(uid, null /* need */, networkCallback, 0 /* timeoutMs */, + TRACK_DEFAULT, TYPE_NONE, cbHandler); + } + + /** + * Registers to receive notifications about changes in the system default network. The callbacks + * will continue to be called until either the application exits or + * {@link #unregisterNetworkCallback(NetworkCallback)} is called. + * + * This method should not be used to determine networking state seen by applications, because in + * many cases, most or even all application traffic may not use the default network directly, + * and traffic from different applications may go on different networks by default. As an + * example, if a VPN is connected, traffic from all applications might be sent through the VPN + * and not onto the system default network. Applications or system components desiring to do + * determine network state as seen by applications should use other methods such as + * {@link #registerDefaultNetworkCallback(NetworkCallback, Handler)}. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param networkCallback The {@link NetworkCallback} that the system will call as the + * system default network changes. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @throws RuntimeException if the app already has too many callbacks registered. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @SuppressLint({"ExecutorRegistration", "PairedRegistration"}) + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS}) + public void registerSystemDefaultNetworkCallback(@NonNull NetworkCallback networkCallback, + @NonNull Handler handler) { + CallbackHandler cbHandler = new CallbackHandler(handler); + sendRequestForNetwork(null /* NetworkCapabilities need */, networkCallback, 0, + TRACK_SYSTEM_DEFAULT, TYPE_NONE, cbHandler); + } + + /** + * Registers to receive notifications about the best matching network which satisfy the given + * {@link NetworkRequest}. The callbacks will continue to be called until + * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is + * called. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * {@link #registerNetworkCallback} and its variants and {@link #requestNetwork} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} that the system will call as suitable + * networks change state. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * @throws RuntimeException if the app already has too many callbacks registered. + */ + @SuppressLint("ExecutorRegistration") + public void registerBestMatchingNetworkCallback(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback, @NonNull Handler handler) { + final NetworkCapabilities nc = request.networkCapabilities; + final CallbackHandler cbHandler = new CallbackHandler(handler); + sendRequestForNetwork(nc, networkCallback, 0, LISTEN_FOR_BEST, TYPE_NONE, cbHandler); + } + + /** + * Requests bandwidth update for a given {@link Network} and returns whether the update request + * is accepted by ConnectivityService. Once accepted, ConnectivityService will poll underlying + * network connection for updated bandwidth information. The caller will be notified via + * {@link ConnectivityManager.NetworkCallback} if there is an update. Notice that this + * method assumes that the caller has previously called + * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} to listen for network + * changes. + * + * @param network {@link Network} specifying which network you're interested. + * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid. + */ + public boolean requestBandwidthUpdate(@NonNull Network network) { + try { + return mService.requestBandwidthUpdate(network); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregisters a {@code NetworkCallback} and possibly releases networks originating from + * {@link #requestNetwork(NetworkRequest, NetworkCallback)} and + * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} calls. + * If the given {@code NetworkCallback} had previously been used with + * {@code #requestNetwork}, any networks that had been connected to only to satisfy that request + * will be disconnected. + * + * Notifications that would have triggered that {@code NetworkCallback} will immediately stop + * triggering it as soon as this call returns. + * + * @param networkCallback The {@link NetworkCallback} used when making the request. + */ + public void unregisterNetworkCallback(@NonNull NetworkCallback networkCallback) { + printStackTrace(); + checkCallbackNotNull(networkCallback); + final List<NetworkRequest> reqs = new ArrayList<>(); + // Find all requests associated to this callback and stop callback triggers immediately. + // Callback is reusable immediately. http://b/20701525, http://b/35921499. + synchronized (sCallbacks) { + if (networkCallback.networkRequest == null) { + throw new IllegalArgumentException("NetworkCallback was not registered"); + } + if (networkCallback.networkRequest == ALREADY_UNREGISTERED) { + Log.d(TAG, "NetworkCallback was already unregistered"); + return; + } + for (Map.Entry<NetworkRequest, NetworkCallback> e : sCallbacks.entrySet()) { + if (e.getValue() == networkCallback) { + reqs.add(e.getKey()); + } + } + // TODO: throw exception if callback was registered more than once (http://b/20701525). + for (NetworkRequest r : reqs) { + try { + mService.releaseNetworkRequest(r); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + // Only remove mapping if rpc was successful. + sCallbacks.remove(r); + } + networkCallback.networkRequest = ALREADY_UNREGISTERED; + } + } + + /** + * Unregisters a callback previously registered via + * {@link #registerNetworkCallback(NetworkRequest, android.app.PendingIntent)}. + * + * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the + * PendingIntent passed to + * {@link #registerNetworkCallback(NetworkRequest, android.app.PendingIntent)}. + * Cannot be null. + */ + public void unregisterNetworkCallback(@NonNull PendingIntent operation) { + releaseNetworkRequest(operation); + } + + /** + * Informs the system whether it should switch to {@code network} regardless of whether it is + * validated or not. If {@code accept} is true, and the network was explicitly selected by the + * user (e.g., by selecting a Wi-Fi network in the Settings app), then the network will become + * the system default network regardless of any other network that's currently connected. If + * {@code always} is true, then the choice is remembered, so that the next time the user + * connects to this network, the system will switch to it. + * + * @param network The network to accept. + * @param accept Whether to accept the network even if unvalidated. + * @param always Whether to remember this choice in the future. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_SETUP_WIZARD, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) + public void setAcceptUnvalidated(@NonNull Network network, boolean accept, boolean always) { + try { + mService.setAcceptUnvalidated(network, accept, always); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Informs the system whether it should consider the network as validated even if it only has + * partial connectivity. If {@code accept} is true, then the network will be considered as + * validated even if connectivity is only partial. If {@code always} is true, then the choice + * is remembered, so that the next time the user connects to this network, the system will + * switch to it. + * + * @param network The network to accept. + * @param accept Whether to consider the network as validated even if it has partial + * connectivity. + * @param always Whether to remember this choice in the future. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_SETUP_WIZARD, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) + public void setAcceptPartialConnectivity(@NonNull Network network, boolean accept, + boolean always) { + try { + mService.setAcceptPartialConnectivity(network, accept, always); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Informs the system to penalize {@code network}'s score when it becomes unvalidated. This is + * only meaningful if the system is configured not to penalize such networks, e.g., if the + * {@code config_networkAvoidBadWifi} configuration variable is set to 0 and the {@code + * NETWORK_AVOID_BAD_WIFI setting is unset}. + * + * @param network The network to accept. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_SETUP_WIZARD, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) + public void setAvoidUnvalidated(@NonNull Network network) { + try { + mService.setAvoidUnvalidated(network); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Requests that the system open the captive portal app on the specified network. + * + * <p>This is to be used on networks where a captive portal was detected, as per + * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}. + * + * @param network The network to log into. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public void startCaptivePortalApp(@NonNull Network network) { + try { + mService.startCaptivePortalApp(network); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Requests that the system open the captive portal app with the specified extras. + * + * <p>This endpoint is exclusively for use by the NetworkStack and is protected by the + * corresponding permission. + * @param network Network on which the captive portal was detected. + * @param appExtras Extras to include in the app start intent. + * @hide + */ + @SystemApi + @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + public void startCaptivePortalApp(@NonNull Network network, @NonNull Bundle appExtras) { + try { + mService.startCaptivePortalAppInternal(network, appExtras); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Determine whether the device is configured to avoid bad wifi. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_STACK}) + public boolean shouldAvoidBadWifi() { + try { + return mService.shouldAvoidBadWifi(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * It is acceptable to briefly use multipath data to provide seamless connectivity for + * time-sensitive user-facing operations when the system default network is temporarily + * unresponsive. The amount of data should be limited (less than one megabyte for every call to + * this method), and the operation should be infrequent to ensure that data usage is limited. + * + * An example of such an operation might be a time-sensitive foreground activity, such as a + * voice command, that the user is performing while walking out of range of a Wi-Fi network. + */ + public static final int MULTIPATH_PREFERENCE_HANDOVER = 1 << 0; + + /** + * It is acceptable to use small amounts of multipath data on an ongoing basis to provide + * a backup channel for traffic that is primarily going over another network. + * + * An example might be maintaining backup connections to peers or servers for the purpose of + * fast fallback if the default network is temporarily unresponsive or disconnects. The traffic + * on backup paths should be negligible compared to the traffic on the main path. + */ + public static final int MULTIPATH_PREFERENCE_RELIABILITY = 1 << 1; + + /** + * It is acceptable to use metered data to improve network latency and performance. + */ + public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 1 << 2; + + /** + * Return value to use for unmetered networks. On such networks we currently set all the flags + * to true. + * @hide + */ + public static final int MULTIPATH_PREFERENCE_UNMETERED = + MULTIPATH_PREFERENCE_HANDOVER | + MULTIPATH_PREFERENCE_RELIABILITY | + MULTIPATH_PREFERENCE_PERFORMANCE; + + /** + * Provides a hint to the calling application on whether it is desirable to use the + * multinetwork APIs (e.g., {@link Network#openConnection}, {@link Network#bindSocket}, etc.) + * for multipath data transfer on this network when it is not the system default network. + * Applications desiring to use multipath network protocols should call this method before + * each such operation. + * + * @param network The network on which the application desires to use multipath data. + * If {@code null}, this method will return the a preference that will generally + * apply to metered networks. + * @return a bitwise OR of zero or more of the {@code MULTIPATH_PREFERENCE_*} constants. + */ + @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) + public @MultipathPreference int getMultipathPreference(@Nullable Network network) { + try { + return mService.getMultipathPreference(network); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Resets all connectivity manager settings back to factory defaults. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) + public void factoryReset() { + try { + mService.factoryReset(); + mTetheringManager.stopAllTethering(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Binds the current process to {@code network}. All Sockets created in the future + * (and not explicitly bound via a bound SocketFactory from + * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to + * {@code network}. All host name resolutions will be limited to {@code network} as well. + * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to + * work and all host name resolutions will fail. This is by design so an application doesn't + * accidentally use Sockets it thinks are still bound to a particular {@link Network}. + * To clear binding pass {@code null} for {@code network}. Using individually bound + * Sockets created by Network.getSocketFactory().createSocket() and + * performing network-specific host name resolutions via + * {@link Network#getAllByName Network.getAllByName} is preferred to calling + * {@code bindProcessToNetwork}. + * + * @param network The {@link Network} to bind the current process to, or {@code null} to clear + * the current binding. + * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid. + */ + public boolean bindProcessToNetwork(@Nullable Network network) { + // Forcing callers to call through non-static function ensures ConnectivityManager + // instantiated. + return setProcessDefaultNetwork(network); + } + + /** + * Binds the current process to {@code network}. All Sockets created in the future + * (and not explicitly bound via a bound SocketFactory from + * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to + * {@code network}. All host name resolutions will be limited to {@code network} as well. + * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to + * work and all host name resolutions will fail. This is by design so an application doesn't + * accidentally use Sockets it thinks are still bound to a particular {@link Network}. + * To clear binding pass {@code null} for {@code network}. Using individually bound + * Sockets created by Network.getSocketFactory().createSocket() and + * performing network-specific host name resolutions via + * {@link Network#getAllByName Network.getAllByName} is preferred to calling + * {@code setProcessDefaultNetwork}. + * + * @param network The {@link Network} to bind the current process to, or {@code null} to clear + * the current binding. + * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid. + * @deprecated This function can throw {@link IllegalStateException}. Use + * {@link #bindProcessToNetwork} instead. {@code bindProcessToNetwork} + * is a direct replacement. + */ + @Deprecated + public static boolean setProcessDefaultNetwork(@Nullable Network network) { + int netId = (network == null) ? NETID_UNSET : network.netId; + boolean isSameNetId = (netId == NetworkUtils.getBoundNetworkForProcess()); + + if (netId != NETID_UNSET) { + netId = network.getNetIdForResolv(); + } + + if (!NetworkUtils.bindProcessToNetwork(netId)) { + return false; + } + + if (!isSameNetId) { + // Set HTTP proxy system properties to match network. + // TODO: Deprecate this static method and replace it with a non-static version. + try { + Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy()); + } catch (SecurityException e) { + // The process doesn't have ACCESS_NETWORK_STATE, so we can't fetch the proxy. + Log.e(TAG, "Can't set proxy properties", e); + } + // Must flush DNS cache as new network may have different DNS resolutions. + InetAddressCompat.clearDnsCache(); + // Must flush socket pool as idle sockets will be bound to previous network and may + // cause subsequent fetches to be performed on old network. + NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged(); + } + + return true; + } + + /** + * Returns the {@link Network} currently bound to this process via + * {@link #bindProcessToNetwork}, or {@code null} if no {@link Network} is explicitly bound. + * + * @return {@code Network} to which this process is bound, or {@code null}. + */ + @Nullable + public Network getBoundNetworkForProcess() { + // Forcing callers to call thru non-static function ensures ConnectivityManager + // instantiated. + return getProcessDefaultNetwork(); + } + + /** + * Returns the {@link Network} currently bound to this process via + * {@link #bindProcessToNetwork}, or {@code null} if no {@link Network} is explicitly bound. + * + * @return {@code Network} to which this process is bound, or {@code null}. + * @deprecated Using this function can lead to other functions throwing + * {@link IllegalStateException}. Use {@link #getBoundNetworkForProcess} instead. + * {@code getBoundNetworkForProcess} is a direct replacement. + */ + @Deprecated + @Nullable + public static Network getProcessDefaultNetwork() { + int netId = NetworkUtils.getBoundNetworkForProcess(); + if (netId == NETID_UNSET) return null; + return new Network(netId); + } + + private void unsupportedStartingFrom(int version) { + if (Process.myUid() == Process.SYSTEM_UID) { + // The getApplicationInfo() call we make below is not supported in system context. Let + // the call through here, and rely on the fact that ConnectivityService will refuse to + // allow the system to use these APIs anyway. + return; + } + + if (mContext.getApplicationInfo().targetSdkVersion >= version) { + throw new UnsupportedOperationException( + "This method is not supported in target SDK version " + version + " and above"); + } + } + + // Checks whether the calling app can use the legacy routing API (startUsingNetworkFeature, + // stopUsingNetworkFeature, requestRouteToHost), and if not throw UnsupportedOperationException. + // TODO: convert the existing system users (Tethering, GnssLocationProvider) to the new APIs and + // remove these exemptions. Note that this check is not secure, and apps can still access these + // functions by accessing ConnectivityService directly. However, it should be clear that doing + // so is unsupported and may break in the future. http://b/22728205 + private void checkLegacyRoutingApiAccess() { + unsupportedStartingFrom(VERSION_CODES.M); + } + + /** + * Binds host resolutions performed by this process to {@code network}. + * {@link #bindProcessToNetwork} takes precedence over this setting. + * + * @param network The {@link Network} to bind host resolutions from the current process to, or + * {@code null} to clear the current binding. + * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid. + * @hide + * @deprecated This is strictly for legacy usage to support {@link #startUsingNetworkFeature}. + */ + @Deprecated + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static boolean setProcessDefaultNetworkForHostResolution(Network network) { + return NetworkUtils.bindProcessToNetworkForHostResolution( + (network == null) ? NETID_UNSET : network.getNetIdForResolv()); + } + + /** + * Device is not restricting metered network activity while application is running on + * background. + */ + public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1; + + /** + * Device is restricting metered network activity while application is running on background, + * but application is allowed to bypass it. + * <p> + * In this state, application should take action to mitigate metered network access. + * For example, a music streaming application should switch to a low-bandwidth bitrate. + */ + public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2; + + /** + * Device is restricting metered network activity while application is running on background. + * <p> + * In this state, application should not try to use the network while running on background, + * because it would be denied. + */ + public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3; + + /** + * A change in the background metered network activity restriction has occurred. + * <p> + * Applications should call {@link #getRestrictBackgroundStatus()} to check if the restriction + * applies to them. + * <p> + * This is only sent to registered receivers, not manifest receivers. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_RESTRICT_BACKGROUND_CHANGED = + "android.net.conn.RESTRICT_BACKGROUND_CHANGED"; + + /** + * Determines if the calling application is subject to metered network restrictions while + * running on background. + * + * @return {@link #RESTRICT_BACKGROUND_STATUS_DISABLED}, + * {@link #RESTRICT_BACKGROUND_STATUS_ENABLED}, + * or {@link #RESTRICT_BACKGROUND_STATUS_WHITELISTED} + */ + public @RestrictBackgroundStatus int getRestrictBackgroundStatus() { + try { + return mService.getRestrictBackgroundStatusByCaller(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * The network watchlist is a list of domains and IP addresses that are associated with + * potentially harmful apps. This method returns the SHA-256 of the watchlist config file + * currently used by the system for validation purposes. + * + * @return Hash of network watchlist config file. Null if config does not exist. + */ + @Nullable + public byte[] getNetworkWatchlistConfigHash() { + try { + return mService.getNetworkWatchlistConfigHash(); + } catch (RemoteException e) { + Log.e(TAG, "Unable to get watchlist config hash"); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the {@code uid} of the owner of a network connection. + * + * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and {@code + * IPPROTO_UDP} currently supported. + * @param local The local {@link InetSocketAddress} of a connection. + * @param remote The remote {@link InetSocketAddress} of a connection. + * @return {@code uid} if the connection is found and the app has permission to observe it + * (e.g., if it is associated with the calling VPN app's VpnService tunnel) or {@link + * android.os.Process#INVALID_UID} if the connection is not found. + * @throws {@link SecurityException} if the caller is not the active VpnService for the current + * user. + * @throws {@link IllegalArgumentException} if an unsupported protocol is requested. + */ + public int getConnectionOwnerUid( + int protocol, @NonNull InetSocketAddress local, @NonNull InetSocketAddress remote) { + ConnectionInfo connectionInfo = new ConnectionInfo(protocol, local, remote); + try { + return mService.getConnectionOwnerUid(connectionInfo); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private void printStackTrace() { + if (DEBUG) { + final StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); + final StringBuffer sb = new StringBuffer(); + for (int i = 3; i < callStack.length; i++) { + final String stackTrace = callStack[i].toString(); + if (stackTrace == null || stackTrace.contains("android.os")) { + break; + } + sb.append(" [").append(stackTrace).append("]"); + } + Log.d(TAG, "StackLog:" + sb.toString()); + } + } + + /** @hide */ + public TestNetworkManager startOrGetTestNetworkManager() { + final IBinder tnBinder; + try { + tnBinder = mService.startOrGetTestNetworkService(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + return new TestNetworkManager(ITestNetworkManager.Stub.asInterface(tnBinder)); + } + + /** @hide */ + public ConnectivityDiagnosticsManager createDiagnosticsManager() { + return new ConnectivityDiagnosticsManager(mContext, mService); + } + + /** + * Simulates a Data Stall for the specified Network. + * + * <p>This method should only be used for tests. + * + * <p>The caller must be the owner of the specified Network. This simulates a data stall to + * have the system behave as if it had happened, but does not actually stall connectivity. + * + * @param detectionMethod The detection method used to identify the Data Stall. + * See ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_*. + * @param timestampMillis The timestamp at which the stall 'occurred', in milliseconds, as per + * SystemClock.elapsedRealtime. + * @param network The Network for which a Data Stall is being simluated. + * @param extras The PersistableBundle of extras included in the Data Stall notification. + * @throws SecurityException if the caller is not the owner of the given network. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_TEST_NETWORKS, + android.Manifest.permission.NETWORK_STACK}) + public void simulateDataStall(@DetectionMethod int detectionMethod, long timestampMillis, + @NonNull Network network, @NonNull PersistableBundle extras) { + try { + mService.simulateDataStall(detectionMethod, timestampMillis, network, extras); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + @NonNull + private final List<QosCallbackConnection> mQosCallbackConnections = new ArrayList<>(); + + /** + * Registers a {@link QosSocketInfo} with an associated {@link QosCallback}. The callback will + * receive available QoS events related to the {@link Network} and local ip + port + * specified within socketInfo. + * <p/> + * The same {@link QosCallback} must be unregistered before being registered a second time, + * otherwise {@link QosCallbackRegistrationException} is thrown. + * <p/> + * This API does not, in itself, require any permission if called with a network that is not + * restricted. However, the underlying implementation currently only supports the IMS network, + * which is always restricted. That means non-preinstalled callers can't possibly find this API + * useful, because they'd never be called back on networks that they would have access to. + * + * @throws SecurityException if {@link QosSocketInfo#getNetwork()} is restricted and the app is + * missing CONNECTIVITY_USE_RESTRICTED_NETWORKS permission. + * @throws QosCallback.QosCallbackRegistrationException if qosCallback is already registered. + * @throws RuntimeException if the app already has too many callbacks registered. + * + * Exceptions after the time of registration is passed through + * {@link QosCallback#onError(QosCallbackException)}. see: {@link QosCallbackException}. + * + * @param socketInfo the socket information used to match QoS events + * @param executor The executor on which the callback will be invoked. The provided + * {@link Executor} must run callback sequentially, otherwise the order of + * callbacks cannot be guaranteed.onQosCallbackRegistered + * @param callback receives qos events that satisfy socketInfo + * + * @hide + */ + @SystemApi + public void registerQosCallback(@NonNull final QosSocketInfo socketInfo, + @CallbackExecutor @NonNull final Executor executor, + @NonNull final QosCallback callback) { + Objects.requireNonNull(socketInfo, "socketInfo must be non-null"); + Objects.requireNonNull(executor, "executor must be non-null"); + Objects.requireNonNull(callback, "callback must be non-null"); + + try { + synchronized (mQosCallbackConnections) { + if (getQosCallbackConnection(callback) == null) { + final QosCallbackConnection connection = + new QosCallbackConnection(this, callback, executor); + mQosCallbackConnections.add(connection); + mService.registerQosSocketCallback(socketInfo, connection); + } else { + Log.e(TAG, "registerQosCallback: Callback already registered"); + throw new QosCallbackRegistrationException(); + } + } + } catch (final RemoteException e) { + Log.e(TAG, "registerQosCallback: Error while registering ", e); + + // The same unregister method method is called for consistency even though nothing + // will be sent to the ConnectivityService since the callback was never successfully + // registered. + unregisterQosCallback(callback); + e.rethrowFromSystemServer(); + } catch (final ServiceSpecificException e) { + Log.e(TAG, "registerQosCallback: Error while registering ", e); + unregisterQosCallback(callback); + throw convertServiceException(e); + } + } + + /** + * Unregisters the given {@link QosCallback}. The {@link QosCallback} will no longer receive + * events once unregistered and can be registered a second time. + * <p/> + * If the {@link QosCallback} does not have an active registration, it is a no-op. + * + * @param callback the callback being unregistered + * + * @hide + */ + @SystemApi + public void unregisterQosCallback(@NonNull final QosCallback callback) { + Objects.requireNonNull(callback, "The callback must be non-null"); + try { + synchronized (mQosCallbackConnections) { + final QosCallbackConnection connection = getQosCallbackConnection(callback); + if (connection != null) { + connection.stopReceivingMessages(); + mService.unregisterQosCallback(connection); + mQosCallbackConnections.remove(connection); + } else { + Log.d(TAG, "unregisterQosCallback: Callback not registered"); + } + } + } catch (final RemoteException e) { + Log.e(TAG, "unregisterQosCallback: Error while unregistering ", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Gets the connection related to the callback. + * + * @param callback the callback to look up + * @return the related connection + */ + @Nullable + private QosCallbackConnection getQosCallbackConnection(final QosCallback callback) { + for (final QosCallbackConnection connection : mQosCallbackConnections) { + // Checking by reference here is intentional + if (connection.getCallback() == callback) { + return connection; + } + } + return null; + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}, but + * does not cause any networks to retain the NET_CAPABILITY_FOREGROUND capability. This can + * be used to request that the system provide a network without causing the network to be + * in the foreground. + * + * <p>This method will attempt to find the best network that matches the passed + * {@link NetworkRequest}, and to bring up one that does if none currently satisfies the + * criteria. The platform will evaluate which network is the best at its own discretion. + * Throughput, latency, cost per byte, policy, user preference and other considerations + * may be factored in the decision of what is considered the best network. + * + * <p>As long as this request is outstanding, the platform will try to maintain the best network + * matching this request, while always attempting to match the request to a better network if + * possible. If a better match is found, the platform will switch this request to the now-best + * network and inform the app of the newly best network by invoking + * {@link NetworkCallback#onAvailable(Network)} on the provided callback. Note that the platform + * will not try to maintain any other network than the best one currently matching the request: + * a network not matching any network request may be disconnected at any time. + * + * <p>For example, an application could use this method to obtain a connected cellular network + * even if the device currently has a data connection over Ethernet. This may cause the cellular + * radio to consume additional power. Or, an application could inform the system that it wants + * a network supporting sending MMSes and have the system let it know about the currently best + * MMS-supporting network through the provided {@link NetworkCallback}. + * + * <p>The status of the request can be followed by listening to the various callbacks described + * in {@link NetworkCallback}. The {@link Network} object passed to the callback methods can be + * used to direct traffic to the network (although accessing some networks may be subject to + * holding specific permissions). Callers will learn about the specific characteristics of the + * network through + * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)} and + * {@link NetworkCallback#onLinkPropertiesChanged(Network, LinkProperties)}. The methods of the + * provided {@link NetworkCallback} will only be invoked due to changes in the best network + * matching the request at any given time; therefore when a better network matching the request + * becomes available, the {@link NetworkCallback#onAvailable(Network)} method is called + * with the new network after which no further updates are given about the previously-best + * network, unless it becomes the best again at some later time. All callbacks are invoked + * in order on the same thread, which by default is a thread created by the framework running + * in the app. + * + * <p>This{@link NetworkRequest} will live until released via + * {@link #unregisterNetworkCallback(NetworkCallback)} or the calling application exits, at + * which point the system may let go of the network at any time. + * + * <p>It is presently unsupported to request a network with mutable + * {@link NetworkCapabilities} such as + * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or + * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL} + * as these {@code NetworkCapabilities} represent states that a particular + * network may never attain, and whether a network will attain these states + * is unknown prior to bringing up the network so the framework does not + * know how to go about satisfying a request with these capabilities. + * + * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the + * number of outstanding requests to 100 per app (identified by their UID), shared with + * all variants of this method, of {@link #registerNetworkCallback} as well as + * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}. + * Requesting a network with this method will count toward this limit. If this limit is + * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources, + * make sure to unregister the callbacks with + * {@link #unregisterNetworkCallback(NetworkCallback)}. + * + * @param request {@link NetworkRequest} describing this request. + * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note + * the callback must not be shared - it uniquely specifies this request. + * @param handler {@link Handler} to specify the thread upon which the callback will be invoked. + * If null, the callback is invoked on the default internal Handler. + * @throws IllegalArgumentException if {@code request} contains invalid network capabilities. + * @throws SecurityException if missing the appropriate permissions. + * @throws RuntimeException if the app already has too many callbacks registered. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @SuppressLint("ExecutorRegistration") + @RequiresPermission(anyOf = { + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK + }) + public void requestBackgroundNetwork(@NonNull NetworkRequest request, + @NonNull NetworkCallback networkCallback, + @SuppressLint("ListenerLast") @NonNull Handler handler) { + final NetworkCapabilities nc = request.networkCapabilities; + sendRequestForNetwork(nc, networkCallback, 0, BACKGROUND_REQUEST, + TYPE_NONE, new CallbackHandler(handler)); + } + + /** + * Used by automotive devices to set the network preferences used to direct traffic at an + * application level as per the given OemNetworkPreferences. An example use-case would be an + * automotive OEM wanting to provide connectivity for applications critical to the usage of a + * vehicle via a particular network. + * + * Calling this will overwrite the existing preference. + * + * @param preference {@link OemNetworkPreferences} The application network preference to be set. + * @param executor the executor on which listener will be invoked. + * @param listener {@link OnSetOemNetworkPreferenceListener} optional listener used to + * communicate completion of setOemNetworkPreference(). This will only be + * called once upon successful completion of setOemNetworkPreference(). + * @throws IllegalArgumentException if {@code preference} contains invalid preference values. + * @throws SecurityException if missing the appropriate permissions. + * @throws UnsupportedOperationException if called on a non-automotive device. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE) + public void setOemNetworkPreference(@NonNull final OemNetworkPreferences preference, + @Nullable @CallbackExecutor final Executor executor, + @Nullable final Runnable listener) { + Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null"); + if (null != listener) { + Objects.requireNonNull(executor, "Executor must be non-null"); + } + final IOnCompleteListener listenerInternal = listener == null ? null : + new IOnCompleteListener.Stub() { + @Override + public void onComplete() { + executor.execute(listener::run); + } + }; + + try { + mService.setOemNetworkPreference(preference, listenerInternal); + } catch (RemoteException e) { + Log.e(TAG, "setOemNetworkPreference() failed for preference: " + preference.toString()); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Request that a user profile is put by default on a network matching a given preference. + * + * See the documentation for the individual preferences for a description of the supported + * behaviors. + * + * @param profile the profile concerned. + * @param preference the preference for this profile. + * @param executor an executor to execute the listener on. Optional if listener is null. + * @param listener an optional listener to listen for completion of the operation. + * @throws IllegalArgumentException if {@code profile} is not a valid user profile. + * @throws SecurityException if missing the appropriate permissions. + * @hide + */ + // This function is for establishing per-profile default networking and can only be called by + // the device policy manager, running as the system server. It would make no sense to call it + // on a context for a user because it does not establish a setting on behalf of a user, rather + // it establishes a setting for a user on behalf of the DPM. + @SuppressLint({"UserHandle"}) + @SystemApi(client = MODULE_LIBRARIES) + @RequiresPermission(android.Manifest.permission.NETWORK_STACK) + public void setProfileNetworkPreference(@NonNull final UserHandle profile, + @ProfileNetworkPreference final int preference, + @Nullable @CallbackExecutor final Executor executor, + @Nullable final Runnable listener) { + if (null != listener) { + Objects.requireNonNull(executor, "Pass a non-null executor, or a null listener"); + } + final IOnCompleteListener proxy; + if (null == listener) { + proxy = null; + } else { + proxy = new IOnCompleteListener.Stub() { + @Override + public void onComplete() { + executor.execute(listener::run); + } + }; + } + try { + mService.setProfileNetworkPreference(profile, preference, proxy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + // The first network ID of IPSec tunnel interface. + private static final int TUN_INTF_NETID_START = 0xFC00; // 0xFC00 = 64512 + // The network ID range of IPSec tunnel interface. + private static final int TUN_INTF_NETID_RANGE = 0x0400; // 0x0400 = 1024 + + /** + * Get the network ID range reserved for IPSec tunnel interfaces. + * + * @return A Range which indicates the network ID range of IPSec tunnel interface. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @NonNull + public static Range<Integer> getIpSecNetIdRange() { + return new Range(TUN_INTF_NETID_START, TUN_INTF_NETID_START + TUN_INTF_NETID_RANGE - 1); + } +} diff --git a/framework/src/android/net/ConnectivityResources.java b/framework/src/android/net/ConnectivityResources.java new file mode 100644 index 0000000000000000000000000000000000000000..18f0de0cc9a17a5e2bbc53727de9b912848be55d --- /dev/null +++ b/framework/src/android/net/ConnectivityResources.java @@ -0,0 +1,108 @@ +/* + * 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 static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; + +/** + * Utility to obtain the {@link com.android.server.ConnectivityService} {@link Resources}, in the + * ServiceConnectivityResources APK. + * @hide + */ +public class ConnectivityResources { + private static final String RESOURCES_APK_INTENT = + "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK"; + private static final String RES_PKG_DIR = "/apex/com.android.tethering/"; + + @NonNull + private final Context mContext; + + @Nullable + private Context mResourcesContext = null; + + @Nullable + private static Context sTestResourcesContext = null; + + public ConnectivityResources(Context context) { + mContext = context; + } + + /** + * Convenience method to mock all resources for the duration of a test. + * + * Call with a null context to reset after the test. + */ + @VisibleForTesting + public static void setResourcesContextForTest(@Nullable Context testContext) { + sTestResourcesContext = testContext; + } + + /** + * Get the {@link Context} of the resources package. + */ + public synchronized Context getResourcesContext() { + if (sTestResourcesContext != null) { + return sTestResourcesContext; + } + + if (mResourcesContext != null) { + return mResourcesContext; + } + + final List<ResolveInfo> pkgs = mContext.getPackageManager() + .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY); + pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(RES_PKG_DIR)); + if (pkgs.size() > 1) { + Log.wtf(ConnectivityResources.class.getSimpleName(), + "More than one package found: " + pkgs); + } + if (pkgs.isEmpty()) { + throw new IllegalStateException("No connectivity resource package found"); + } + + final Context pkgContext; + try { + pkgContext = mContext.createPackageContext( + pkgs.get(0).activityInfo.applicationInfo.packageName, 0 /* flags */); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException("Resolved package not found", e); + } + + mResourcesContext = pkgContext; + return pkgContext; + } + + /** + * Get the {@link Resources} of the ServiceConnectivityResources APK. + */ + public Resources get() { + return getResourcesContext().getResources(); + } +} diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java new file mode 100644 index 0000000000000000000000000000000000000000..1a690991cade56d6fbe28376f2e56a10f0ff42d4 --- /dev/null +++ b/framework/src/android/net/ConnectivitySettingsManager.java @@ -0,0 +1,1087 @@ +/* + * 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 static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER; +import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE; +import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.ContentResolver; +import android.content.Context; +import android.net.ConnectivityAnnotations.MultipathPreference; +import android.os.Process; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Range; + +import com.android.net.module.util.ProxyUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +/** + * A manager class for connectivity module settings. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class ConnectivitySettingsManager { + + private ConnectivitySettingsManager() {} + + /** Data activity timeout settings */ + + /** + * Inactivity timeout to track mobile data activity. + * + * If set to a positive integer, it indicates the inactivity timeout value in seconds to + * infer the data activity of mobile network. After a period of no activity on mobile + * networks with length specified by the timeout, an {@code ACTION_DATA_ACTIVITY_CHANGE} + * intent is fired to indicate a transition of network status from "active" to "idle". Any + * subsequent activity on mobile networks triggers the firing of {@code + * ACTION_DATA_ACTIVITY_CHANGE} intent indicating transition from "idle" to "active". + * + * Network activity refers to transmitting or receiving data on the network interfaces. + * + * Tracking is disabled if set to zero or negative value. + * + * @hide + */ + public static final String DATA_ACTIVITY_TIMEOUT_MOBILE = "data_activity_timeout_mobile"; + + /** + * Timeout to tracking Wifi data activity. Same as {@code DATA_ACTIVITY_TIMEOUT_MOBILE} + * but for Wifi network. + * + * @hide + */ + public static final String DATA_ACTIVITY_TIMEOUT_WIFI = "data_activity_timeout_wifi"; + + /** Dns resolver settings */ + + /** + * Sample validity in seconds to configure for the system DNS resolver. + * + * @hide + */ + public static final String DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS = + "dns_resolver_sample_validity_seconds"; + + /** + * Success threshold in percent for use with the system DNS resolver. + * + * @hide + */ + public static final String DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT = + "dns_resolver_success_threshold_percent"; + + /** + * Minimum number of samples needed for statistics to be considered meaningful in the + * system DNS resolver. + * + * @hide + */ + public static final String DNS_RESOLVER_MIN_SAMPLES = "dns_resolver_min_samples"; + + /** + * Maximum number taken into account for statistics purposes in the system DNS resolver. + * + * @hide + */ + public static final String DNS_RESOLVER_MAX_SAMPLES = "dns_resolver_max_samples"; + + private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8; + private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64; + + /** Network switch notification settings */ + + /** + * The maximum number of notifications shown in 24 hours when switching networks. + * + * @hide + */ + public static final String NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT = + "network_switch_notification_daily_limit"; + + /** + * The minimum time in milliseconds between notifications when switching networks. + * + * @hide + */ + public static final String NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS = + "network_switch_notification_rate_limit_millis"; + + /** Captive portal settings */ + + /** + * The URL used for HTTP captive portal detection upon a new connection. + * A 204 response code from the server is used for validation. + * + * @hide + */ + public static final String CAPTIVE_PORTAL_HTTP_URL = "captive_portal_http_url"; + + /** + * What to do when connecting a network that presents a captive portal. + * Must be one of the CAPTIVE_PORTAL_MODE_* constants below. + * + * The default for this setting is CAPTIVE_PORTAL_MODE_PROMPT. + * + * @hide + */ + public static final String CAPTIVE_PORTAL_MODE = "captive_portal_mode"; + + /** + * Don't attempt to detect captive portals. + */ + public static final int CAPTIVE_PORTAL_MODE_IGNORE = 0; + + /** + * When detecting a captive portal, display a notification that + * prompts the user to sign in. + */ + public static final int CAPTIVE_PORTAL_MODE_PROMPT = 1; + + /** + * When detecting a captive portal, immediately disconnect from the + * network and do not reconnect to that network in the future. + */ + public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + CAPTIVE_PORTAL_MODE_IGNORE, + CAPTIVE_PORTAL_MODE_PROMPT, + CAPTIVE_PORTAL_MODE_AVOID, + }) + public @interface CaptivePortalMode {} + + /** Global http proxy settings */ + + /** + * Host name for global http proxy. Set via ConnectivityManager. + * + * @hide + */ + public static final String GLOBAL_HTTP_PROXY_HOST = "global_http_proxy_host"; + + /** + * Integer host port for global http proxy. Set via ConnectivityManager. + * + * @hide + */ + public static final String GLOBAL_HTTP_PROXY_PORT = "global_http_proxy_port"; + + /** + * Exclusion list for global proxy. This string contains a list of + * comma-separated domains where the global proxy does not apply. + * Domains should be listed in a comma- separated list. Example of + * acceptable formats: ".domain1.com,my.domain2.com" Use + * ConnectivityManager to set/get. + * + * @hide + */ + public static final String GLOBAL_HTTP_PROXY_EXCLUSION_LIST = + "global_http_proxy_exclusion_list"; + + /** + * The location PAC File for the proxy. + * + * @hide + */ + public static final String GLOBAL_HTTP_PROXY_PAC = "global_proxy_pac_url"; + + /** Private dns settings */ + + /** + * The requested Private DNS mode (string), and an accompanying specifier (string). + * + * Currently, the specifier holds the chosen provider name when the mode requests + * a specific provider. It may be used to store the provider name even when the + * mode changes so that temporarily disabling and re-enabling the specific + * provider mode does not necessitate retyping the provider hostname. + * + * @hide + */ + public static final String PRIVATE_DNS_MODE = "private_dns_mode"; + + /** + * The specific Private DNS provider name. + * + * @hide + */ + public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier"; + + /** + * Forced override of the default mode (hardcoded as "automatic", nee "opportunistic"). + * This allows changing the default mode without effectively disabling other modes, + * all of which require explicit user action to enable/configure. See also b/79719289. + * + * Value is a string, suitable for assignment to PRIVATE_DNS_MODE above. + * + * @hide + */ + public static final String PRIVATE_DNS_DEFAULT_MODE = "private_dns_default_mode"; + + /** Other settings */ + + /** + * The number of milliseconds to hold on to a PendingIntent based request. This delay gives + * the receivers of the PendingIntent an opportunity to make a new network request before + * the Network satisfying the request is potentially removed. + * + * @hide + */ + public static final String CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS = + "connectivity_release_pending_intent_delay_ms"; + + /** + * Whether the mobile data connection should remain active even when higher + * priority networks like WiFi are active, to help make network switching faster. + * + * See ConnectivityService for more info. + * + * (0 = disabled, 1 = enabled) + * + * @hide + */ + public static final String MOBILE_DATA_ALWAYS_ON = "mobile_data_always_on"; + + /** + * Whether the wifi data connection should remain active even when higher + * priority networks like Ethernet are active, to keep both networks. + * In the case where higher priority networks are connected, wifi will be + * unused unless an application explicitly requests to use it. + * + * See ConnectivityService for more info. + * + * (0 = disabled, 1 = enabled) + * + * @hide + */ + public static final String WIFI_ALWAYS_REQUESTED = "wifi_always_requested"; + + /** + * Whether to automatically switch away from wifi networks that lose Internet access. + * Only meaningful if config_networkAvoidBadWifi is set to 0, otherwise the system always + * avoids such networks. Valid values are: + * + * 0: Don't avoid bad wifi, don't prompt the user. Get stuck on bad wifi like it's 2013. + * null: Ask the user whether to switch away from bad wifi. + * 1: Avoid bad wifi. + * + * @hide + */ + public static final String NETWORK_AVOID_BAD_WIFI = "network_avoid_bad_wifi"; + + /** + * Don't avoid bad wifi, don't prompt the user. Get stuck on bad wifi like it's 2013. + */ + public static final int NETWORK_AVOID_BAD_WIFI_IGNORE = 0; + + /** + * Ask the user whether to switch away from bad wifi. + */ + public static final int NETWORK_AVOID_BAD_WIFI_PROMPT = 1; + + /** + * Avoid bad wifi. + */ + public static final int NETWORK_AVOID_BAD_WIFI_AVOID = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + NETWORK_AVOID_BAD_WIFI_IGNORE, + NETWORK_AVOID_BAD_WIFI_PROMPT, + NETWORK_AVOID_BAD_WIFI_AVOID, + }) + public @interface NetworkAvoidBadWifi {} + + /** + * User setting for ConnectivityManager.getMeteredMultipathPreference(). This value may be + * overridden by the system based on device or application state. If null, the value + * specified by config_networkMeteredMultipathPreference is used. + * + * @hide + */ + public static final String NETWORK_METERED_MULTIPATH_PREFERENCE = + "network_metered_multipath_preference"; + + /** + * A list of uids that should go on cellular networks in preference even when higher-priority + * networks are connected. + * + * @hide + */ + public static final String MOBILE_DATA_PREFERRED_UIDS = "mobile_data_preferred_uids"; + + /** + * One of the private DNS modes that indicates the private DNS mode is off. + */ + public static final int PRIVATE_DNS_MODE_OFF = 1; + + /** + * One of the private DNS modes that indicates the private DNS mode is automatic, which + * will try to use the current DNS as private DNS. + */ + public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; + + /** + * One of the private DNS modes that indicates the private DNS mode is strict and the + * {@link #PRIVATE_DNS_SPECIFIER} is required, which will try to use the value of + * {@link #PRIVATE_DNS_SPECIFIER} as private DNS. + */ + public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + PRIVATE_DNS_MODE_OFF, + PRIVATE_DNS_MODE_OPPORTUNISTIC, + PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, + }) + public @interface PrivateDnsMode {} + + private static final String PRIVATE_DNS_MODE_OFF_STRING = "off"; + private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC_STRING = "opportunistic"; + private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING = "hostname"; + + /** + * A list of apps that is allowed on restricted networks. + * + * @hide + */ + public static final String APPS_ALLOWED_ON_RESTRICTED_NETWORKS = + "apps_allowed_on_restricted_networks"; + + /** + * Get mobile data activity timeout from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @param def The default timeout if no setting value. + * @return The {@link Duration} of timeout to track mobile data activity. + */ + @NonNull + public static Duration getMobileDataActivityTimeout(@NonNull Context context, + @NonNull Duration def) { + final int timeout = Settings.Global.getInt( + context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_MOBILE, (int) def.getSeconds()); + return Duration.ofSeconds(timeout); + } + + /** + * Set mobile data activity timeout to {@link Settings}. + * Tracking is disabled if set to zero or negative value. + * + * Note: Only use the number of seconds in this duration, lower second(nanoseconds) will be + * ignored. + * + * @param context The {@link Context} to set the setting. + * @param timeout The mobile data activity timeout. + */ + public static void setMobileDataActivityTimeout(@NonNull Context context, + @NonNull Duration timeout) { + Settings.Global.putInt(context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_MOBILE, + (int) timeout.getSeconds()); + } + + /** + * Get wifi data activity timeout from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @param def The default timeout if no setting value. + * @return The {@link Duration} of timeout to track wifi data activity. + */ + @NonNull + public static Duration getWifiDataActivityTimeout(@NonNull Context context, + @NonNull Duration def) { + final int timeout = Settings.Global.getInt( + context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_WIFI, (int) def.getSeconds()); + return Duration.ofSeconds(timeout); + } + + /** + * Set wifi data activity timeout to {@link Settings}. + * Tracking is disabled if set to zero or negative value. + * + * Note: Only use the number of seconds in this duration, lower second(nanoseconds) will be + * ignored. + * + * @param context The {@link Context} to set the setting. + * @param timeout The wifi data activity timeout. + */ + public static void setWifiDataActivityTimeout(@NonNull Context context, + @NonNull Duration timeout) { + Settings.Global.putInt(context.getContentResolver(), DATA_ACTIVITY_TIMEOUT_WIFI, + (int) timeout.getSeconds()); + } + + /** + * Get dns resolver sample validity duration from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @param def The default duration if no setting value. + * @return The {@link Duration} of sample validity duration to configure for the system DNS + * resolver. + */ + @NonNull + public static Duration getDnsResolverSampleValidityDuration(@NonNull Context context, + @NonNull Duration def) { + final int duration = Settings.Global.getInt(context.getContentResolver(), + DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS, (int) def.getSeconds()); + return Duration.ofSeconds(duration); + } + + /** + * Set dns resolver sample validity duration to {@link Settings}. The duration must be a + * positive number of seconds. + * + * @param context The {@link Context} to set the setting. + * @param duration The sample validity duration. + */ + public static void setDnsResolverSampleValidityDuration(@NonNull Context context, + @NonNull Duration duration) { + final int time = (int) duration.getSeconds(); + if (time <= 0) { + throw new IllegalArgumentException("Invalid duration"); + } + Settings.Global.putInt( + context.getContentResolver(), DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS, time); + } + + /** + * Get dns resolver success threshold percent from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @param def The default value if no setting value. + * @return The success threshold in percent for use with the system DNS resolver. + */ + public static int getDnsResolverSuccessThresholdPercent(@NonNull Context context, int def) { + return Settings.Global.getInt( + context.getContentResolver(), DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT, def); + } + + /** + * Set dns resolver success threshold percent to {@link Settings}. The threshold percent must + * be 0~100. + * + * @param context The {@link Context} to set the setting. + * @param percent The success threshold percent. + */ + public static void setDnsResolverSuccessThresholdPercent(@NonNull Context context, + @IntRange(from = 0, to = 100) int percent) { + if (percent < 0 || percent > 100) { + throw new IllegalArgumentException("Percent must be 0~100"); + } + Settings.Global.putInt( + context.getContentResolver(), DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT, percent); + } + + /** + * Get dns resolver samples range from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @return The {@link Range<Integer>} of samples needed for statistics to be considered + * meaningful in the system DNS resolver. + */ + @NonNull + public static Range<Integer> getDnsResolverSampleRanges(@NonNull Context context) { + final int minSamples = Settings.Global.getInt(context.getContentResolver(), + DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES); + final int maxSamples = Settings.Global.getInt(context.getContentResolver(), + DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES); + return new Range<>(minSamples, maxSamples); + } + + /** + * Set dns resolver samples range to {@link Settings}. + * + * @param context The {@link Context} to set the setting. + * @param range The samples range. The minimum number should be more than 0 and the maximum + * number should be less that 64. + */ + public static void setDnsResolverSampleRanges(@NonNull Context context, + @NonNull Range<Integer> range) { + if (range.getLower() < 0 || range.getUpper() > 64) { + throw new IllegalArgumentException("Argument must be 0~64"); + } + Settings.Global.putInt( + context.getContentResolver(), DNS_RESOLVER_MIN_SAMPLES, range.getLower()); + Settings.Global.putInt( + context.getContentResolver(), DNS_RESOLVER_MAX_SAMPLES, range.getUpper()); + } + + /** + * Get maximum count (from {@link Settings}) of switching network notifications shown in 24 + * hours. + * + * @param context The {@link Context} to query the setting. + * @param def The default value if no setting value. + * @return The maximum count of notifications shown in 24 hours when switching networks. + */ + public static int getNetworkSwitchNotificationMaximumDailyCount(@NonNull Context context, + int def) { + return Settings.Global.getInt( + context.getContentResolver(), NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, def); + } + + /** + * Set maximum count (to {@link Settings}) of switching network notifications shown in 24 hours. + * The count must be at least 0. + * + * @param context The {@link Context} to set the setting. + * @param count The maximum count of switching network notifications shown in 24 hours. + */ + public static void setNetworkSwitchNotificationMaximumDailyCount(@NonNull Context context, + @IntRange(from = 0) int count) { + if (count < 0) { + throw new IllegalArgumentException("Count must be 0~10."); + } + Settings.Global.putInt( + context.getContentResolver(), NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, count); + } + + /** + * Get minimum duration (from {@link Settings}) between each switching network notifications. + * + * @param context The {@link Context} to query the setting. + * @param def The default time if no setting value. + * @return The minimum duration between notifications when switching networks. + */ + @NonNull + public static Duration getNetworkSwitchNotificationRateDuration(@NonNull Context context, + @NonNull Duration def) { + final int duration = Settings.Global.getInt(context.getContentResolver(), + NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, (int) def.toMillis()); + return Duration.ofMillis(duration); + } + + /** + * Set minimum duration (to {@link Settings}) between each switching network notifications. + * + * @param context The {@link Context} to set the setting. + * @param duration The minimum duration between notifications when switching networks. + */ + public static void setNetworkSwitchNotificationRateDuration(@NonNull Context context, + @NonNull Duration duration) { + final int time = (int) duration.toMillis(); + if (time < 0) { + throw new IllegalArgumentException("Invalid duration."); + } + Settings.Global.putInt(context.getContentResolver(), + NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, time); + } + + /** + * Get URL (from {@link Settings}) used for HTTP captive portal detection upon a new connection. + * + * @param context The {@link Context} to query the setting. + * @return The URL used for HTTP captive portal detection upon a new connection. + */ + @Nullable + public static String getCaptivePortalHttpUrl(@NonNull Context context) { + return Settings.Global.getString(context.getContentResolver(), CAPTIVE_PORTAL_HTTP_URL); + } + + /** + * Set URL (to {@link Settings}) used for HTTP captive portal detection upon a new connection. + * This URL should respond with a 204 response to a GET request to indicate no captive portal is + * present. And this URL must be HTTP as redirect responses are used to find captive portal + * sign-in pages. If the URL set to null or be incorrect, it will result in captive portal + * detection failed and lost the connection. + * + * @param context The {@link Context} to set the setting. + * @param url The URL used for HTTP captive portal detection upon a new connection. + */ + public static void setCaptivePortalHttpUrl(@NonNull Context context, @Nullable String url) { + Settings.Global.putString(context.getContentResolver(), CAPTIVE_PORTAL_HTTP_URL, url); + } + + /** + * Get mode (from {@link Settings}) when connecting a network that presents a captive portal. + * + * @param context The {@link Context} to query the setting. + * @param def The default mode if no setting value. + * @return The mode when connecting a network that presents a captive portal. + */ + @CaptivePortalMode + public static int getCaptivePortalMode(@NonNull Context context, + @CaptivePortalMode int def) { + return Settings.Global.getInt(context.getContentResolver(), CAPTIVE_PORTAL_MODE, def); + } + + /** + * Set mode (to {@link Settings}) when connecting a network that presents a captive portal. + * + * @param context The {@link Context} to set the setting. + * @param mode The mode when connecting a network that presents a captive portal. + */ + public static void setCaptivePortalMode(@NonNull Context context, @CaptivePortalMode int mode) { + if (!(mode == CAPTIVE_PORTAL_MODE_IGNORE + || mode == CAPTIVE_PORTAL_MODE_PROMPT + || mode == CAPTIVE_PORTAL_MODE_AVOID)) { + throw new IllegalArgumentException("Invalid captive portal mode"); + } + Settings.Global.putInt(context.getContentResolver(), CAPTIVE_PORTAL_MODE, mode); + } + + /** + * Get the global HTTP proxy applied to the device, or null if none. + * + * @param context The {@link Context} to query the setting. + * @return The {@link ProxyInfo} which build from global http proxy settings. + */ + @Nullable + public static ProxyInfo getGlobalProxy(@NonNull Context context) { + final String host = Settings.Global.getString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST); + final int port = Settings.Global.getInt( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, 0 /* def */); + final String exclusionList = Settings.Global.getString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST); + final String pacFileUrl = Settings.Global.getString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC); + + if (TextUtils.isEmpty(host) && TextUtils.isEmpty(pacFileUrl)) { + return null; // No global proxy. + } + + if (TextUtils.isEmpty(pacFileUrl)) { + return ProxyInfo.buildDirectProxy( + host, port, ProxyUtils.exclusionStringAsList(exclusionList)); + } else { + return ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl)); + } + } + + /** + * Set global http proxy settings from given {@link ProxyInfo}. + * + * @param context The {@link Context} to set the setting. + * @param proxyInfo The {@link ProxyInfo} for global http proxy settings which build from + * {@link ProxyInfo#buildPacProxy(Uri)} or + * {@link ProxyInfo#buildDirectProxy(String, int, List)} + */ + public static void setGlobalProxy(@NonNull Context context, @NonNull ProxyInfo proxyInfo) { + final String host = proxyInfo.getHost(); + final int port = proxyInfo.getPort(); + final String exclusionList = proxyInfo.getExclusionListAsString(); + final String pacFileUrl = proxyInfo.getPacFileUrl().toString(); + + if (TextUtils.isEmpty(pacFileUrl)) { + Settings.Global.putString(context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, host); + Settings.Global.putInt(context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, port); + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclusionList); + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC, "" /* value */); + } else { + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC, pacFileUrl); + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, "" /* value */); + Settings.Global.putInt( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, 0 /* value */); + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST, "" /* value */); + } + } + + /** + * Clear all global http proxy settings. + * + * @param context The {@link Context} to set the setting. + */ + public static void clearGlobalProxy(@NonNull Context context) { + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_HOST, "" /* value */); + Settings.Global.putInt( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PORT, 0 /* value */); + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_EXCLUSION_LIST, "" /* value */); + Settings.Global.putString( + context.getContentResolver(), GLOBAL_HTTP_PROXY_PAC, "" /* value */); + } + + private static String getPrivateDnsModeAsString(@PrivateDnsMode int mode) { + switch (mode) { + case PRIVATE_DNS_MODE_OFF: + return PRIVATE_DNS_MODE_OFF_STRING; + case PRIVATE_DNS_MODE_OPPORTUNISTIC: + return PRIVATE_DNS_MODE_OPPORTUNISTIC_STRING; + case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME: + return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING; + default: + throw new IllegalArgumentException("Invalid private dns mode: " + mode); + } + } + + private static int getPrivateDnsModeAsInt(String mode) { + switch (mode) { + case "off": + return PRIVATE_DNS_MODE_OFF; + case "hostname": + return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; + case "opportunistic": + return PRIVATE_DNS_MODE_OPPORTUNISTIC; + default: + throw new IllegalArgumentException("Invalid private dns mode: " + mode); + } + } + + /** + * Get private DNS mode from settings. + * + * @param context The Context to query the private DNS mode from settings. + * @return A string of private DNS mode. + */ + @PrivateDnsMode + public static int getPrivateDnsMode(@NonNull Context context) { + final ContentResolver cr = context.getContentResolver(); + String mode = Settings.Global.getString(cr, PRIVATE_DNS_MODE); + if (TextUtils.isEmpty(mode)) mode = Settings.Global.getString(cr, PRIVATE_DNS_DEFAULT_MODE); + // If both PRIVATE_DNS_MODE and PRIVATE_DNS_DEFAULT_MODE are not set, choose + // PRIVATE_DNS_MODE_OPPORTUNISTIC as default mode. + if (TextUtils.isEmpty(mode)) return PRIVATE_DNS_MODE_OPPORTUNISTIC; + return getPrivateDnsModeAsInt(mode); + } + + /** + * Set private DNS mode to settings. + * + * @param context The {@link Context} to set the private DNS mode. + * @param mode The private dns mode. This should be one of the PRIVATE_DNS_MODE_* constants. + */ + public static void setPrivateDnsMode(@NonNull Context context, @PrivateDnsMode int mode) { + if (!(mode == PRIVATE_DNS_MODE_OFF + || mode == PRIVATE_DNS_MODE_OPPORTUNISTIC + || mode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) { + throw new IllegalArgumentException("Invalid private dns mode: " + mode); + } + Settings.Global.putString(context.getContentResolver(), PRIVATE_DNS_MODE, + getPrivateDnsModeAsString(mode)); + } + + /** + * Get specific private dns provider name from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @return The specific private dns provider name, or null if no setting value. + */ + @Nullable + public static String getPrivateDnsHostname(@NonNull Context context) { + return Settings.Global.getString(context.getContentResolver(), PRIVATE_DNS_SPECIFIER); + } + + /** + * Set specific private dns provider name to {@link Settings}. + * + * @param context The {@link Context} to set the setting. + * @param specifier The specific private dns provider name. + */ + public static void setPrivateDnsHostname(@NonNull Context context, + @Nullable String specifier) { + Settings.Global.putString(context.getContentResolver(), PRIVATE_DNS_SPECIFIER, specifier); + } + + /** + * Get default private dns mode from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @return The default private dns mode. + */ + @PrivateDnsMode + @NonNull + public static String getPrivateDnsDefaultMode(@NonNull Context context) { + return Settings.Global.getString(context.getContentResolver(), PRIVATE_DNS_DEFAULT_MODE); + } + + /** + * Set default private dns mode to {@link Settings}. + * + * @param context The {@link Context} to set the setting. + * @param mode The default private dns mode. This should be one of the PRIVATE_DNS_MODE_* + * constants. + */ + public static void setPrivateDnsDefaultMode(@NonNull Context context, + @NonNull @PrivateDnsMode int mode) { + if (!(mode == PRIVATE_DNS_MODE_OFF + || mode == PRIVATE_DNS_MODE_OPPORTUNISTIC + || mode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) { + throw new IllegalArgumentException("Invalid private dns mode"); + } + Settings.Global.putString(context.getContentResolver(), PRIVATE_DNS_DEFAULT_MODE, + getPrivateDnsModeAsString(mode)); + } + + /** + * Get duration (from {@link Settings}) to keep a PendingIntent-based request. + * + * @param context The {@link Context} to query the setting. + * @param def The default duration if no setting value. + * @return The duration to keep a PendingIntent-based request. + */ + @NonNull + public static Duration getConnectivityKeepPendingIntentDuration(@NonNull Context context, + @NonNull Duration def) { + final int duration = Settings.Secure.getInt(context.getContentResolver(), + CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, (int) def.toMillis()); + return Duration.ofMillis(duration); + } + + /** + * Set duration (to {@link Settings}) to keep a PendingIntent-based request. + * + * @param context The {@link Context} to set the setting. + * @param duration The duration to keep a PendingIntent-based request. + */ + public static void setConnectivityKeepPendingIntentDuration(@NonNull Context context, + @NonNull Duration duration) { + final int time = (int) duration.toMillis(); + if (time < 0) { + throw new IllegalArgumentException("Invalid duration."); + } + Settings.Secure.putInt( + context.getContentResolver(), CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, time); + } + + /** + * Read from {@link Settings} whether the mobile data connection should remain active + * even when higher priority networks are active. + * + * @param context The {@link Context} to query the setting. + * @param def The default value if no setting value. + * @return Whether the mobile data connection should remain active even when higher + * priority networks are active. + */ + public static boolean getMobileDataAlwaysOn(@NonNull Context context, boolean def) { + final int enable = Settings.Global.getInt( + context.getContentResolver(), MOBILE_DATA_ALWAYS_ON, (def ? 1 : 0)); + return (enable != 0) ? true : false; + } + + /** + * Write into {@link Settings} whether the mobile data connection should remain active + * even when higher priority networks are active. + * + * @param context The {@link Context} to set the setting. + * @param enable Whether the mobile data connection should remain active even when higher + * priority networks are active. + */ + public static void setMobileDataAlwaysOn(@NonNull Context context, boolean enable) { + Settings.Global.putInt( + context.getContentResolver(), MOBILE_DATA_ALWAYS_ON, (enable ? 1 : 0)); + } + + /** + * Read from {@link Settings} whether the wifi data connection should remain active + * even when higher priority networks are active. + * + * @param context The {@link Context} to query the setting. + * @param def The default value if no setting value. + * @return Whether the wifi data connection should remain active even when higher + * priority networks are active. + */ + public static boolean getWifiAlwaysRequested(@NonNull Context context, boolean def) { + final int enable = Settings.Global.getInt( + context.getContentResolver(), WIFI_ALWAYS_REQUESTED, (def ? 1 : 0)); + return (enable != 0) ? true : false; + } + + /** + * Write into {@link Settings} whether the wifi data connection should remain active + * even when higher priority networks are active. + * + * @param context The {@link Context} to set the setting. + * @param enable Whether the wifi data connection should remain active even when higher + * priority networks are active + */ + public static void setWifiAlwaysRequested(@NonNull Context context, boolean enable) { + Settings.Global.putInt( + context.getContentResolver(), WIFI_ALWAYS_REQUESTED, (enable ? 1 : 0)); + } + + /** + * Get avoid bad wifi setting from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @return The setting whether to automatically switch away from wifi networks that lose + * internet access. + */ + @NetworkAvoidBadWifi + public static int getNetworkAvoidBadWifi(@NonNull Context context) { + final String setting = + Settings.Global.getString(context.getContentResolver(), NETWORK_AVOID_BAD_WIFI); + if ("0".equals(setting)) { + return NETWORK_AVOID_BAD_WIFI_IGNORE; + } else if ("1".equals(setting)) { + return NETWORK_AVOID_BAD_WIFI_AVOID; + } else { + return NETWORK_AVOID_BAD_WIFI_PROMPT; + } + } + + /** + * Set avoid bad wifi setting to {@link Settings}. + * + * @param context The {@link Context} to set the setting. + * @param value Whether to automatically switch away from wifi networks that lose internet + * access. + */ + public static void setNetworkAvoidBadWifi(@NonNull Context context, + @NetworkAvoidBadWifi int value) { + final String setting; + if (value == NETWORK_AVOID_BAD_WIFI_IGNORE) { + setting = "0"; + } else if (value == NETWORK_AVOID_BAD_WIFI_AVOID) { + setting = "1"; + } else if (value == NETWORK_AVOID_BAD_WIFI_PROMPT) { + setting = null; + } else { + throw new IllegalArgumentException("Invalid avoid bad wifi setting"); + } + Settings.Global.putString(context.getContentResolver(), NETWORK_AVOID_BAD_WIFI, setting); + } + + /** + * Get network metered multipath preference from {@link Settings}. + * + * @param context The {@link Context} to query the setting. + * @return The network metered multipath preference which should be one of + * ConnectivityManager#MULTIPATH_PREFERENCE_* value or null if the value specified + * by config_networkMeteredMultipathPreference is used. + */ + @Nullable + public static String getNetworkMeteredMultipathPreference(@NonNull Context context) { + return Settings.Global.getString( + context.getContentResolver(), NETWORK_METERED_MULTIPATH_PREFERENCE); + } + + /** + * Set network metered multipath preference to {@link Settings}. + * + * @param context The {@link Context} to set the setting. + * @param preference The network metered multipath preference which should be one of + * ConnectivityManager#MULTIPATH_PREFERENCE_* value or null if the value + * specified by config_networkMeteredMultipathPreference is used. + */ + public static void setNetworkMeteredMultipathPreference(@NonNull Context context, + @NonNull @MultipathPreference String preference) { + if (!(Integer.valueOf(preference) == MULTIPATH_PREFERENCE_HANDOVER + || Integer.valueOf(preference) == MULTIPATH_PREFERENCE_RELIABILITY + || Integer.valueOf(preference) == MULTIPATH_PREFERENCE_PERFORMANCE)) { + throw new IllegalArgumentException("Invalid private dns mode"); + } + Settings.Global.putString( + context.getContentResolver(), NETWORK_METERED_MULTIPATH_PREFERENCE, preference); + } + + /** + * Get the list of uids(from {@link Settings}) that should go on cellular networks in preference + * even when higher-priority networks are connected. + * + * @param context The {@link Context} to query the setting. + * @return A list of uids that should go on cellular networks in preference even when + * higher-priority networks are connected or null if no setting value. + */ + @NonNull + public static Set<Integer> getMobileDataPreferredUids(@NonNull Context context) { + final String uidList = Settings.Secure.getString( + context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS); + final Set<Integer> uids = new ArraySet<>(); + if (TextUtils.isEmpty(uidList)) { + return uids; + } + for (String uid : uidList.split(";")) { + uids.add(Integer.valueOf(uid)); + } + return uids; + } + + /** + * Set the list of uids(to {@link Settings}) that should go on cellular networks in preference + * even when higher-priority networks are connected. + * + * @param context The {@link Context} to set the setting. + * @param uidList A list of uids that should go on cellular networks in preference even when + * higher-priority networks are connected. + */ + public static void setMobileDataPreferredUids(@NonNull Context context, + @NonNull Set<Integer> uidList) { + final StringJoiner joiner = new StringJoiner(";"); + for (Integer uid : uidList) { + if (uid < 0 || UserHandle.getAppId(uid) > Process.LAST_APPLICATION_UID) { + throw new IllegalArgumentException("Invalid uid"); + } + joiner.add(uid.toString()); + } + Settings.Secure.putString( + context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS, joiner.toString()); + } + + /** + * Get the list of apps(from {@link Settings}) that is allowed on restricted networks. + * + * @param context The {@link Context} to query the setting. + * @return A list of apps that is allowed on restricted networks or null if no setting + * value. + */ + @NonNull + public static Set<String> getAppsAllowedOnRestrictedNetworks(@NonNull Context context) { + final String appList = Settings.Secure.getString( + context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS); + if (TextUtils.isEmpty(appList)) { + return new ArraySet<>(); + } + return new ArraySet<>(appList.split(";")); + } + + /** + * Set the list of apps(from {@link Settings}) that is allowed on restricted networks. + * + * Note: Please refer to android developer guidelines for valid app(package name). + * https://developer.android.com/guide/topics/manifest/manifest-element.html#package + * + * @param context The {@link Context} to set the setting. + * @param list A list of apps that is allowed on restricted networks. + */ + public static void setAppsAllowedOnRestrictedNetworks(@NonNull Context context, + @NonNull Set<String> list) { + final StringJoiner joiner = new StringJoiner(";"); + for (String app : list) { + if (app == null || app.contains(";")) { + throw new IllegalArgumentException("Invalid app(package name)"); + } + joiner.add(app); + } + Settings.Secure.putString(context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS, + joiner.toString()); + } +} diff --git a/framework/src/android/net/ConnectivityThread.java b/framework/src/android/net/ConnectivityThread.java new file mode 100644 index 0000000000000000000000000000000000000000..0b218e738b7793fbef23c2f51e1b7beb92d3d9eb --- /dev/null +++ b/framework/src/android/net/ConnectivityThread.java @@ -0,0 +1,56 @@ +/* + * 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; + +import android.os.HandlerThread; +import android.os.Looper; + +/** + * Shared singleton connectivity thread for the system. This is a thread for + * connectivity operations such as AsyncChannel connections to system services. + * Various connectivity manager objects can use this singleton as a common + * resource for their handlers instead of creating separate threads of their own. + * @hide + */ +public final class ConnectivityThread extends HandlerThread { + + // A class implementing the lazy holder idiom: the unique static instance + // of ConnectivityThread is instantiated in a thread-safe way (guaranteed by + // the language specs) the first time that Singleton is referenced in get() + // or getInstanceLooper(). + private static class Singleton { + private static final ConnectivityThread INSTANCE = createInstance(); + } + + private ConnectivityThread() { + super("ConnectivityThread"); + } + + private static ConnectivityThread createInstance() { + ConnectivityThread t = new ConnectivityThread(); + t.start(); + return t; + } + + public static ConnectivityThread get() { + return Singleton.INSTANCE; + } + + public static Looper getInstanceLooper() { + return Singleton.INSTANCE.getLooper(); + } +} diff --git a/framework/src/android/net/DhcpInfo.java b/framework/src/android/net/DhcpInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..912df67a0b45b1f68a488796c0a9196dcb0946e2 --- /dev/null +++ b/framework/src/android/net/DhcpInfo.java @@ -0,0 +1,105 @@ +/* + * 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A simple object for retrieving the results of a DHCP request. + */ +public class DhcpInfo implements Parcelable { + public int ipAddress; + public int gateway; + public int netmask; + public int dns1; + public int dns2; + public int serverAddress; + + public int leaseDuration; + + public DhcpInfo() { + super(); + } + + /** copy constructor {@hide} */ + public DhcpInfo(DhcpInfo source) { + if (source != null) { + ipAddress = source.ipAddress; + gateway = source.gateway; + netmask = source.netmask; + dns1 = source.dns1; + dns2 = source.dns2; + serverAddress = source.serverAddress; + leaseDuration = source.leaseDuration; + } + } + + public String toString() { + StringBuffer str = new StringBuffer(); + + str.append("ipaddr "); putAddress(str, ipAddress); + str.append(" gateway "); putAddress(str, gateway); + str.append(" netmask "); putAddress(str, netmask); + str.append(" dns1 "); putAddress(str, dns1); + str.append(" dns2 "); putAddress(str, dns2); + str.append(" DHCP server "); putAddress(str, serverAddress); + str.append(" lease ").append(leaseDuration).append(" seconds"); + + return str.toString(); + } + + private static void putAddress(StringBuffer buf, int addr) { + buf.append(NetworkUtils.intToInetAddress(addr).getHostAddress()); + } + + /** Implement the Parcelable interface */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(ipAddress); + dest.writeInt(gateway); + dest.writeInt(netmask); + dest.writeInt(dns1); + dest.writeInt(dns2); + dest.writeInt(serverAddress); + dest.writeInt(leaseDuration); + } + + /** Implement the Parcelable interface */ + public static final @android.annotation.NonNull Creator<DhcpInfo> CREATOR = + new Creator<DhcpInfo>() { + public DhcpInfo createFromParcel(Parcel in) { + DhcpInfo info = new DhcpInfo(); + info.ipAddress = in.readInt(); + info.gateway = in.readInt(); + info.netmask = in.readInt(); + info.dns1 = in.readInt(); + info.dns2 = in.readInt(); + info.serverAddress = in.readInt(); + info.leaseDuration = in.readInt(); + return info; + } + + public DhcpInfo[] newArray(int size) { + return new DhcpInfo[size]; + } + }; +} diff --git a/framework/src/android/net/DnsResolver.java b/framework/src/android/net/DnsResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..dac88ad907526e68c72e3b95e11dac9cebe72ae7 --- /dev/null +++ b/framework/src/android/net/DnsResolver.java @@ -0,0 +1,577 @@ +/* + * 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.net.NetworkUtils.getDnsNetwork; +import static android.net.NetworkUtils.resNetworkCancel; +import static android.net.NetworkUtils.resNetworkQuery; +import static android.net.NetworkUtils.resNetworkResult; +import static android.net.NetworkUtils.resNetworkSend; +import static android.net.util.DnsUtils.haveIpv4; +import static android.net.util.DnsUtils.haveIpv6; +import static android.net.util.DnsUtils.rfc6724Sort; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.system.OsConstants.ENONET; + +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.CancellationSignal; +import android.os.Looper; +import android.os.MessageQueue; +import android.system.ErrnoException; +import android.util.Log; + +import com.android.net.module.util.DnsPacket; + +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Dns resolver class for asynchronous dns querying + * + * Note that if a client sends a query with more than 1 record in the question section but + * the remote dns server does not support this, it may not respond at all, leading to a timeout. + * + */ +public final class DnsResolver { + private static final String TAG = "DnsResolver"; + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + private static final int MAXPACKET = 8 * 1024; + private static final int SLEEP_TIME_MS = 2; + + @IntDef(prefix = { "CLASS_" }, value = { + CLASS_IN + }) + @Retention(RetentionPolicy.SOURCE) + @interface QueryClass {} + public static final int CLASS_IN = 1; + + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_A, + TYPE_AAAA + }) + @Retention(RetentionPolicy.SOURCE) + @interface QueryType {} + public static final int TYPE_A = 1; + public static final int TYPE_AAAA = 28; + + @IntDef(prefix = { "FLAG_" }, value = { + FLAG_EMPTY, + FLAG_NO_RETRY, + FLAG_NO_CACHE_STORE, + FLAG_NO_CACHE_LOOKUP + }) + @Retention(RetentionPolicy.SOURCE) + @interface QueryFlag {} + public static final int FLAG_EMPTY = 0; + public static final int FLAG_NO_RETRY = 1 << 0; + public static final int FLAG_NO_CACHE_STORE = 1 << 1; + public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2; + + @IntDef(prefix = { "ERROR_" }, value = { + ERROR_PARSE, + ERROR_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + @interface DnsError {} + /** + * Indicates that there was an error parsing the response the query. + * The cause of this error is available via getCause() and is a {@link ParseException}. + */ + public static final int ERROR_PARSE = 0; + /** + * Indicates that there was an error sending the query. + * The cause of this error is available via getCause() and is an ErrnoException. + */ + public static final int ERROR_SYSTEM = 1; + + private static final int NETID_UNSET = 0; + + private static final DnsResolver sInstance = new DnsResolver(); + + /** + * Get instance for DnsResolver + */ + public static @NonNull DnsResolver getInstance() { + return sInstance; + } + + private DnsResolver() {} + + /** + * Base interface for answer callbacks + * + * @param <T> The type of the answer + */ + public interface Callback<T> { + /** + * Success response to + * {@link android.net.DnsResolver#query query()} or + * {@link android.net.DnsResolver#rawQuery rawQuery()}. + * + * Invoked when the answer to a query was successfully parsed. + * + * @param answer <T> answer to the query. + * @param rcode The response code in the DNS response. + * + * {@see android.net.DnsResolver#query query()} + */ + void onAnswer(@NonNull T answer, int rcode); + /** + * Error response to + * {@link android.net.DnsResolver#query query()} or + * {@link android.net.DnsResolver#rawQuery rawQuery()}. + * + * Invoked when there is no valid answer to + * {@link android.net.DnsResolver#query query()} + * {@link android.net.DnsResolver#rawQuery rawQuery()}. + * + * @param error a {@link DnsException} object with additional + * detail regarding the failure + */ + void onError(@NonNull DnsException error); + } + + /** + * Class to represent DNS error + */ + public static class DnsException extends Exception { + /** + * DNS error code as one of the ERROR_* constants + */ + @DnsError public final int code; + + DnsException(@DnsError int code, @Nullable Throwable cause) { + super(cause); + this.code = code; + } + } + + /** + * Send a raw DNS query. + * The answer will be provided asynchronously through the provided {@link Callback}. + * + * @param network {@link Network} specifying which network to query on. + * {@code null} for query on default network. + * @param query blob message to query + * @param flags flags as a combination of the FLAGS_* constants + * @param executor The {@link Executor} that the callback should be executed on. + * @param cancellationSignal used by the caller to signal if the query should be + * cancelled. May be {@code null}. + * @param callback a {@link Callback} which will be called to notify the caller + * of the result of dns query. + */ + public void rawQuery(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags, + @NonNull @CallbackExecutor Executor executor, + @Nullable CancellationSignal cancellationSignal, + @NonNull Callback<? super byte[]> callback) { + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + return; + } + final Object lock = new Object(); + final FileDescriptor queryfd; + try { + queryfd = resNetworkSend((network != null) + ? network.getNetIdForResolv() : NETID_UNSET, query, query.length, flags); + } catch (ErrnoException e) { + executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); + return; + } + + synchronized (lock) { + registerFDListener(executor, queryfd, callback, cancellationSignal, lock); + if (cancellationSignal == null) return; + addCancellationSignal(cancellationSignal, queryfd, lock); + } + } + + /** + * Send a DNS query with the specified name, class and query type. + * The answer will be provided asynchronously through the provided {@link Callback}. + * + * @param network {@link Network} specifying which network to query on. + * {@code null} for query on default network. + * @param domain domain name to query + * @param nsClass dns class as one of the CLASS_* constants + * @param nsType dns resource record (RR) type as one of the TYPE_* constants + * @param flags flags as a combination of the FLAGS_* constants + * @param executor The {@link Executor} that the callback should be executed on. + * @param cancellationSignal used by the caller to signal if the query should be + * cancelled. May be {@code null}. + * @param callback a {@link Callback} which will be called to notify the caller + * of the result of dns query. + */ + public void rawQuery(@Nullable Network network, @NonNull String domain, + @QueryClass int nsClass, @QueryType int nsType, @QueryFlag int flags, + @NonNull @CallbackExecutor Executor executor, + @Nullable CancellationSignal cancellationSignal, + @NonNull Callback<? super byte[]> callback) { + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + return; + } + final Object lock = new Object(); + final FileDescriptor queryfd; + try { + queryfd = resNetworkQuery((network != null) + ? network.getNetIdForResolv() : NETID_UNSET, domain, nsClass, nsType, flags); + } catch (ErrnoException e) { + executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); + return; + } + synchronized (lock) { + registerFDListener(executor, queryfd, callback, cancellationSignal, lock); + if (cancellationSignal == null) return; + addCancellationSignal(cancellationSignal, queryfd, lock); + } + } + + private class InetAddressAnswerAccumulator implements Callback<byte[]> { + private final List<InetAddress> mAllAnswers; + private final Network mNetwork; + private int mRcode; + private DnsException mDnsException; + private final Callback<? super List<InetAddress>> mUserCallback; + private final int mTargetAnswerCount; + private int mReceivedAnswerCount = 0; + + InetAddressAnswerAccumulator(@NonNull Network network, int size, + @NonNull Callback<? super List<InetAddress>> callback) { + mNetwork = network; + mTargetAnswerCount = size; + mAllAnswers = new ArrayList<>(); + mUserCallback = callback; + } + + private boolean maybeReportError() { + if (mRcode != 0) { + mUserCallback.onAnswer(mAllAnswers, mRcode); + return true; + } + if (mDnsException != null) { + mUserCallback.onError(mDnsException); + return true; + } + return false; + } + + private void maybeReportAnswer() { + if (++mReceivedAnswerCount != mTargetAnswerCount) return; + if (mAllAnswers.isEmpty() && maybeReportError()) return; + mUserCallback.onAnswer(rfc6724Sort(mNetwork, mAllAnswers), mRcode); + } + + @Override + public void onAnswer(@NonNull byte[] answer, int rcode) { + // If at least one query succeeded, return an rcode of 0. + // Otherwise, arbitrarily return the first rcode received. + if (mReceivedAnswerCount == 0 || rcode == 0) { + mRcode = rcode; + } + try { + mAllAnswers.addAll(new DnsAddressAnswer(answer).getAddresses()); + } catch (DnsPacket.ParseException e) { + // Convert the com.android.net.module.util.DnsPacket.ParseException to an + // android.net.ParseException. This is the type that was used in Q and is implied + // by the public documentation of ERROR_PARSE. + // + // DnsPacket cannot throw android.net.ParseException directly because it's @hide. + ParseException pe = new ParseException(e.reason, e.getCause()); + pe.setStackTrace(e.getStackTrace()); + mDnsException = new DnsException(ERROR_PARSE, pe); + } + maybeReportAnswer(); + } + + @Override + public void onError(@NonNull DnsException error) { + mDnsException = error; + maybeReportAnswer(); + } + } + + /** + * Send a DNS query with the specified name on a network with both IPv4 and IPv6, + * get back a set of InetAddresses with rfc6724 sorting style asynchronously. + * + * This method will examine the connection ability on given network, and query IPv4 + * and IPv6 if connection is available. + * + * If at least one query succeeded with valid answer, rcode will be 0 + * + * The answer will be provided asynchronously through the provided {@link Callback}. + * + * @param network {@link Network} specifying which network to query on. + * {@code null} for query on default network. + * @param domain domain name to query + * @param flags flags as a combination of the FLAGS_* constants + * @param executor The {@link Executor} that the callback should be executed on. + * @param cancellationSignal used by the caller to signal if the query should be + * cancelled. May be {@code null}. + * @param callback a {@link Callback} which will be called to notify the + * caller of the result of dns query. + */ + public void query(@Nullable Network network, @NonNull String domain, @QueryFlag int flags, + @NonNull @CallbackExecutor Executor executor, + @Nullable CancellationSignal cancellationSignal, + @NonNull Callback<? super List<InetAddress>> callback) { + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + return; + } + final Object lock = new Object(); + final Network queryNetwork; + try { + queryNetwork = (network != null) ? network : getDnsNetwork(); + } catch (ErrnoException e) { + executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); + return; + } + final boolean queryIpv6 = haveIpv6(queryNetwork); + final boolean queryIpv4 = haveIpv4(queryNetwork); + + // This can only happen if queryIpv4 and queryIpv6 are both false. + // This almost certainly means that queryNetwork does not exist or no longer exists. + if (!queryIpv6 && !queryIpv4) { + executor.execute(() -> callback.onError( + new DnsException(ERROR_SYSTEM, new ErrnoException("resNetworkQuery", ENONET)))); + return; + } + + final FileDescriptor v4fd; + final FileDescriptor v6fd; + + int queryCount = 0; + + if (queryIpv6) { + try { + v6fd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, + TYPE_AAAA, flags); + } catch (ErrnoException e) { + executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); + return; + } + queryCount++; + } else v6fd = null; + + // Avoiding gateways drop packets if queries are sent too close together + try { + Thread.sleep(SLEEP_TIME_MS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + + if (queryIpv4) { + try { + v4fd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, TYPE_A, + flags); + } catch (ErrnoException e) { + if (queryIpv6) resNetworkCancel(v6fd); // Closes fd, marks it invalid. + executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); + return; + } + queryCount++; + } else v4fd = null; + + final InetAddressAnswerAccumulator accumulator = + new InetAddressAnswerAccumulator(queryNetwork, queryCount, callback); + + synchronized (lock) { + if (queryIpv6) { + registerFDListener(executor, v6fd, accumulator, cancellationSignal, lock); + } + if (queryIpv4) { + registerFDListener(executor, v4fd, accumulator, cancellationSignal, lock); + } + if (cancellationSignal == null) return; + cancellationSignal.setOnCancelListener(() -> { + synchronized (lock) { + if (queryIpv4) cancelQuery(v4fd); + if (queryIpv6) cancelQuery(v6fd); + } + }); + } + } + + /** + * Send a DNS query with the specified name and query type, get back a set of + * InetAddresses with rfc6724 sorting style asynchronously. + * + * The answer will be provided asynchronously through the provided {@link Callback}. + * + * @param network {@link Network} specifying which network to query on. + * {@code null} for query on default network. + * @param domain domain name to query + * @param nsType dns resource record (RR) type as one of the TYPE_* constants + * @param flags flags as a combination of the FLAGS_* constants + * @param executor The {@link Executor} that the callback should be executed on. + * @param cancellationSignal used by the caller to signal if the query should be + * cancelled. May be {@code null}. + * @param callback a {@link Callback} which will be called to notify the caller + * of the result of dns query. + */ + public void query(@Nullable Network network, @NonNull String domain, + @QueryType int nsType, @QueryFlag int flags, + @NonNull @CallbackExecutor Executor executor, + @Nullable CancellationSignal cancellationSignal, + @NonNull Callback<? super List<InetAddress>> callback) { + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + return; + } + final Object lock = new Object(); + final FileDescriptor queryfd; + final Network queryNetwork; + try { + queryNetwork = (network != null) ? network : getDnsNetwork(); + queryfd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, nsType, + flags); + } catch (ErrnoException e) { + executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e))); + return; + } + final InetAddressAnswerAccumulator accumulator = + new InetAddressAnswerAccumulator(queryNetwork, 1, callback); + synchronized (lock) { + registerFDListener(executor, queryfd, accumulator, cancellationSignal, lock); + if (cancellationSignal == null) return; + addCancellationSignal(cancellationSignal, queryfd, lock); + } + } + + /** + * Class to retrieve DNS response + * + * @hide + */ + public static final class DnsResponse { + public final @NonNull byte[] answerbuf; + public final int rcode; + public DnsResponse(@NonNull byte[] answerbuf, int rcode) { + this.answerbuf = answerbuf; + this.rcode = rcode; + } + } + + private void registerFDListener(@NonNull Executor executor, + @NonNull FileDescriptor queryfd, @NonNull Callback<? super byte[]> answerCallback, + @Nullable CancellationSignal cancellationSignal, @NonNull Object lock) { + final MessageQueue mainThreadMessageQueue = Looper.getMainLooper().getQueue(); + mainThreadMessageQueue.addOnFileDescriptorEventListener( + queryfd, + FD_EVENTS, + (fd, events) -> { + // b/134310704 + // Unregister fd event listener before resNetworkResult is called to prevent + // race condition caused by fd reused. + // For example when querying v4 and v6, it's possible that the first query ends + // and the fd is closed before the second request starts, which might return + // the same fd for the second request. By that time, the looper must have + // unregistered the fd, otherwise another event listener can't be registered. + mainThreadMessageQueue.removeOnFileDescriptorEventListener(fd); + + executor.execute(() -> { + DnsResponse resp = null; + ErrnoException exception = null; + synchronized (lock) { + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + return; + } + try { + resp = resNetworkResult(fd); // Closes fd, marks it invalid. + } catch (ErrnoException e) { + Log.w(TAG, "resNetworkResult:" + e.toString()); + exception = e; + } + } + if (exception != null) { + answerCallback.onError(new DnsException(ERROR_SYSTEM, exception)); + return; + } + answerCallback.onAnswer(resp.answerbuf, resp.rcode); + }); + + // The file descriptor has already been unregistered, so it does not really + // matter what is returned here. In spirit 0 (meaning "unregister this FD") + // is still the closest to what the looper needs to do. When returning 0, + // Looper knows to ignore the fd if it has already been unregistered. + return 0; + }); + } + + private void cancelQuery(@NonNull FileDescriptor queryfd) { + if (!queryfd.valid()) return; + Looper.getMainLooper().getQueue().removeOnFileDescriptorEventListener(queryfd); + resNetworkCancel(queryfd); // Closes fd, marks it invalid. + } + + private void addCancellationSignal(@NonNull CancellationSignal cancellationSignal, + @NonNull FileDescriptor queryfd, @NonNull Object lock) { + cancellationSignal.setOnCancelListener(() -> { + synchronized (lock) { + cancelQuery(queryfd); + } + }); + } + + private static class DnsAddressAnswer extends DnsPacket { + private static final String TAG = "DnsResolver.DnsAddressAnswer"; + private static final boolean DBG = false; + + private final int mQueryType; + + DnsAddressAnswer(@NonNull byte[] data) throws ParseException { + super(data); + if ((mHeader.flags & (1 << 15)) == 0) { + throw new ParseException("Not an answer packet"); + } + if (mHeader.getRecordCount(QDSECTION) == 0) { + throw new ParseException("No question found"); + } + // Expect only one question in question section. + mQueryType = mRecords[QDSECTION].get(0).nsType; + } + + public @NonNull List<InetAddress> getAddresses() { + final List<InetAddress> results = new ArrayList<InetAddress>(); + if (mHeader.getRecordCount(ANSECTION) == 0) return results; + + for (final DnsRecord ansSec : mRecords[ANSECTION]) { + // Only support A and AAAA, also ignore answers if query type != answer type. + int nsType = ansSec.nsType; + if (nsType != mQueryType || (nsType != TYPE_A && nsType != TYPE_AAAA)) { + continue; + } + try { + results.add(InetAddress.getByAddress(ansSec.getRR())); + } catch (UnknownHostException e) { + if (DBG) { + Log.w(TAG, "rr to address fail"); + } + } + } + return results; + } + } + +} diff --git a/framework/src/android/net/DnsResolverServiceManager.java b/framework/src/android/net/DnsResolverServiceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..79009e8d629eb799da512f2d33fff49556fe331a --- /dev/null +++ b/framework/src/android/net/DnsResolverServiceManager.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.os.IBinder; + +/** + * Provides a way to obtain the DnsResolver binder objects. + * + * @hide + */ +public class DnsResolverServiceManager { + /** Service name for the DNS resolver. Keep in sync with DnsResolverService.h */ + public static final String DNS_RESOLVER_SERVICE = "dnsresolver"; + + private final IBinder mResolver; + + DnsResolverServiceManager(IBinder resolver) { + mResolver = resolver; + } + + /** + * Get an {@link IBinder} representing the DnsResolver stable AIDL interface + * + * @return {@link android.net.IDnsResolver} IBinder. + */ + @NonNull + public IBinder getService() { + return mResolver; + } +} diff --git a/framework/src/android/net/ICaptivePortal.aidl b/framework/src/android/net/ICaptivePortal.aidl new file mode 100644 index 0000000000000000000000000000000000000000..e35f8d46afe747792baa4655afff399d5efb7a0d --- /dev/null +++ b/framework/src/android/net/ICaptivePortal.aidl @@ -0,0 +1,26 @@ +/** + * 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; + +/** + * Interface to inform NetworkMonitor of decisions of app handling captive portal. + * @hide + */ +oneway interface ICaptivePortal { + void appRequest(int request); + void appResponse(int response); +} diff --git a/framework/src/android/net/IConnectivityDiagnosticsCallback.aidl b/framework/src/android/net/IConnectivityDiagnosticsCallback.aidl new file mode 100644 index 0000000000000000000000000000000000000000..82b64a9280009dd781469c9245bea3449023bb6b --- /dev/null +++ b/framework/src/android/net/IConnectivityDiagnosticsCallback.aidl @@ -0,0 +1,28 @@ +/** + * + * 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.ConnectivityDiagnosticsManager; +import android.net.Network; + +/** @hide */ +oneway interface IConnectivityDiagnosticsCallback { + void onConnectivityReportAvailable(in ConnectivityDiagnosticsManager.ConnectivityReport report); + void onDataStallSuspected(in ConnectivityDiagnosticsManager.DataStallReport report); + void onNetworkConnectivityReported(in Network n, boolean hasConnectivity); +} \ No newline at end of file diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl new file mode 100644 index 0000000000000000000000000000000000000000..c434bbcb0f51d708d89fb0de9e6e47efd5d193b9 --- /dev/null +++ b/framework/src/android/net/IConnectivityManager.aidl @@ -0,0 +1,229 @@ +/** + * 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; + +import android.app.PendingIntent; +import android.net.ConnectionInfo; +import android.net.ConnectivityDiagnosticsManager; +import android.net.IConnectivityDiagnosticsCallback; +import android.net.INetworkAgent; +import android.net.IOnCompleteListener; +import android.net.INetworkActivityListener; +import android.net.INetworkOfferCallback; +import android.net.IQosCallback; +import android.net.ISocketKeepaliveCallback; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.NetworkState; +import android.net.NetworkStateSnapshot; +import android.net.OemNetworkPreferences; +import android.net.ProxyInfo; +import android.net.UidRange; +import android.net.QosSocketInfo; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Messenger; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.ResultReceiver; +import android.os.UserHandle; + +/** + * Interface that answers queries about, and allows changing, the + * state of network connectivity. + */ +/** {@hide} */ +interface IConnectivityManager +{ + Network getActiveNetwork(); + Network getActiveNetworkForUid(int uid, boolean ignoreBlocked); + @UnsupportedAppUsage + NetworkInfo getActiveNetworkInfo(); + NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked); + @UnsupportedAppUsage(maxTargetSdk = 28) + NetworkInfo getNetworkInfo(int networkType); + NetworkInfo getNetworkInfoForUid(in Network network, int uid, boolean ignoreBlocked); + @UnsupportedAppUsage + NetworkInfo[] getAllNetworkInfo(); + Network getNetworkForType(int networkType); + Network[] getAllNetworks(); + NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser( + int userId, String callingPackageName, String callingAttributionTag); + + boolean isNetworkSupported(int networkType); + + @UnsupportedAppUsage + LinkProperties getActiveLinkProperties(); + LinkProperties getLinkPropertiesForType(int networkType); + LinkProperties getLinkProperties(in Network network); + + NetworkCapabilities getNetworkCapabilities(in Network network, String callingPackageName, + String callingAttributionTag); + + @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) + NetworkState[] getAllNetworkState(); + + List<NetworkStateSnapshot> getAllNetworkStateSnapshots(); + + boolean isActiveNetworkMetered(); + + boolean requestRouteToHostAddress(int networkType, in byte[] hostAddress, + String callingPackageName, String callingAttributionTag); + + @UnsupportedAppUsage(maxTargetSdk = 29, + publicAlternatives = "Use {@code TetheringManager#getLastTetherError} as alternative") + int getLastTetherError(String iface); + + @UnsupportedAppUsage(maxTargetSdk = 29, + publicAlternatives = "Use {@code TetheringManager#getTetherableIfaces} as alternative") + String[] getTetherableIfaces(); + + @UnsupportedAppUsage(maxTargetSdk = 29, + publicAlternatives = "Use {@code TetheringManager#getTetheredIfaces} as alternative") + String[] getTetheredIfaces(); + + @UnsupportedAppUsage(maxTargetSdk = 29, + publicAlternatives = "Use {@code TetheringManager#getTetheringErroredIfaces} " + + "as Alternative") + String[] getTetheringErroredIfaces(); + + @UnsupportedAppUsage(maxTargetSdk = 29, + publicAlternatives = "Use {@code TetheringManager#getTetherableUsbRegexs} as alternative") + String[] getTetherableUsbRegexs(); + + @UnsupportedAppUsage(maxTargetSdk = 29, + publicAlternatives = "Use {@code TetheringManager#getTetherableWifiRegexs} as alternative") + String[] getTetherableWifiRegexs(); + + @UnsupportedAppUsage(maxTargetSdk = 28) + void reportInetCondition(int networkType, int percentage); + + void reportNetworkConnectivity(in Network network, boolean hasConnectivity); + + ProxyInfo getGlobalProxy(); + + void setGlobalProxy(in ProxyInfo p); + + ProxyInfo getProxyForNetwork(in Network nework); + + void setRequireVpnForUids(boolean requireVpn, in UidRange[] ranges); + void setLegacyLockdownVpnEnabled(boolean enabled); + + void setProvisioningNotificationVisible(boolean visible, int networkType, in String action); + + void setAirplaneMode(boolean enable); + + boolean requestBandwidthUpdate(in Network network); + + int registerNetworkProvider(in Messenger messenger, in String name); + void unregisterNetworkProvider(in Messenger messenger); + + void declareNetworkRequestUnfulfillable(in NetworkRequest request); + + Network registerNetworkAgent(in INetworkAgent na, in NetworkInfo ni, in LinkProperties lp, + in NetworkCapabilities nc, in NetworkScore score, in NetworkAgentConfig config, + in int factorySerialNumber); + + NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType, + in Messenger messenger, int timeoutSec, in IBinder binder, int legacy, + int callbackFlags, String callingPackageName, String callingAttributionTag); + + NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities, + in PendingIntent operation, String callingPackageName, String callingAttributionTag); + + void releasePendingNetworkRequest(in PendingIntent operation); + + NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities, + in Messenger messenger, in IBinder binder, int callbackFlags, String callingPackageName, + String callingAttributionTag); + + void pendingListenForNetwork(in NetworkCapabilities networkCapabilities, + in PendingIntent operation, String callingPackageName, + String callingAttributionTag); + + void releaseNetworkRequest(in NetworkRequest networkRequest); + + void setAcceptUnvalidated(in Network network, boolean accept, boolean always); + void setAcceptPartialConnectivity(in Network network, boolean accept, boolean always); + void setAvoidUnvalidated(in Network network); + void startCaptivePortalApp(in Network network); + void startCaptivePortalAppInternal(in Network network, in Bundle appExtras); + + boolean shouldAvoidBadWifi(); + int getMultipathPreference(in Network Network); + + NetworkRequest getDefaultRequest(); + + int getRestoreDefaultNetworkDelay(int networkType); + + void factoryReset(); + + void startNattKeepalive(in Network network, int intervalSeconds, + in ISocketKeepaliveCallback cb, String srcAddr, int srcPort, String dstAddr); + + void startNattKeepaliveWithFd(in Network network, in ParcelFileDescriptor pfd, int resourceId, + int intervalSeconds, in ISocketKeepaliveCallback cb, String srcAddr, + String dstAddr); + + void startTcpKeepalive(in Network network, in ParcelFileDescriptor pfd, int intervalSeconds, + in ISocketKeepaliveCallback cb); + + void stopKeepalive(in Network network, int slot); + + String getCaptivePortalServerUrl(); + + byte[] getNetworkWatchlistConfigHash(); + + int getConnectionOwnerUid(in ConnectionInfo connectionInfo); + + void registerConnectivityDiagnosticsCallback(in IConnectivityDiagnosticsCallback callback, + in NetworkRequest request, String callingPackageName); + void unregisterConnectivityDiagnosticsCallback(in IConnectivityDiagnosticsCallback callback); + + IBinder startOrGetTestNetworkService(); + + void simulateDataStall(int detectionMethod, long timestampMillis, in Network network, + in PersistableBundle extras); + + void systemReady(); + + void registerNetworkActivityListener(in INetworkActivityListener l); + + void unregisterNetworkActivityListener(in INetworkActivityListener l); + + boolean isDefaultNetworkActive(); + + void registerQosSocketCallback(in QosSocketInfo socketInfo, in IQosCallback callback); + void unregisterQosCallback(in IQosCallback callback); + + void setOemNetworkPreference(in OemNetworkPreferences preference, + in IOnCompleteListener listener); + + void setProfileNetworkPreference(in UserHandle profile, int preference, + in IOnCompleteListener listener); + + int getRestrictBackgroundStatusByCaller(); + + void offerNetwork(int providerId, in NetworkScore score, + in NetworkCapabilities caps, in INetworkOfferCallback callback); + void unofferNetwork(in INetworkOfferCallback callback); +} diff --git a/framework/src/android/net/INetworkActivityListener.aidl b/framework/src/android/net/INetworkActivityListener.aidl new file mode 100644 index 0000000000000000000000000000000000000000..79687dd3bf06eebd4338e179dc5a8281520e8094 --- /dev/null +++ b/framework/src/android/net/INetworkActivityListener.aidl @@ -0,0 +1,24 @@ +/* Copyright 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; + +/** + * @hide + */ +oneway interface INetworkActivityListener +{ + void onNetworkActive(); +} diff --git a/framework/src/android/net/INetworkAgent.aidl b/framework/src/android/net/INetworkAgent.aidl new file mode 100644 index 0000000000000000000000000000000000000000..d941d4b95b56abd18c1c568379630af375db9bf9 --- /dev/null +++ b/framework/src/android/net/INetworkAgent.aidl @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing perNmissions and + * limitations under the License. + */ +package android.net; + +import android.net.NattKeepalivePacketData; +import android.net.QosFilterParcelable; +import android.net.TcpKeepalivePacketData; + +import android.net.INetworkAgentRegistry; + +/** + * Interface to notify NetworkAgent of connectivity events. + * @hide + */ +oneway interface INetworkAgent { + void onRegistered(in INetworkAgentRegistry registry); + void onDisconnected(); + void onBandwidthUpdateRequested(); + void onValidationStatusChanged(int validationStatus, + in @nullable String captivePortalUrl); + void onSaveAcceptUnvalidated(boolean acceptUnvalidated); + void onStartNattSocketKeepalive(int slot, int intervalDurationMs, + in NattKeepalivePacketData packetData); + void onStartTcpSocketKeepalive(int slot, int intervalDurationMs, + in TcpKeepalivePacketData packetData); + void onStopSocketKeepalive(int slot); + void onSignalStrengthThresholdsUpdated(in int[] thresholds); + void onPreventAutomaticReconnect(); + void onAddNattKeepalivePacketFilter(int slot, + in NattKeepalivePacketData packetData); + void onAddTcpKeepalivePacketFilter(int slot, + in TcpKeepalivePacketData packetData); + void onRemoveKeepalivePacketFilter(int slot); + void onQosFilterCallbackRegistered(int qosCallbackId, in QosFilterParcelable filterParcel); + void onQosCallbackUnregistered(int qosCallbackId); + void onNetworkCreated(); + void onNetworkDestroyed(); +} diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl new file mode 100644 index 0000000000000000000000000000000000000000..9a58add5d2ba1d1272b244e29ba08bd5224e50ca --- /dev/null +++ b/framework/src/android/net/INetworkAgentRegistry.aidl @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2020, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing perNmissions and + * limitations under the License. + */ +package android.net; + +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkScore; +import android.net.QosSession; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; + +/** + * Interface for NetworkAgents to send network properties. + * @hide + */ +oneway interface INetworkAgentRegistry { + void sendNetworkCapabilities(in NetworkCapabilities nc); + void sendLinkProperties(in LinkProperties lp); + // TODO: consider replacing this by "markConnected()" and removing + void sendNetworkInfo(in NetworkInfo info); + void sendScore(in NetworkScore score); + void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial); + void sendSocketKeepaliveEvent(int slot, int reason); + void sendUnderlyingNetworks(in @nullable List<Network> networks); + void sendEpsQosSessionAvailable(int callbackId, in QosSession session, in EpsBearerQosSessionAttributes attributes); + void sendNrQosSessionAvailable(int callbackId, in QosSession session, in NrQosSessionAttributes attributes); + void sendQosSessionLost(int qosCallbackId, in QosSession session); + void sendQosCallbackError(int qosCallbackId, int exceptionType); + void sendTeardownDelayMs(int teardownDelayMs); + void sendLingerDuration(int durationMs); +} diff --git a/framework/src/android/net/INetworkOfferCallback.aidl b/framework/src/android/net/INetworkOfferCallback.aidl new file mode 100644 index 0000000000000000000000000000000000000000..ecfba21b8ab6127f216242bd916a625fadd08db3 --- /dev/null +++ b/framework/src/android/net/INetworkOfferCallback.aidl @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.NetworkRequest; + +/** + * A callback registered with connectivity by network providers together with + * a NetworkOffer. + * + * When the network for this offer is needed to satisfy some application or + * system component, connectivity will call onNetworkNeeded on this callback. + * When this happens, the provider should try and bring up the network. + * + * When the network for this offer is no longer needed, for example because + * the application has withdrawn the request or if the request is being + * satisfied by a network that this offer will never be able to beat, + * connectivity calls onNetworkUnneeded. When this happens, the provider + * should stop trying to bring up the network, or tear it down if it has + * already been brought up. + * + * When NetworkProvider#offerNetwork is called, the provider can expect to + * immediately receive all requests that can be fulfilled by that offer and + * are not already satisfied by a better network. It is possible no such + * request is currently outstanding, because no requests have been made that + * can be satisfied by this offer, or because all such requests are already + * satisfied by a better network. + * onNetworkNeeded can be called at any time after registration and until the + * offer is withdrawn with NetworkProvider#unofferNetwork is called. This + * typically happens when a new network request is filed by an application, + * or when the network satisfying a request disconnects and this offer now + * stands a chance to supply the best network for it. + * + * @hide + */ +oneway interface INetworkOfferCallback { + /** + * Called when a network for this offer is needed to fulfill this request. + * @param networkRequest the request to satisfy + */ + void onNetworkNeeded(in NetworkRequest networkRequest); + + /** + * Informs the registrant that the offer is no longer valuable to fulfill this request. + */ + void onNetworkUnneeded(in NetworkRequest networkRequest); +} diff --git a/framework/src/android/net/IOnCompleteListener.aidl b/framework/src/android/net/IOnCompleteListener.aidl new file mode 100644 index 0000000000000000000000000000000000000000..4bb89f6c89e455db568b750956f5a2d4b3702913 --- /dev/null +++ b/framework/src/android/net/IOnCompleteListener.aidl @@ -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 android.net; + +/** @hide */ +oneway interface IOnCompleteListener { + void onComplete(); +} diff --git a/framework/src/android/net/IQosCallback.aidl b/framework/src/android/net/IQosCallback.aidl new file mode 100644 index 0000000000000000000000000000000000000000..c9735419f7dd3069caf887c73325da371c97d7eb --- /dev/null +++ b/framework/src/android/net/IQosCallback.aidl @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.os.Bundle; +import android.net.QosSession; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; + +/** + * AIDL interface for QosCallback + * + * @hide + */ +oneway interface IQosCallback +{ + void onQosEpsBearerSessionAvailable(in QosSession session, + in EpsBearerQosSessionAttributes attributes); + void onNrQosSessionAvailable(in QosSession session, + in NrQosSessionAttributes attributes); + void onQosSessionLost(in QosSession session); + void onError(in int type); +} diff --git a/framework/src/android/net/ISocketKeepaliveCallback.aidl b/framework/src/android/net/ISocketKeepaliveCallback.aidl new file mode 100644 index 0000000000000000000000000000000000000000..020fbcacbfef9c027bdad8f54035704203056486 --- /dev/null +++ b/framework/src/android/net/ISocketKeepaliveCallback.aidl @@ -0,0 +1,34 @@ +/** + * 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; + +/** + * Callback to provide status changes of keepalive offload. + * + * @hide + */ +oneway interface ISocketKeepaliveCallback +{ + /** The keepalive was successfully started. */ + void onStarted(int slot); + /** The keepalive was successfully stopped. */ + void onStopped(); + /** The keepalive was stopped because of an error. */ + void onError(int error); + /** The keepalive on a TCP socket was stopped because the socket received data. */ + void onDataReceived(); +} diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl new file mode 100644 index 0000000000000000000000000000000000000000..2a863adde581d364b285a209753df521ff661ffb --- /dev/null +++ b/framework/src/android/net/ITestNetworkManager.aidl @@ -0,0 +1,39 @@ +/** + * 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; + +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.TestNetworkInterface; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; + +/** + * Interface that allows for creation and management of test-only networks. + * + * @hide + */ +interface ITestNetworkManager +{ + TestNetworkInterface createTunInterface(in LinkAddress[] linkAddrs); + TestNetworkInterface createTapInterface(); + + void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered, + in int[] administratorUids, in IBinder binder); + + void teardownTestNetwork(int netId); +} diff --git a/framework/src/android/net/InetAddressCompat.java b/framework/src/android/net/InetAddressCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..6b7e75c75359e86d0bb0e0ae02964fd059a47f18 --- /dev/null +++ b/framework/src/android/net/InetAddressCompat.java @@ -0,0 +1,88 @@ +/* + * 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.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Compatibility utility for InetAddress core platform APIs. + * + * Connectivity has access to such APIs, but they are not part of the module_current stubs yet + * (only core_current). Most stable core platform APIs are included manually in the connectivity + * build rules, but because InetAddress is also part of the base java SDK that is earlier on the + * classpath, the extra core platform APIs are not seen. + * + * TODO (b/183097033): remove this utility as soon as core_current is part of module_current + * @hide + */ +public class InetAddressCompat { + + /** + * @see InetAddress#clearDnsCache() + */ + public static void clearDnsCache() { + try { + InetAddress.class.getMethod("clearDnsCache").invoke(null); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw new IllegalStateException("Unknown InvocationTargetException", e.getCause()); + } catch (IllegalAccessException | NoSuchMethodException e) { + Log.wtf(InetAddressCompat.class.getSimpleName(), "Error clearing DNS cache", e); + } + } + + /** + * @see InetAddress#getAllByNameOnNet(String, int) + */ + public static InetAddress[] getAllByNameOnNet(String host, int netId) throws + UnknownHostException { + return (InetAddress[]) callGetByNameMethod("getAllByNameOnNet", host, netId); + } + + /** + * @see InetAddress#getByNameOnNet(String, int) + */ + public static InetAddress getByNameOnNet(String host, int netId) throws + UnknownHostException { + return (InetAddress) callGetByNameMethod("getByNameOnNet", host, netId); + } + + private static Object callGetByNameMethod(String method, String host, int netId) + throws UnknownHostException { + try { + return InetAddress.class.getMethod(method, String.class, int.class) + .invoke(null, host, netId); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof UnknownHostException) { + throw (UnknownHostException) e.getCause(); + } + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw new IllegalStateException("Unknown InvocationTargetException", e.getCause()); + } catch (IllegalAccessException | NoSuchMethodException e) { + Log.wtf(InetAddressCompat.class.getSimpleName(), "Error calling " + method, e); + throw new IllegalStateException("Error querying via " + method, e); + } + } +} diff --git a/framework/src/android/net/InetAddresses.java b/framework/src/android/net/InetAddresses.java new file mode 100644 index 0000000000000000000000000000000000000000..01b795e456faefa7cd446bfb3293e36124cff104 --- /dev/null +++ b/framework/src/android/net/InetAddresses.java @@ -0,0 +1,65 @@ +/* + * 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; + +import android.annotation.NonNull; + +import libcore.net.InetAddressUtils; + +import java.net.InetAddress; + +/** + * Utility methods for {@link InetAddress} implementations. + */ +public class InetAddresses { + + private InetAddresses() {} + + /** + * Checks to see if the {@code address} is a numeric address (such as {@code "192.0.2.1"} or + * {@code "2001:db8::1:2"}). + * + * <p>A numeric address is either an IPv4 address containing exactly 4 decimal numbers or an + * IPv6 numeric address. IPv4 addresses that consist of either hexadecimal or octal digits or + * do not have exactly 4 numbers are not treated as numeric. + * + * <p>This method will never do a DNS lookup. + * + * @param address the address to parse. + * @return true if the supplied address is numeric, false otherwise. + */ + public static boolean isNumericAddress(@NonNull String address) { + return InetAddressUtils.isNumericAddress(address); + } + + /** + * Returns an InetAddress corresponding to the given numeric address (such + * as {@code "192.168.0.1"} or {@code "2001:4860:800d::68"}). + * + * <p>See {@link #isNumericAddress(String)} (String)} for a definition as to what constitutes a + * numeric address. + * + * <p>This method will never do a DNS lookup. + * + * @param address the address to parse, must be numeric. + * @return an {@link InetAddress} instance corresponding to the address. + * @throws IllegalArgumentException if {@code address} is not a numeric address. + */ + public static @NonNull InetAddress parseNumericAddress(@NonNull String address) { + return InetAddressUtils.parseNumericAddress(address); + } +} diff --git a/framework/src/android/net/InvalidPacketException.java b/framework/src/android/net/InvalidPacketException.java new file mode 100644 index 0000000000000000000000000000000000000000..1873d778c0f20cbe5b954a4bccb36c43086ff03f --- /dev/null +++ b/framework/src/android/net/InvalidPacketException.java @@ -0,0 +1,66 @@ +/* + * 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.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Thrown when a packet is invalid. + * @hide + */ +@SystemApi +public final class InvalidPacketException extends Exception { + private final int mError; + + // Must match SocketKeepalive#ERROR_INVALID_IP_ADDRESS. + /** Invalid IP address. */ + public static final int ERROR_INVALID_IP_ADDRESS = -21; + + // Must match SocketKeepalive#ERROR_INVALID_PORT. + /** Invalid port number. */ + public static final int ERROR_INVALID_PORT = -22; + + // Must match SocketKeepalive#ERROR_INVALID_LENGTH. + /** Invalid packet length. */ + public static final int ERROR_INVALID_LENGTH = -23; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ERROR_" }, value = { + ERROR_INVALID_IP_ADDRESS, + ERROR_INVALID_PORT, + ERROR_INVALID_LENGTH + }) + public @interface ErrorCode {} + + /** + * This packet is invalid. + * See the error code for details. + */ + public InvalidPacketException(@ErrorCode final int error) { + this.mError = error; + } + + /** Get error code. */ + public int getError() { + return mError; + } +} diff --git a/framework/src/android/net/IpConfiguration.java b/framework/src/android/net/IpConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..d5f8b2edb6bbda24dfd22f1e89e809e8bde8b20a --- /dev/null +++ b/framework/src/android/net/IpConfiguration.java @@ -0,0 +1,223 @@ +/* + * 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 android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * A class representing a configured network. + * @hide + */ +@SystemApi +public final class IpConfiguration implements Parcelable { + private static final String TAG = "IpConfiguration"; + + // This enum has been used by apps through reflection for many releases. + // Therefore they can't just be removed. Duplicating these constants to + // give an alternate SystemApi is a worse option than exposing them. + @SuppressLint("Enum") + public enum IpAssignment { + /* Use statically configured IP settings. Configuration can be accessed + * with staticIpConfiguration */ + STATIC, + /* Use dynamically configured IP settings */ + DHCP, + /* no IP details are assigned, this is used to indicate + * that any existing IP settings should be retained */ + UNASSIGNED + } + + /** @hide */ + public IpAssignment ipAssignment; + + /** @hide */ + public StaticIpConfiguration staticIpConfiguration; + + // This enum has been used by apps through reflection for many releases. + // Therefore they can't just be removed. Duplicating these constants to + // give an alternate SystemApi is a worse option than exposing them. + @SuppressLint("Enum") + public enum ProxySettings { + /* No proxy is to be used. Any existing proxy settings + * should be cleared. */ + NONE, + /* Use statically configured proxy. Configuration can be accessed + * with httpProxy. */ + STATIC, + /* no proxy details are assigned, this is used to indicate + * that any existing proxy settings should be retained */ + UNASSIGNED, + /* Use a Pac based proxy. + */ + PAC + } + + /** @hide */ + public ProxySettings proxySettings; + + /** @hide */ + @UnsupportedAppUsage + public ProxyInfo httpProxy; + + private void init(IpAssignment ipAssignment, + ProxySettings proxySettings, + StaticIpConfiguration staticIpConfiguration, + ProxyInfo httpProxy) { + this.ipAssignment = ipAssignment; + this.proxySettings = proxySettings; + this.staticIpConfiguration = (staticIpConfiguration == null) ? + null : new StaticIpConfiguration(staticIpConfiguration); + this.httpProxy = (httpProxy == null) ? + null : new ProxyInfo(httpProxy); + } + + public IpConfiguration() { + init(IpAssignment.UNASSIGNED, ProxySettings.UNASSIGNED, null, null); + } + + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public IpConfiguration(IpAssignment ipAssignment, + ProxySettings proxySettings, + StaticIpConfiguration staticIpConfiguration, + ProxyInfo httpProxy) { + init(ipAssignment, proxySettings, staticIpConfiguration, httpProxy); + } + + public IpConfiguration(@NonNull IpConfiguration source) { + this(); + if (source != null) { + init(source.ipAssignment, source.proxySettings, + source.staticIpConfiguration, source.httpProxy); + } + } + + public @NonNull IpAssignment getIpAssignment() { + return ipAssignment; + } + + public void setIpAssignment(@NonNull IpAssignment ipAssignment) { + this.ipAssignment = ipAssignment; + } + + public @Nullable StaticIpConfiguration getStaticIpConfiguration() { + return staticIpConfiguration; + } + + public void setStaticIpConfiguration(@Nullable StaticIpConfiguration staticIpConfiguration) { + this.staticIpConfiguration = staticIpConfiguration; + } + + public @NonNull ProxySettings getProxySettings() { + return proxySettings; + } + + public void setProxySettings(@NonNull ProxySettings proxySettings) { + this.proxySettings = proxySettings; + } + + public @Nullable ProxyInfo getHttpProxy() { + return httpProxy; + } + + public void setHttpProxy(@Nullable ProxyInfo httpProxy) { + this.httpProxy = httpProxy; + } + + @Override + public String toString() { + StringBuilder sbuf = new StringBuilder(); + sbuf.append("IP assignment: " + ipAssignment.toString()); + sbuf.append("\n"); + if (staticIpConfiguration != null) { + sbuf.append("Static configuration: " + staticIpConfiguration.toString()); + sbuf.append("\n"); + } + sbuf.append("Proxy settings: " + proxySettings.toString()); + sbuf.append("\n"); + if (httpProxy != null) { + sbuf.append("HTTP proxy: " + httpProxy.toString()); + sbuf.append("\n"); + } + + return sbuf.toString(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof IpConfiguration)) { + return false; + } + + IpConfiguration other = (IpConfiguration) o; + return this.ipAssignment == other.ipAssignment && + this.proxySettings == other.proxySettings && + Objects.equals(this.staticIpConfiguration, other.staticIpConfiguration) && + Objects.equals(this.httpProxy, other.httpProxy); + } + + @Override + public int hashCode() { + return 13 + (staticIpConfiguration != null ? staticIpConfiguration.hashCode() : 0) + + 17 * ipAssignment.ordinal() + + 47 * proxySettings.ordinal() + + 83 * httpProxy.hashCode(); + } + + /** Implement the Parcelable interface */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(ipAssignment.name()); + dest.writeString(proxySettings.name()); + dest.writeParcelable(staticIpConfiguration, flags); + dest.writeParcelable(httpProxy, flags); + } + + /** Implement the Parcelable interface */ + public static final @NonNull Creator<IpConfiguration> CREATOR = + new Creator<IpConfiguration>() { + public IpConfiguration createFromParcel(Parcel in) { + IpConfiguration config = new IpConfiguration(); + config.ipAssignment = IpAssignment.valueOf(in.readString()); + config.proxySettings = ProxySettings.valueOf(in.readString()); + config.staticIpConfiguration = in.readParcelable(null); + config.httpProxy = in.readParcelable(null); + return config; + } + + public IpConfiguration[] newArray(int size) { + return new IpConfiguration[size]; + } + }; +} diff --git a/framework/src/android/net/IpPrefix.java b/framework/src/android/net/IpPrefix.java new file mode 100644 index 0000000000000000000000000000000000000000..bf4481afc53dd502c528d3e182dcb258e6a00f03 --- /dev/null +++ b/framework/src/android/net/IpPrefix.java @@ -0,0 +1,303 @@ +/* + * 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 android.net; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; + +import com.android.net.module.util.NetUtils; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Comparator; + +/** + * This class represents an IP prefix, i.e., a contiguous block of IP addresses aligned on a + * power of two boundary (also known as an "IP subnet"). A prefix is specified by two pieces of + * information: + * + * <ul> + * <li>A starting IP address (IPv4 or IPv6). This is the first IP address of the prefix. + * <li>A prefix length. This specifies the length of the prefix by specifing the number of bits + * in the IP address, starting from the most significant bit in network byte order, that + * are constant for all addresses in the prefix. + * </ul> + * + * For example, the prefix <code>192.0.2.0/24</code> covers the 256 IPv4 addresses from + * <code>192.0.2.0</code> to <code>192.0.2.255</code>, inclusive, and the prefix + * <code>2001:db8:1:2</code> covers the 2^64 IPv6 addresses from <code>2001:db8:1:2::</code> to + * <code>2001:db8:1:2:ffff:ffff:ffff:ffff</code>, inclusive. + * + * Objects of this class are immutable. + */ +public final class IpPrefix implements Parcelable { + private final byte[] address; // network byte order + private final int prefixLength; + + private void checkAndMaskAddressAndPrefixLength() { + if (address.length != 4 && address.length != 16) { + throw new IllegalArgumentException( + "IpPrefix has " + address.length + " bytes which is neither 4 nor 16"); + } + NetUtils.maskRawAddress(address, prefixLength); + } + + /** + * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in + * network byte order and a prefix length. Silently truncates the address to the prefix length, + * so for example {@code 192.0.2.1/24} is silently converted to {@code 192.0.2.0/24}. + * + * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long. + * @param prefixLength the prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6). + * + * @hide + */ + public IpPrefix(@NonNull byte[] address, @IntRange(from = 0, to = 128) int prefixLength) { + this.address = address.clone(); + this.prefixLength = prefixLength; + checkAndMaskAddressAndPrefixLength(); + } + + /** + * Constructs a new {@code IpPrefix} from an IPv4 or IPv6 address and a prefix length. Silently + * truncates the address to the prefix length, so for example {@code 192.0.2.1/24} is silently + * converted to {@code 192.0.2.0/24}. + * + * @param address the IP address. Must be non-null. + * @param prefixLength the prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6). + * @hide + */ + @SystemApi + public IpPrefix(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength) { + // We don't reuse the (byte[], int) constructor because it calls clone() on the byte array, + // which is unnecessary because getAddress() already returns a clone. + this.address = address.getAddress(); + this.prefixLength = prefixLength; + checkAndMaskAddressAndPrefixLength(); + } + + /** + * Constructs a new IpPrefix from a string such as "192.0.2.1/24" or "2001:db8::1/64". + * Silently truncates the address to the prefix length, so for example {@code 192.0.2.1/24} + * is silently converted to {@code 192.0.2.0/24}. + * + * @param prefix the prefix to parse + * + * @hide + */ + @SystemApi + public IpPrefix(@NonNull String prefix) { + // We don't reuse the (InetAddress, int) constructor because "error: call to this must be + // first statement in constructor". We could factor out setting the member variables to an + // init() method, but if we did, then we'd have to make the members non-final, or "error: + // cannot assign a value to final variable address". So we just duplicate the code here. + Pair<InetAddress, Integer> ipAndMask = NetworkUtils.legacyParseIpAndMask(prefix); + this.address = ipAndMask.first.getAddress(); + this.prefixLength = ipAndMask.second; + checkAndMaskAddressAndPrefixLength(); + } + + /** + * Compares this {@code IpPrefix} object against the specified object in {@code obj}. Two + * objects are equal if they have the same startAddress and prefixLength. + * + * @param obj the object to be tested for equality. + * @return {@code true} if both objects are equal, {@code false} otherwise. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof IpPrefix)) { + return false; + } + IpPrefix that = (IpPrefix) obj; + return Arrays.equals(this.address, that.address) && this.prefixLength == that.prefixLength; + } + + /** + * Gets the hashcode of the represented IP prefix. + * + * @return the appropriate hashcode value. + */ + @Override + public int hashCode() { + return Arrays.hashCode(address) + 11 * prefixLength; + } + + /** + * Returns a copy of the first IP address in the prefix. Modifying the returned object does not + * change this object's contents. + * + * @return the address in the form of a byte array. + */ + public @NonNull InetAddress getAddress() { + try { + return InetAddress.getByAddress(address); + } catch (UnknownHostException e) { + // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte + // array is the wrong length, but we check that in the constructor. + throw new IllegalArgumentException("Address is invalid"); + } + } + + /** + * Returns a copy of the IP address bytes in network order (the highest order byte is the zeroth + * element). Modifying the returned array does not change this object's contents. + * + * @return the address in the form of a byte array. + */ + public @NonNull byte[] getRawAddress() { + return address.clone(); + } + + /** + * Returns the prefix length of this {@code IpPrefix}. + * + * @return the prefix length. + */ + @IntRange(from = 0, to = 128) + public int getPrefixLength() { + return prefixLength; + } + + /** + * Determines whether the prefix contains the specified address. + * + * @param address An {@link InetAddress} to test. + * @return {@code true} if the prefix covers the given address. {@code false} otherwise. + */ + public boolean contains(@NonNull InetAddress address) { + byte[] addrBytes = address.getAddress(); + if (addrBytes == null || addrBytes.length != this.address.length) { + return false; + } + NetUtils.maskRawAddress(addrBytes, prefixLength); + return Arrays.equals(this.address, addrBytes); + } + + /** + * Returns whether the specified prefix is entirely contained in this prefix. + * + * Note this is mathematical inclusion, so a prefix is always contained within itself. + * @param otherPrefix the prefix to test + * @hide + */ + public boolean containsPrefix(@NonNull IpPrefix otherPrefix) { + if (otherPrefix.getPrefixLength() < prefixLength) return false; + final byte[] otherAddress = otherPrefix.getRawAddress(); + NetUtils.maskRawAddress(otherAddress, prefixLength); + return Arrays.equals(otherAddress, address); + } + + /** + * @hide + */ + public boolean isIPv6() { + return getAddress() instanceof Inet6Address; + } + + /** + * @hide + */ + public boolean isIPv4() { + return getAddress() instanceof Inet4Address; + } + + /** + * Returns a string representation of this {@code IpPrefix}. + * + * @return a string such as {@code "192.0.2.0/24"} or {@code "2001:db8:1:2::/64"}. + */ + public String toString() { + try { + return InetAddress.getByAddress(address).getHostAddress() + "/" + prefixLength; + } catch(UnknownHostException e) { + // Cosmic rays? + throw new IllegalStateException("IpPrefix with invalid address! Shouldn't happen.", e); + } + } + + /** + * Implement the Parcelable interface. + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeByteArray(address); + dest.writeInt(prefixLength); + } + + /** + * Returns a comparator ordering IpPrefixes by length, shorter to longer. + * Contents of the address will break ties. + * @hide + */ + public static Comparator<IpPrefix> lengthComparator() { + return new Comparator<IpPrefix>() { + @Override + public int compare(IpPrefix prefix1, IpPrefix prefix2) { + if (prefix1.isIPv4()) { + if (prefix2.isIPv6()) return -1; + } else { + if (prefix2.isIPv4()) return 1; + } + final int p1len = prefix1.getPrefixLength(); + final int p2len = prefix2.getPrefixLength(); + if (p1len < p2len) return -1; + if (p2len < p1len) return 1; + final byte[] a1 = prefix1.address; + final byte[] a2 = prefix2.address; + final int len = a1.length < a2.length ? a1.length : a2.length; + for (int i = 0; i < len; ++i) { + if (a1[i] < a2[i]) return -1; + if (a1[i] > a2[i]) return 1; + } + if (a2.length < len) return 1; + if (a1.length < len) return -1; + return 0; + } + }; + } + + /** + * Implement the Parcelable interface. + */ + public static final @android.annotation.NonNull Creator<IpPrefix> CREATOR = + new Creator<IpPrefix>() { + public IpPrefix createFromParcel(Parcel in) { + byte[] address = in.createByteArray(); + int prefixLength = in.readInt(); + return new IpPrefix(address, prefixLength); + } + + public IpPrefix[] newArray(int size) { + return new IpPrefix[size]; + } + }; +} diff --git a/framework/src/android/net/KeepalivePacketData.java b/framework/src/android/net/KeepalivePacketData.java new file mode 100644 index 0000000000000000000000000000000000000000..5877f1f4e269760963496c47045484ed4d25158a --- /dev/null +++ b/framework/src/android/net/KeepalivePacketData.java @@ -0,0 +1,119 @@ +/* + * 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; + +import static android.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS; +import static android.net.InvalidPacketException.ERROR_INVALID_PORT; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.util.Log; + +import com.android.net.module.util.IpUtils; + +import java.net.InetAddress; + +/** + * Represents the actual packets that are sent by the + * {@link android.net.SocketKeepalive} API. + * @hide + */ +@SystemApi +public class KeepalivePacketData { + private static final String TAG = "KeepalivePacketData"; + + /** Source IP address */ + @NonNull + private final InetAddress mSrcAddress; + + /** Destination IP address */ + @NonNull + private final InetAddress mDstAddress; + + /** Source port */ + private final int mSrcPort; + + /** Destination port */ + private final int mDstPort; + + /** Packet data. A raw byte string of packet data, not including the link-layer header. */ + private final byte[] mPacket; + + // Note: If you add new fields, please modify the parcelling code in the child classes. + + + // This should only be constructed via static factory methods, such as + // nattKeepalivePacket. + /** + * A holding class for data necessary to build a keepalive packet. + */ + protected KeepalivePacketData(@NonNull InetAddress srcAddress, + @IntRange(from = 0, to = 65535) int srcPort, @NonNull InetAddress dstAddress, + @IntRange(from = 0, to = 65535) int dstPort, + @NonNull byte[] data) throws InvalidPacketException { + this.mSrcAddress = srcAddress; + this.mDstAddress = dstAddress; + this.mSrcPort = srcPort; + this.mDstPort = dstPort; + this.mPacket = data; + + // Check we have two IP addresses of the same family. + if (srcAddress == null || dstAddress == null || !srcAddress.getClass().getName() + .equals(dstAddress.getClass().getName())) { + Log.e(TAG, "Invalid or mismatched InetAddresses in KeepalivePacketData"); + throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS); + } + + // Check the ports. + if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) { + Log.e(TAG, "Invalid ports in KeepalivePacketData"); + throw new InvalidPacketException(ERROR_INVALID_PORT); + } + } + + /** Get source IP address. */ + @NonNull + public InetAddress getSrcAddress() { + return mSrcAddress; + } + + /** Get destination IP address. */ + @NonNull + public InetAddress getDstAddress() { + return mDstAddress; + } + + /** Get source port number. */ + public int getSrcPort() { + return mSrcPort; + } + + /** Get destination port number. */ + public int getDstPort() { + return mDstPort; + } + + /** + * Returns a byte array of the given packet data. + */ + @NonNull + public byte[] getPacket() { + return mPacket.clone(); + } + +} diff --git a/framework/src/android/net/LinkAddress.java b/framework/src/android/net/LinkAddress.java new file mode 100644 index 0000000000000000000000000000000000000000..d48b8c71f495f7985f10d4f17b2e2d1d04599642 --- /dev/null +++ b/framework/src/android/net/LinkAddress.java @@ -0,0 +1,549 @@ +/* + * 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; + +import static android.system.OsConstants.IFA_F_DADFAILED; +import static android.system.OsConstants.IFA_F_DEPRECATED; +import static android.system.OsConstants.IFA_F_OPTIMISTIC; +import static android.system.OsConstants.IFA_F_PERMANENT; +import static android.system.OsConstants.IFA_F_TENTATIVE; +import static android.system.OsConstants.RT_SCOPE_HOST; +import static android.system.OsConstants.RT_SCOPE_LINK; +import static android.system.OsConstants.RT_SCOPE_SITE; +import static android.system.OsConstants.RT_SCOPE_UNIVERSE; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.Pair; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +/** + * Identifies an IP address on a network link. + * + * A {@code LinkAddress} consists of: + * <ul> + * <li>An IP address and prefix length (e.g., {@code 2001:db8::1/64} or {@code 192.0.2.1/24}). + * The address must be unicast, as multicast addresses cannot be assigned to interfaces. + * <li>Address flags: A bitmask of {@code OsConstants.IFA_F_*} values representing properties + * of the address (e.g., {@code android.system.OsConstants.IFA_F_OPTIMISTIC}). + * <li>Address scope: One of the {@code OsConstants.IFA_F_*} values; defines the scope in which + * the address is unique (e.g., + * {@code android.system.OsConstants.RT_SCOPE_LINK} or + * {@code android.system.OsConstants.RT_SCOPE_UNIVERSE}). + * </ul> + */ +public class LinkAddress implements Parcelable { + + /** + * Indicates the deprecation or expiration time is unknown + * @hide + */ + @SystemApi + public static final long LIFETIME_UNKNOWN = -1; + + /** + * Indicates this address is permanent. + * @hide + */ + @SystemApi + public static final long LIFETIME_PERMANENT = Long.MAX_VALUE; + + /** + * IPv4 or IPv6 address. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private InetAddress address; + + /** + * Prefix length. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private int prefixLength; + + /** + * Address flags. A bitmask of {@code IFA_F_*} values. Note that {@link #getFlags()} may not + * return these exact values. For example, it may set or clear the {@code IFA_F_DEPRECATED} + * flag depending on the current preferred lifetime. + */ + private int flags; + + /** + * Address scope. One of the RT_SCOPE_* constants. + */ + private int scope; + + /** + * The time, as reported by {@link SystemClock#elapsedRealtime}, when this LinkAddress will be + * or was deprecated. At the time existing connections can still use this address until it + * expires, but new connections should use the new address. {@link #LIFETIME_UNKNOWN} indicates + * this information is not available. {@link #LIFETIME_PERMANENT} indicates this + * {@link LinkAddress} will never be deprecated. + */ + private long deprecationTime; + + /** + * The time, as reported by {@link SystemClock#elapsedRealtime}, when this {@link LinkAddress} + * will expire and be removed from the interface. {@link #LIFETIME_UNKNOWN} indicates this + * information is not available. {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} + * will never expire. + */ + private long expirationTime; + + /** + * Utility function to determines the scope of a unicast address. Per RFC 4291 section 2.5 and + * RFC 6724 section 3.2. + * @hide + */ + private static int scopeForUnicastAddress(InetAddress addr) { + if (addr.isAnyLocalAddress()) { + return RT_SCOPE_HOST; + } + + if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) { + return RT_SCOPE_LINK; + } + + // isSiteLocalAddress() returns true for private IPv4 addresses, but RFC 6724 section 3.2 + // says that they are assigned global scope. + if (!(addr instanceof Inet4Address) && addr.isSiteLocalAddress()) { + return RT_SCOPE_SITE; + } + + return RT_SCOPE_UNIVERSE; + } + + /** + * Utility function to check if |address| is a Unique Local IPv6 Unicast Address + * (a.k.a. "ULA"; RFC 4193). + * + * Per RFC 4193 section 8, fc00::/7 identifies these addresses. + */ + private boolean isIpv6ULA() { + if (isIpv6()) { + byte[] bytes = address.getAddress(); + return ((bytes[0] & (byte)0xfe) == (byte)0xfc); + } + return false; + } + + /** + * @return true if the address is IPv6. + * @hide + */ + @SystemApi + public boolean isIpv6() { + return address instanceof Inet6Address; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return true if the address is IPv6. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean isIPv6() { + return isIpv6(); + } + + /** + * @return true if the address is IPv4 or is a mapped IPv4 address. + * @hide + */ + @SystemApi + public boolean isIpv4() { + return address instanceof Inet4Address; + } + + /** + * Utility function for the constructors. + */ + private void init(InetAddress address, int prefixLength, int flags, int scope, + long deprecationTime, long expirationTime) { + if (address == null || + address.isMulticastAddress() || + prefixLength < 0 || + (address instanceof Inet4Address && prefixLength > 32) || + (prefixLength > 128)) { + throw new IllegalArgumentException("Bad LinkAddress params " + address + + "/" + prefixLength); + } + + // deprecation time and expiration time must be both provided, or neither. + if ((deprecationTime == LIFETIME_UNKNOWN) != (expirationTime == LIFETIME_UNKNOWN)) { + throw new IllegalArgumentException( + "Must not specify only one of deprecation time and expiration time"); + } + + // deprecation time needs to be a positive value. + if (deprecationTime != LIFETIME_UNKNOWN && deprecationTime < 0) { + throw new IllegalArgumentException("invalid deprecation time " + deprecationTime); + } + + // expiration time needs to be a positive value. + if (expirationTime != LIFETIME_UNKNOWN && expirationTime < 0) { + throw new IllegalArgumentException("invalid expiration time " + expirationTime); + } + + // expiration time can't be earlier than deprecation time + if (deprecationTime != LIFETIME_UNKNOWN && expirationTime != LIFETIME_UNKNOWN + && expirationTime < deprecationTime) { + throw new IllegalArgumentException("expiration earlier than deprecation (" + + deprecationTime + ", " + expirationTime + ")"); + } + + this.address = address; + this.prefixLength = prefixLength; + this.flags = flags; + this.scope = scope; + this.deprecationTime = deprecationTime; + this.expirationTime = expirationTime; + } + + /** + * Constructs a new {@code LinkAddress} from an {@code InetAddress} and prefix length, with + * the specified flags and scope. Flags and scope are not checked for validity. + * + * @param address The IP address. + * @param prefixLength The prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6). + * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address. + * @param scope An integer defining the scope in which the address is unique (e.g., + * {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}). + * @hide + */ + @SystemApi + public LinkAddress(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength, + int flags, int scope) { + init(address, prefixLength, flags, scope, LIFETIME_UNKNOWN, LIFETIME_UNKNOWN); + } + + /** + * Constructs a new {@code LinkAddress} from an {@code InetAddress}, prefix length, with + * the specified flags, scope, deprecation time, and expiration time. Flags and scope are not + * checked for validity. The value of the {@code IFA_F_DEPRECATED} and {@code IFA_F_PERMANENT} + * flag will be adjusted based on the passed-in lifetimes. + * + * @param address The IP address. + * @param prefixLength The prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6). + * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address. + * @param scope An integer defining the scope in which the address is unique (e.g., + * {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}). + * @param deprecationTime The time, as reported by {@link SystemClock#elapsedRealtime}, when + * this {@link LinkAddress} will be or was deprecated. At the time + * existing connections can still use this address until it expires, but + * new connections should use the new address. {@link #LIFETIME_UNKNOWN} + * indicates this information is not available. + * {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} will + * never be deprecated. + * @param expirationTime The time, as reported by {@link SystemClock#elapsedRealtime}, when this + * {@link LinkAddress} will expire and be removed from the interface. + * {@link #LIFETIME_UNKNOWN} indicates this information is not available. + * {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} will + * never expire. + * @hide + */ + @SystemApi + public LinkAddress(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength, + int flags, int scope, long deprecationTime, long expirationTime) { + init(address, prefixLength, flags, scope, deprecationTime, expirationTime); + } + + /** + * Constructs a new {@code LinkAddress} from an {@code InetAddress} and a prefix length. + * The flags are set to zero and the scope is determined from the address. + * @param address The IP address. + * @param prefixLength The prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6). + * @hide + */ + @SystemApi + public LinkAddress(@NonNull InetAddress address, + @IntRange(from = 0, to = 128) int prefixLength) { + this(address, prefixLength, 0, 0); + this.scope = scopeForUnicastAddress(address); + } + + /** + * Constructs a new {@code LinkAddress} from an {@code InterfaceAddress}. + * The flags are set to zero and the scope is determined from the address. + * @param interfaceAddress The interface address. + * @hide + */ + public LinkAddress(@NonNull InterfaceAddress interfaceAddress) { + this(interfaceAddress.getAddress(), + interfaceAddress.getNetworkPrefixLength()); + } + + /** + * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or + * "2001:db8::1/64". The flags are set to zero and the scope is determined from the address. + * @param address The string to parse. + * @hide + */ + @SystemApi + public LinkAddress(@NonNull String address) { + this(address, 0, 0); + this.scope = scopeForUnicastAddress(this.address); + } + + /** + * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or + * "2001:db8::1/64", with the specified flags and scope. + * @param address The string to parse. + * @param flags The address flags. + * @param scope The address scope. + * @hide + */ + @SystemApi + public LinkAddress(@NonNull String address, int flags, int scope) { + // This may throw an IllegalArgumentException; catching it is the caller's responsibility. + // TODO: consider rejecting mapped IPv4 addresses such as "::ffff:192.0.2.5/24". + Pair<InetAddress, Integer> ipAndMask = NetworkUtils.legacyParseIpAndMask(address); + init(ipAndMask.first, ipAndMask.second, flags, scope, LIFETIME_UNKNOWN, LIFETIME_UNKNOWN); + } + + /** + * Returns a string representation of this address, such as "192.0.2.1/24" or "2001:db8::1/64". + * The string representation does not contain the flags and scope, just the address and prefix + * length. + */ + @Override + public String toString() { + return address.getHostAddress() + "/" + prefixLength; + } + + /** + * Compares this {@code LinkAddress} instance against {@code obj}. Two addresses are equal if + * their address, prefix length, flags and scope are equal. Thus, for example, two addresses + * that have the same address and prefix length are not equal if one of them is deprecated and + * the other is not. + * + * @param obj the object to be tested for equality. + * @return {@code true} if both objects are equal, {@code false} otherwise. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof LinkAddress)) { + return false; + } + LinkAddress linkAddress = (LinkAddress) obj; + return this.address.equals(linkAddress.address) + && this.prefixLength == linkAddress.prefixLength + && this.flags == linkAddress.flags + && this.scope == linkAddress.scope + && this.deprecationTime == linkAddress.deprecationTime + && this.expirationTime == linkAddress.expirationTime; + } + + /** + * Returns a hashcode for this address. + */ + @Override + public int hashCode() { + return Objects.hash(address, prefixLength, flags, scope, deprecationTime, expirationTime); + } + + /** + * Determines whether this {@code LinkAddress} and the provided {@code LinkAddress} + * represent the same address. Two {@code LinkAddresses} represent the same address + * if they have the same IP address and prefix length, even if their properties are + * different. + * + * @param other the {@code LinkAddress} to compare to. + * @return {@code true} if both objects have the same address and prefix length, {@code false} + * otherwise. + * @hide + */ + @SystemApi + public boolean isSameAddressAs(@Nullable LinkAddress other) { + if (other == null) { + return false; + } + return address.equals(other.address) && prefixLength == other.prefixLength; + } + + /** + * Returns the {@link InetAddress} of this {@code LinkAddress}. + */ + public InetAddress getAddress() { + return address; + } + + /** + * Returns the prefix length of this {@code LinkAddress}. + */ + @IntRange(from = 0, to = 128) + public int getPrefixLength() { + return prefixLength; + } + + /** + * Returns the prefix length of this {@code LinkAddress}. + * TODO: Delete all callers and remove in favour of getPrefixLength(). + * @hide + */ + @UnsupportedAppUsage + @IntRange(from = 0, to = 128) + public int getNetworkPrefixLength() { + return getPrefixLength(); + } + + /** + * Returns the flags of this {@code LinkAddress}. + */ + public int getFlags() { + int flags = this.flags; + if (deprecationTime != LIFETIME_UNKNOWN) { + if (SystemClock.elapsedRealtime() >= deprecationTime) { + flags |= IFA_F_DEPRECATED; + } else { + // If deprecation time is in the future, or permanent. + flags &= ~IFA_F_DEPRECATED; + } + } + + if (expirationTime == LIFETIME_PERMANENT) { + flags |= IFA_F_PERMANENT; + } else if (expirationTime != LIFETIME_UNKNOWN) { + // If we know this address expired or will expire in the future, then this address + // should not be permanent. + flags &= ~IFA_F_PERMANENT; + } + + // Do no touch the original flags. Return the adjusted flags here. + return flags; + } + + /** + * Returns the scope of this {@code LinkAddress}. + */ + public int getScope() { + return scope; + } + + /** + * Get the deprecation time, as reported by {@link SystemClock#elapsedRealtime}, when this + * {@link LinkAddress} will be or was deprecated. At the time existing connections can still use + * this address until it expires, but new connections should use the new address. + * + * @return The deprecation time in milliseconds. {@link #LIFETIME_UNKNOWN} indicates this + * information is not available. {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} + * will never be deprecated. + * + * @hide + */ + @SystemApi + public long getDeprecationTime() { + return deprecationTime; + } + + /** + * Get the expiration time, as reported by {@link SystemClock#elapsedRealtime}, when this + * {@link LinkAddress} will expire and be removed from the interface. + * + * @return The expiration time in milliseconds. {@link #LIFETIME_UNKNOWN} indicates this + * information is not available. {@link #LIFETIME_PERMANENT} indicates this {@link LinkAddress} + * will never expire. + * + * @hide + */ + @SystemApi + public long getExpirationTime() { + return expirationTime; + } + + /** + * Returns true if this {@code LinkAddress} is global scope and preferred (i.e., not currently + * deprecated). + * + * @hide + */ + @SystemApi + public boolean isGlobalPreferred() { + /** + * Note that addresses flagged as IFA_F_OPTIMISTIC are + * simultaneously flagged as IFA_F_TENTATIVE (when the tentative + * state has cleared either DAD has succeeded or failed, and both + * flags are cleared regardless). + */ + int flags = getFlags(); + return (scope == RT_SCOPE_UNIVERSE + && !isIpv6ULA() + && (flags & (IFA_F_DADFAILED | IFA_F_DEPRECATED)) == 0L + && ((flags & IFA_F_TENTATIVE) == 0L || (flags & IFA_F_OPTIMISTIC) != 0L)); + } + + /** + * Implement the Parcelable interface. + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeByteArray(address.getAddress()); + dest.writeInt(prefixLength); + dest.writeInt(this.flags); + dest.writeInt(scope); + dest.writeLong(deprecationTime); + dest.writeLong(expirationTime); + } + + /** + * Implement the Parcelable interface. + */ + public static final @android.annotation.NonNull Creator<LinkAddress> CREATOR = + new Creator<LinkAddress>() { + public LinkAddress createFromParcel(Parcel in) { + InetAddress address = null; + try { + address = InetAddress.getByAddress(in.createByteArray()); + } catch (UnknownHostException e) { + // Nothing we can do here. When we call the constructor, we'll throw an + // IllegalArgumentException, because a LinkAddress can't have a null + // InetAddress. + } + int prefixLength = in.readInt(); + int flags = in.readInt(); + int scope = in.readInt(); + long deprecationTime = in.readLong(); + long expirationTime = in.readLong(); + return new LinkAddress(address, prefixLength, flags, scope, deprecationTime, + expirationTime); + } + + public LinkAddress[] newArray(int size) { + return new LinkAddress[size]; + } + }; +} diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..99f48b49c6b5910a1acfac3669d2f395b9c73bfd --- /dev/null +++ b/framework/src/android/net/LinkProperties.java @@ -0,0 +1,1823 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import com.android.net.module.util.LinkPropertiesUtils; + +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.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +/** + * Describes the properties of a network link. + * + * A link represents a connection to a network. + * It may have multiple addresses and multiple gateways, + * multiple dns servers but only one http proxy and one + * network interface. + * + * Note that this is just a holder of data. Modifying it + * does not affect live networks. + * + */ +public final class LinkProperties implements Parcelable { + // The interface described by the network link. + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private String mIfaceName; + private final ArrayList<LinkAddress> mLinkAddresses = new ArrayList<>(); + private final ArrayList<InetAddress> mDnses = new ArrayList<>(); + // PCSCF addresses are addresses of SIP proxies that only exist for the IMS core service. + private final ArrayList<InetAddress> mPcscfs = new ArrayList<InetAddress>(); + private final ArrayList<InetAddress> mValidatedPrivateDnses = new ArrayList<>(); + private boolean mUsePrivateDns; + private String mPrivateDnsServerName; + private String mDomains; + private ArrayList<RouteInfo> mRoutes = new ArrayList<>(); + private Inet4Address mDhcpServerAddress; + private ProxyInfo mHttpProxy; + private int mMtu; + // in the format "rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max" + private String mTcpBufferSizes; + private IpPrefix mNat64Prefix; + private boolean mWakeOnLanSupported; + private Uri mCaptivePortalApiUrl; + private CaptivePortalData mCaptivePortalData; + + /** + * Indicates whether parceling should preserve fields that are set based on permissions of + * the process receiving the {@link LinkProperties}. + */ + private final transient boolean mParcelSensitiveFields; + + private static final int MIN_MTU = 68; + + private static final int MIN_MTU_V6 = 1280; + + private static final int MAX_MTU = 10000; + + private static final int INET6_ADDR_LENGTH = 16; + + // Stores the properties of links that are "stacked" above this link. + // Indexed by interface name to allow modification and to prevent duplicates being added. + private Hashtable<String, LinkProperties> mStackedLinks = new Hashtable<>(); + + /** + * @hide + */ + @UnsupportedAppUsage(implicitMember = + "values()[Landroid/net/LinkProperties$ProvisioningChange;") + public enum ProvisioningChange { + @UnsupportedAppUsage + STILL_NOT_PROVISIONED, + @UnsupportedAppUsage + LOST_PROVISIONING, + @UnsupportedAppUsage + GAINED_PROVISIONING, + @UnsupportedAppUsage + STILL_PROVISIONED, + } + + /** + * Compare the provisioning states of two LinkProperties instances. + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static ProvisioningChange compareProvisioning( + LinkProperties before, LinkProperties after) { + if (before.isProvisioned() && after.isProvisioned()) { + // On dual-stack networks, DHCPv4 renewals can occasionally fail. + // When this happens, IPv6-reachable services continue to function + // normally but IPv4-only services (naturally) fail. + // + // When an application using an IPv4-only service reports a bad + // network condition to the framework, attempts to re-validate + // the network succeed (since we support IPv6-only networks) and + // nothing is changed. + // + // For users, this is confusing and unexpected behaviour, and is + // not necessarily easy to diagnose. Therefore, we treat changing + // from a dual-stack network to an IPv6-only network equivalent to + // a total loss of provisioning. + // + // For one such example of this, see b/18867306. + // + // Additionally, losing IPv6 provisioning can result in TCP + // connections getting stuck until timeouts fire and other + // baffling failures. Therefore, loss of either IPv4 or IPv6 on a + // previously dual-stack network is deemed a lost of provisioning. + if ((before.isIpv4Provisioned() && !after.isIpv4Provisioned()) + || (before.isIpv6Provisioned() && !after.isIpv6Provisioned())) { + return ProvisioningChange.LOST_PROVISIONING; + } + return ProvisioningChange.STILL_PROVISIONED; + } else if (before.isProvisioned() && !after.isProvisioned()) { + return ProvisioningChange.LOST_PROVISIONING; + } else if (!before.isProvisioned() && after.isProvisioned()) { + return ProvisioningChange.GAINED_PROVISIONING; + } else { // !before.isProvisioned() && !after.isProvisioned() + return ProvisioningChange.STILL_NOT_PROVISIONED; + } + } + + /** + * Constructs a new {@code LinkProperties} with default values. + */ + public LinkProperties() { + mParcelSensitiveFields = false; + } + + /** + * @hide + */ + @SystemApi + public LinkProperties(@Nullable LinkProperties source) { + this(source, false /* parcelSensitiveFields */); + } + + /** + * Create a copy of a {@link LinkProperties} that may preserve fields that were set + * based on the permissions of the process that originally received it. + * + * <p>By default {@link LinkProperties} does not preserve such fields during parceling, as + * they should not be shared outside of the process that receives them without appropriate + * checks. + * @param parcelSensitiveFields Whether the sensitive fields should be kept when parceling + * @hide + */ + @SystemApi + public LinkProperties(@Nullable LinkProperties source, boolean parcelSensitiveFields) { + mParcelSensitiveFields = parcelSensitiveFields; + if (source == null) return; + mIfaceName = source.mIfaceName; + mLinkAddresses.addAll(source.mLinkAddresses); + mDnses.addAll(source.mDnses); + mValidatedPrivateDnses.addAll(source.mValidatedPrivateDnses); + mUsePrivateDns = source.mUsePrivateDns; + mPrivateDnsServerName = source.mPrivateDnsServerName; + mPcscfs.addAll(source.mPcscfs); + mDomains = source.mDomains; + mRoutes.addAll(source.mRoutes); + mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy); + for (LinkProperties l: source.mStackedLinks.values()) { + addStackedLink(l); + } + setMtu(source.mMtu); + setDhcpServerAddress(source.getDhcpServerAddress()); + mTcpBufferSizes = source.mTcpBufferSizes; + mNat64Prefix = source.mNat64Prefix; + mWakeOnLanSupported = source.mWakeOnLanSupported; + mCaptivePortalApiUrl = source.mCaptivePortalApiUrl; + mCaptivePortalData = source.mCaptivePortalData; + } + + /** + * Sets the interface name for this link. All {@link RouteInfo} already set for this + * will have their interface changed to match this new value. + * + * @param iface The name of the network interface used for this link. + */ + public void setInterfaceName(@Nullable String iface) { + mIfaceName = iface; + ArrayList<RouteInfo> newRoutes = new ArrayList<>(mRoutes.size()); + for (RouteInfo route : mRoutes) { + newRoutes.add(routeWithInterface(route)); + } + mRoutes = newRoutes; + } + + /** + * Gets the interface name for this link. May be {@code null} if not set. + * + * @return The interface name set for this link or {@code null}. + */ + public @Nullable String getInterfaceName() { + return mIfaceName; + } + + /** + * @hide + */ + @SystemApi + public @NonNull List<String> getAllInterfaceNames() { + List<String> interfaceNames = new ArrayList<>(mStackedLinks.size() + 1); + if (mIfaceName != null) interfaceNames.add(mIfaceName); + for (LinkProperties stacked: mStackedLinks.values()) { + interfaceNames.addAll(stacked.getAllInterfaceNames()); + } + return interfaceNames; + } + + /** + * Returns all the addresses on this link. We often think of a link having a single address, + * however, particularly with Ipv6 several addresses are typical. Note that the + * {@code LinkProperties} actually contains {@link LinkAddress} objects which also include + * prefix lengths for each address. This is a simplified utility alternative to + * {@link LinkProperties#getLinkAddresses}. + * + * @return An unmodifiable {@link List} of {@link InetAddress} for this link. + * @hide + */ + @SystemApi + public @NonNull List<InetAddress> getAddresses() { + final List<InetAddress> addresses = new ArrayList<>(); + for (LinkAddress linkAddress : mLinkAddresses) { + addresses.add(linkAddress.getAddress()); + } + return Collections.unmodifiableList(addresses); + } + + /** + * Returns all the addresses on this link and all the links stacked above it. + * @hide + */ + @UnsupportedAppUsage + public @NonNull List<InetAddress> getAllAddresses() { + List<InetAddress> addresses = new ArrayList<>(); + for (LinkAddress linkAddress : mLinkAddresses) { + addresses.add(linkAddress.getAddress()); + } + for (LinkProperties stacked: mStackedLinks.values()) { + addresses.addAll(stacked.getAllAddresses()); + } + return addresses; + } + + private int findLinkAddressIndex(LinkAddress address) { + for (int i = 0; i < mLinkAddresses.size(); i++) { + if (mLinkAddresses.get(i).isSameAddressAs(address)) { + return i; + } + } + return -1; + } + + /** + * Adds a {@link LinkAddress} to this {@code LinkProperties} if a {@link LinkAddress} of the + * same address/prefix does not already exist. If it does exist it is replaced. + * @param address The {@code LinkAddress} to add. + * @return true if {@code address} was added or updated, false otherwise. + * @hide + */ + @SystemApi + public boolean addLinkAddress(@NonNull LinkAddress address) { + if (address == null) { + return false; + } + int i = findLinkAddressIndex(address); + if (i < 0) { + // Address was not present. Add it. + mLinkAddresses.add(address); + return true; + } else if (mLinkAddresses.get(i).equals(address)) { + // Address was present and has same properties. Do nothing. + return false; + } else { + // Address was present and has different properties. Update it. + mLinkAddresses.set(i, address); + return true; + } + } + + /** + * Removes a {@link LinkAddress} from this {@code LinkProperties}. Specifically, matches + * and {@link LinkAddress} with the same address and prefix. + * + * @param toRemove A {@link LinkAddress} specifying the address to remove. + * @return true if the address was removed, false if it did not exist. + * @hide + */ + @SystemApi + public boolean removeLinkAddress(@NonNull LinkAddress toRemove) { + int i = findLinkAddressIndex(toRemove); + if (i >= 0) { + mLinkAddresses.remove(i); + return true; + } + return false; + } + + /** + * Returns all the {@link LinkAddress} on this link. Typically a link will have + * one IPv4 address and one or more IPv6 addresses. + * + * @return An unmodifiable {@link List} of {@link LinkAddress} for this link. + */ + public @NonNull List<LinkAddress> getLinkAddresses() { + return Collections.unmodifiableList(mLinkAddresses); + } + + /** + * Returns all the addresses on this link and all the links stacked above it. + * @hide + */ + @SystemApi + public @NonNull List<LinkAddress> getAllLinkAddresses() { + List<LinkAddress> addresses = new ArrayList<>(mLinkAddresses); + for (LinkProperties stacked: mStackedLinks.values()) { + addresses.addAll(stacked.getAllLinkAddresses()); + } + return addresses; + } + + /** + * Replaces the {@link LinkAddress} in this {@code LinkProperties} with + * the given {@link Collection} of {@link LinkAddress}. + * + * @param addresses The {@link Collection} of {@link LinkAddress} to set in this + * object. + */ + public void setLinkAddresses(@NonNull Collection<LinkAddress> addresses) { + mLinkAddresses.clear(); + for (LinkAddress address: addresses) { + addLinkAddress(address); + } + } + + /** + * Adds the given {@link InetAddress} to the list of DNS servers, if not present. + * + * @param dnsServer The {@link InetAddress} to add to the list of DNS servers. + * @return true if the DNS server was added, false if it was already present. + * @hide + */ + @SystemApi + public boolean addDnsServer(@NonNull InetAddress dnsServer) { + if (dnsServer != null && !mDnses.contains(dnsServer)) { + mDnses.add(dnsServer); + return true; + } + return false; + } + + /** + * Removes the given {@link InetAddress} from the list of DNS servers. + * + * @param dnsServer The {@link InetAddress} to remove from the list of DNS servers. + * @return true if the DNS server was removed, false if it did not exist. + * @hide + */ + @SystemApi + public boolean removeDnsServer(@NonNull InetAddress dnsServer) { + return mDnses.remove(dnsServer); + } + + /** + * Replaces the DNS servers in this {@code LinkProperties} with + * the given {@link Collection} of {@link InetAddress} objects. + * + * @param dnsServers The {@link Collection} of DNS servers to set in this object. + */ + public void setDnsServers(@NonNull Collection<InetAddress> dnsServers) { + mDnses.clear(); + for (InetAddress dnsServer: dnsServers) { + addDnsServer(dnsServer); + } + } + + /** + * Returns all the {@link InetAddress} for DNS servers on this link. + * + * @return An unmodifiable {@link List} of {@link InetAddress} for DNS servers on + * this link. + */ + public @NonNull List<InetAddress> getDnsServers() { + return Collections.unmodifiableList(mDnses); + } + + /** + * Set whether private DNS is currently in use on this network. + * + * @param usePrivateDns The private DNS state. + * @hide + */ + @SystemApi + public void setUsePrivateDns(boolean usePrivateDns) { + mUsePrivateDns = usePrivateDns; + } + + /** + * Returns whether private DNS is currently in use on this network. When + * private DNS is in use, applications must not send unencrypted DNS + * queries as doing so could reveal private user information. Furthermore, + * if private DNS is in use and {@link #getPrivateDnsServerName} is not + * {@code null}, DNS queries must be sent to the specified DNS server. + * + * @return {@code true} if private DNS is in use, {@code false} otherwise. + */ + public boolean isPrivateDnsActive() { + return mUsePrivateDns; + } + + /** + * Set the name of the private DNS server to which private DNS queries + * should be sent when in strict mode. This value should be {@code null} + * when private DNS is off or in opportunistic mode. + * + * @param privateDnsServerName The private DNS server name. + * @hide + */ + @SystemApi + public void setPrivateDnsServerName(@Nullable String privateDnsServerName) { + mPrivateDnsServerName = privateDnsServerName; + } + + /** + * Set DHCP server address. + * + * @param serverAddress the server address to set. + */ + public void setDhcpServerAddress(@Nullable Inet4Address serverAddress) { + mDhcpServerAddress = serverAddress; + } + + /** + * Get DHCP server address + * + * @return The current DHCP server address. + */ + public @Nullable Inet4Address getDhcpServerAddress() { + return mDhcpServerAddress; + } + + /** + * Returns the private DNS server name that is in use. If not {@code null}, + * private DNS is in strict mode. In this mode, applications should ensure + * that all DNS queries are encrypted and sent to this hostname and that + * queries are only sent if the hostname's certificate is valid. If + * {@code null} and {@link #isPrivateDnsActive} is {@code true}, private + * DNS is in opportunistic mode, and applications should ensure that DNS + * queries are encrypted and sent to a DNS server returned by + * {@link #getDnsServers}. System DNS will handle each of these cases + * correctly, but applications implementing their own DNS lookups must make + * sure to follow these requirements. + * + * @return The private DNS server name. + */ + public @Nullable String getPrivateDnsServerName() { + return mPrivateDnsServerName; + } + + /** + * Adds the given {@link InetAddress} to the list of validated private DNS servers, + * if not present. This is distinct from the server name in that these are actually + * resolved addresses. + * + * @param dnsServer The {@link InetAddress} to add to the list of validated private DNS servers. + * @return true if the DNS server was added, false if it was already present. + * @hide + */ + public boolean addValidatedPrivateDnsServer(@NonNull InetAddress dnsServer) { + if (dnsServer != null && !mValidatedPrivateDnses.contains(dnsServer)) { + mValidatedPrivateDnses.add(dnsServer); + return true; + } + return false; + } + + /** + * Removes the given {@link InetAddress} from the list of validated private DNS servers. + * + * @param dnsServer The {@link InetAddress} to remove from the list of validated private DNS + * servers. + * @return true if the DNS server was removed, false if it did not exist. + * @hide + */ + public boolean removeValidatedPrivateDnsServer(@NonNull InetAddress dnsServer) { + return mValidatedPrivateDnses.remove(dnsServer); + } + + /** + * Replaces the validated private DNS servers in this {@code LinkProperties} with + * the given {@link Collection} of {@link InetAddress} objects. + * + * @param dnsServers The {@link Collection} of validated private DNS servers to set in this + * object. + * @hide + */ + @SystemApi + public void setValidatedPrivateDnsServers(@NonNull Collection<InetAddress> dnsServers) { + mValidatedPrivateDnses.clear(); + for (InetAddress dnsServer: dnsServers) { + addValidatedPrivateDnsServer(dnsServer); + } + } + + /** + * Returns all the {@link InetAddress} for validated private DNS servers on this link. + * These are resolved from the private DNS server name. + * + * @return An unmodifiable {@link List} of {@link InetAddress} for validated private + * DNS servers on this link. + * @hide + */ + @SystemApi + public @NonNull List<InetAddress> getValidatedPrivateDnsServers() { + return Collections.unmodifiableList(mValidatedPrivateDnses); + } + + /** + * Adds the given {@link InetAddress} to the list of PCSCF servers, if not present. + * + * @param pcscfServer The {@link InetAddress} to add to the list of PCSCF servers. + * @return true if the PCSCF server was added, false otherwise. + * @hide + */ + @SystemApi + public boolean addPcscfServer(@NonNull InetAddress pcscfServer) { + if (pcscfServer != null && !mPcscfs.contains(pcscfServer)) { + mPcscfs.add(pcscfServer); + return true; + } + return false; + } + + /** + * Removes the given {@link InetAddress} from the list of PCSCF servers. + * + * @param pcscfServer The {@link InetAddress} to remove from the list of PCSCF servers. + * @return true if the PCSCF server was removed, false otherwise. + * @hide + */ + public boolean removePcscfServer(@NonNull InetAddress pcscfServer) { + return mPcscfs.remove(pcscfServer); + } + + /** + * Replaces the PCSCF servers in this {@code LinkProperties} with + * the given {@link Collection} of {@link InetAddress} objects. + * + * @param pcscfServers The {@link Collection} of PCSCF servers to set in this object. + * @hide + */ + @SystemApi + public void setPcscfServers(@NonNull Collection<InetAddress> pcscfServers) { + mPcscfs.clear(); + for (InetAddress pcscfServer: pcscfServers) { + addPcscfServer(pcscfServer); + } + } + + /** + * Returns all the {@link InetAddress} for PCSCF servers on this link. + * + * @return An unmodifiable {@link List} of {@link InetAddress} for PCSCF servers on + * this link. + * @hide + */ + @SystemApi + public @NonNull List<InetAddress> getPcscfServers() { + return Collections.unmodifiableList(mPcscfs); + } + + /** + * Sets the DNS domain search path used on this link. + * + * @param domains A {@link String} listing in priority order the comma separated + * domains to search when resolving host names on this link. + */ + public void setDomains(@Nullable String domains) { + mDomains = domains; + } + + /** + * Get the DNS domains search path set for this link. May be {@code null} if not set. + * + * @return A {@link String} containing the comma separated domains to search when resolving host + * names on this link or {@code null}. + */ + public @Nullable String getDomains() { + return mDomains; + } + + /** + * Sets the Maximum Transmission Unit size to use on this link. This should not be used + * unless the system default (1500) is incorrect. Values less than 68 or greater than + * 10000 will be ignored. + * + * @param mtu The MTU to use for this link. + */ + public void setMtu(int mtu) { + mMtu = mtu; + } + + /** + * Gets any non-default MTU size set for this link. Note that if the default is being used + * this will return 0. + * + * @return The mtu value set for this link. + */ + public int getMtu() { + return mMtu; + } + + /** + * Sets the tcp buffers sizes to be used when this link is the system default. + * Should be of the form "rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max". + * + * @param tcpBufferSizes The tcp buffers sizes to use. + * + * @hide + */ + @SystemApi + public void setTcpBufferSizes(@Nullable String tcpBufferSizes) { + mTcpBufferSizes = tcpBufferSizes; + } + + /** + * Gets the tcp buffer sizes. May be {@code null} if not set. + * + * @return the tcp buffer sizes to use when this link is the system default or {@code null}. + * + * @hide + */ + @SystemApi + public @Nullable String getTcpBufferSizes() { + return mTcpBufferSizes; + } + + private RouteInfo routeWithInterface(RouteInfo route) { + return new RouteInfo( + route.getDestination(), + route.getGateway(), + mIfaceName, + route.getType(), + route.getMtu()); + } + + private int findRouteIndexByRouteKey(RouteInfo route) { + for (int i = 0; i < mRoutes.size(); i++) { + if (mRoutes.get(i).getRouteKey().equals(route.getRouteKey())) { + return i; + } + } + return -1; + } + + /** + * Adds a {@link RouteInfo} to this {@code LinkProperties}. If there is a {@link RouteInfo} + * with the same destination, gateway and interface with different properties + * (e.g., different MTU), it will be updated. If the {@link RouteInfo} had an + * interface name set and that differs from the interface set for this + * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown. + * The proper course is to add either un-named or properly named {@link RouteInfo}. + * + * @param route A {@link RouteInfo} to add to this object. + * @return {@code true} was added or updated, false otherwise. + */ + public boolean addRoute(@NonNull RouteInfo route) { + String routeIface = route.getInterface(); + if (routeIface != null && !routeIface.equals(mIfaceName)) { + throw new IllegalArgumentException( + "Route added with non-matching interface: " + routeIface + + " vs. " + mIfaceName); + } + route = routeWithInterface(route); + + int i = findRouteIndexByRouteKey(route); + if (i == -1) { + // Route was not present. Add it. + mRoutes.add(route); + return true; + } else if (mRoutes.get(i).equals(route)) { + // Route was present and has same properties. Do nothing. + return false; + } else { + // Route was present and has different properties. Update it. + mRoutes.set(i, route); + return true; + } + } + + /** + * Removes a {@link RouteInfo} from this {@code LinkProperties}, if present. The route must + * specify an interface and the interface must match the interface of this + * {@code LinkProperties}, or it will not be removed. + * + * @param route A {@link RouteInfo} specifying the route to remove. + * @return {@code true} if the route was removed, {@code false} if it was not present. + * + * @hide + */ + @SystemApi + public boolean removeRoute(@NonNull RouteInfo route) { + return Objects.equals(mIfaceName, route.getInterface()) && mRoutes.remove(route); + } + + /** + * Returns all the {@link RouteInfo} set on this link. + * + * @return An unmodifiable {@link List} of {@link RouteInfo} for this link. + */ + public @NonNull List<RouteInfo> getRoutes() { + return Collections.unmodifiableList(mRoutes); + } + + /** + * Make sure this LinkProperties instance contains routes that cover the local subnet + * of its link addresses. Add any route that is missing. + * @hide + */ + public void ensureDirectlyConnectedRoutes() { + for (LinkAddress addr : mLinkAddresses) { + addRoute(new RouteInfo(addr, null, mIfaceName)); + } + } + + /** + * Returns all the routes on this link and all the links stacked above it. + * @hide + */ + @SystemApi + public @NonNull List<RouteInfo> getAllRoutes() { + List<RouteInfo> routes = new ArrayList<>(mRoutes); + for (LinkProperties stacked: mStackedLinks.values()) { + routes.addAll(stacked.getAllRoutes()); + } + return routes; + } + + /** + * Sets the recommended {@link ProxyInfo} to use on this link, or {@code null} for none. + * Note that Http Proxies are only a hint - the system recommends their use, but it does + * not enforce it and applications may ignore them. + * + * @param proxy A {@link ProxyInfo} defining the HTTP Proxy to use on this link. + */ + public void setHttpProxy(@Nullable ProxyInfo proxy) { + mHttpProxy = proxy; + } + + /** + * Gets the recommended {@link ProxyInfo} (or {@code null}) set on this link. + * + * @return The {@link ProxyInfo} set on this link or {@code null}. + */ + public @Nullable ProxyInfo getHttpProxy() { + return mHttpProxy; + } + + /** + * Returns the NAT64 prefix in use on this link, if any. + * + * @return the NAT64 prefix or {@code null}. + */ + public @Nullable IpPrefix getNat64Prefix() { + return mNat64Prefix; + } + + /** + * Sets the NAT64 prefix in use on this link. + * + * Currently, only 96-bit prefixes (i.e., where the 32-bit IPv4 address is at the end of the + * 128-bit IPv6 address) are supported or {@code null} for no prefix. + * + * @param prefix the NAT64 prefix. + */ + public void setNat64Prefix(@Nullable IpPrefix prefix) { + if (prefix != null && prefix.getPrefixLength() != 96) { + throw new IllegalArgumentException("Only 96-bit prefixes are supported: " + prefix); + } + mNat64Prefix = prefix; // IpPrefix objects are immutable. + } + + /** + * Adds a stacked link. + * + * If there is already a stacked link with the same interface name as link, + * that link is replaced with link. Otherwise, link is added to the list + * of stacked links. + * + * @param link The link to add. + * @return true if the link was stacked, false otherwise. + * @hide + */ + @UnsupportedAppUsage + public boolean addStackedLink(@NonNull LinkProperties link) { + if (link.getInterfaceName() != null) { + mStackedLinks.put(link.getInterfaceName(), link); + return true; + } + return false; + } + + /** + * Removes a stacked link. + * + * If there is a stacked link with the given interface name, it is + * removed. Otherwise, nothing changes. + * + * @param iface The interface name of the link to remove. + * @return true if the link was removed, false otherwise. + * @hide + */ + public boolean removeStackedLink(@NonNull String iface) { + LinkProperties removed = mStackedLinks.remove(iface); + return removed != null; + } + + /** + * Returns all the links stacked on top of this link. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public @NonNull List<LinkProperties> getStackedLinks() { + if (mStackedLinks.isEmpty()) { + return Collections.emptyList(); + } + final List<LinkProperties> stacked = new ArrayList<>(); + for (LinkProperties link : mStackedLinks.values()) { + stacked.add(new LinkProperties(link)); + } + return Collections.unmodifiableList(stacked); + } + + /** + * Clears this object to its initial state. + */ + public void clear() { + if (mParcelSensitiveFields) { + throw new UnsupportedOperationException( + "Cannot clear LinkProperties when parcelSensitiveFields is set"); + } + + mIfaceName = null; + mLinkAddresses.clear(); + mDnses.clear(); + mUsePrivateDns = false; + mPrivateDnsServerName = null; + mPcscfs.clear(); + mDomains = null; + mRoutes.clear(); + mHttpProxy = null; + mStackedLinks.clear(); + mMtu = 0; + mDhcpServerAddress = null; + mTcpBufferSizes = null; + mNat64Prefix = null; + mWakeOnLanSupported = false; + mCaptivePortalApiUrl = null; + mCaptivePortalData = null; + } + + /** + * Implement the Parcelable interface + */ + public int describeContents() { + return 0; + } + + @Override + public String toString() { + // Space as a separator, so no need for spaces at start/end of the individual fragments. + final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}"); + + if (mIfaceName != null) { + resultJoiner.add("InterfaceName:"); + resultJoiner.add(mIfaceName); + } + + resultJoiner.add("LinkAddresses: ["); + if (!mLinkAddresses.isEmpty()) { + resultJoiner.add(TextUtils.join(",", mLinkAddresses)); + } + resultJoiner.add("]"); + + resultJoiner.add("DnsAddresses: ["); + if (!mDnses.isEmpty()) { + resultJoiner.add(TextUtils.join(",", mDnses)); + } + resultJoiner.add("]"); + + if (mUsePrivateDns) { + resultJoiner.add("UsePrivateDns: true"); + } + + if (mPrivateDnsServerName != null) { + resultJoiner.add("PrivateDnsServerName:"); + resultJoiner.add(mPrivateDnsServerName); + } + + if (!mPcscfs.isEmpty()) { + resultJoiner.add("PcscfAddresses: ["); + resultJoiner.add(TextUtils.join(",", mPcscfs)); + resultJoiner.add("]"); + } + + if (!mValidatedPrivateDnses.isEmpty()) { + final StringJoiner validatedPrivateDnsesJoiner = + new StringJoiner(",", "ValidatedPrivateDnsAddresses: [", "]"); + for (final InetAddress addr : mValidatedPrivateDnses) { + validatedPrivateDnsesJoiner.add(addr.getHostAddress()); + } + resultJoiner.add(validatedPrivateDnsesJoiner.toString()); + } + + resultJoiner.add("Domains:"); + resultJoiner.add(mDomains); + + resultJoiner.add("MTU:"); + resultJoiner.add(Integer.toString(mMtu)); + + if (mWakeOnLanSupported) { + resultJoiner.add("WakeOnLanSupported: true"); + } + + if (mDhcpServerAddress != null) { + resultJoiner.add("ServerAddress:"); + resultJoiner.add(mDhcpServerAddress.toString()); + } + + if (mCaptivePortalApiUrl != null) { + resultJoiner.add("CaptivePortalApiUrl: " + mCaptivePortalApiUrl); + } + + if (mCaptivePortalData != null) { + resultJoiner.add("CaptivePortalData: " + mCaptivePortalData); + } + + if (mTcpBufferSizes != null) { + resultJoiner.add("TcpBufferSizes:"); + resultJoiner.add(mTcpBufferSizes); + } + + resultJoiner.add("Routes: ["); + if (!mRoutes.isEmpty()) { + resultJoiner.add(TextUtils.join(",", mRoutes)); + } + resultJoiner.add("]"); + + if (mHttpProxy != null) { + resultJoiner.add("HttpProxy:"); + resultJoiner.add(mHttpProxy.toString()); + } + + if (mNat64Prefix != null) { + resultJoiner.add("Nat64Prefix:"); + resultJoiner.add(mNat64Prefix.toString()); + } + + final Collection<LinkProperties> stackedLinksValues = mStackedLinks.values(); + if (!stackedLinksValues.isEmpty()) { + final StringJoiner stackedLinksJoiner = new StringJoiner(",", "Stacked: [", "]"); + for (final LinkProperties lp : stackedLinksValues) { + stackedLinksJoiner.add("[ " + lp + " ]"); + } + resultJoiner.add(stackedLinksJoiner.toString()); + } + + return resultJoiner.toString(); + } + + /** + * Returns true if this link has an IPv4 address. + * + * @return {@code true} if there is an IPv4 address, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean hasIpv4Address() { + for (LinkAddress address : mLinkAddresses) { + if (address.getAddress() instanceof Inet4Address) { + return true; + } + } + return false; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if there is an IPv4 address, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean hasIPv4Address() { + return hasIpv4Address(); + } + + /** + * Returns true if this link or any of its stacked interfaces has an IPv4 address. + * + * @return {@code true} if there is an IPv4 address, {@code false} otherwise. + */ + private boolean hasIpv4AddressOnInterface(String iface) { + // mIfaceName can be null. + return (Objects.equals(iface, mIfaceName) && hasIpv4Address()) + || (iface != null && mStackedLinks.containsKey(iface) + && mStackedLinks.get(iface).hasIpv4Address()); + } + + /** + * Returns true if this link has a global preferred IPv6 address. + * + * @return {@code true} if there is a global preferred IPv6 address, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean hasGlobalIpv6Address() { + for (LinkAddress address : mLinkAddresses) { + if (address.getAddress() instanceof Inet6Address && address.isGlobalPreferred()) { + return true; + } + } + return false; + } + + /** + * Returns true if this link has an IPv4 unreachable default route. + * + * @return {@code true} if there is an IPv4 unreachable default route, {@code false} otherwise. + * @hide + */ + public boolean hasIpv4UnreachableDefaultRoute() { + for (RouteInfo r : mRoutes) { + if (r.isIPv4UnreachableDefault()) { + return true; + } + } + return false; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if there is a global preferred IPv6 address, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean hasGlobalIPv6Address() { + return hasGlobalIpv6Address(); + } + + /** + * Returns true if this link has an IPv4 default route. + * + * @return {@code true} if there is an IPv4 default route, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean hasIpv4DefaultRoute() { + for (RouteInfo r : mRoutes) { + if (r.isIPv4Default()) { + return true; + } + } + return false; + } + + /** + * Returns true if this link has an IPv6 unreachable default route. + * + * @return {@code true} if there is an IPv6 unreachable default route, {@code false} otherwise. + * @hide + */ + public boolean hasIpv6UnreachableDefaultRoute() { + for (RouteInfo r : mRoutes) { + if (r.isIPv6UnreachableDefault()) { + return true; + } + } + return false; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if there is an IPv4 default route, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean hasIPv4DefaultRoute() { + return hasIpv4DefaultRoute(); + } + + /** + * Returns true if this link has an IPv6 default route. + * + * @return {@code true} if there is an IPv6 default route, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean hasIpv6DefaultRoute() { + for (RouteInfo r : mRoutes) { + if (r.isIPv6Default()) { + return true; + } + } + return false; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if there is an IPv6 default route, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean hasIPv6DefaultRoute() { + return hasIpv6DefaultRoute(); + } + + /** + * Returns true if this link has an IPv4 DNS server. + * + * @return {@code true} if there is an IPv4 DNS server, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean hasIpv4DnsServer() { + for (InetAddress ia : mDnses) { + if (ia instanceof Inet4Address) { + return true; + } + } + return false; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if there is an IPv4 DNS server, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean hasIPv4DnsServer() { + return hasIpv4DnsServer(); + } + + /** + * Returns true if this link has an IPv6 DNS server. + * + * @return {@code true} if there is an IPv6 DNS server, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean hasIpv6DnsServer() { + for (InetAddress ia : mDnses) { + if (ia instanceof Inet6Address) { + return true; + } + } + return false; + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if there is an IPv6 DNS server, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean hasIPv6DnsServer() { + return hasIpv6DnsServer(); + } + + /** + * Returns true if this link has an IPv4 PCSCF server. + * + * @return {@code true} if there is an IPv4 PCSCF server, {@code false} otherwise. + * @hide + */ + public boolean hasIpv4PcscfServer() { + for (InetAddress ia : mPcscfs) { + if (ia instanceof Inet4Address) { + return true; + } + } + return false; + } + + /** + * Returns true if this link has an IPv6 PCSCF server. + * + * @return {@code true} if there is an IPv6 PCSCF server, {@code false} otherwise. + * @hide + */ + public boolean hasIpv6PcscfServer() { + for (InetAddress ia : mPcscfs) { + if (ia instanceof Inet6Address) { + return true; + } + } + return false; + } + + /** + * Returns true if this link is provisioned for global IPv4 connectivity. + * This requires an IP address, default route, and DNS server. + * + * @return {@code true} if the link is provisioned, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean isIpv4Provisioned() { + return (hasIpv4Address() + && hasIpv4DefaultRoute() + && hasIpv4DnsServer()); + } + + /** + * Returns true if this link is provisioned for global IPv6 connectivity. + * This requires an IP address, default route, and DNS server. + * + * @return {@code true} if the link is provisioned, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean isIpv6Provisioned() { + return (hasGlobalIpv6Address() + && hasIpv6DefaultRoute() + && hasIpv6DnsServer()); + } + + /** + * For backward compatibility. + * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely + * just yet. + * @return {@code true} if the link is provisioned, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + public boolean isIPv6Provisioned() { + return isIpv6Provisioned(); + } + + + /** + * Returns true if this link is provisioned for global connectivity, + * for at least one Internet Protocol family. + * + * @return {@code true} if the link is provisioned, {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean isProvisioned() { + return (isIpv4Provisioned() || isIpv6Provisioned()); + } + + /** + * Evaluate whether the {@link InetAddress} is considered reachable. + * + * @return {@code true} if the given {@link InetAddress} is considered reachable, + * {@code false} otherwise. + * @hide + */ + @SystemApi + public boolean isReachable(@NonNull InetAddress ip) { + final List<RouteInfo> allRoutes = getAllRoutes(); + // If we don't have a route to this IP address, it's not reachable. + final RouteInfo bestRoute = RouteInfo.selectBestRoute(allRoutes, ip); + if (bestRoute == null) { + return false; + } + + // TODO: better source address evaluation for destination addresses. + + if (ip instanceof Inet4Address) { + // For IPv4, it suffices for now to simply have any address. + return hasIpv4AddressOnInterface(bestRoute.getInterface()); + } else if (ip instanceof Inet6Address) { + if (ip.isLinkLocalAddress()) { + // For now, just make sure link-local destinations have + // scopedIds set, since transmits will generally fail otherwise. + // TODO: verify it matches the ifindex of one of the interfaces. + return (((Inet6Address)ip).getScopeId() != 0); + } else { + // For non-link-local destinations check that either the best route + // is directly connected or that some global preferred address exists. + // TODO: reconsider all cases (disconnected ULA networks, ...). + return (!bestRoute.hasGateway() || hasGlobalIpv6Address()); + } + } + + return false; + } + + /** + * Compares this {@code LinkProperties} interface name against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage + public boolean isIdenticalInterfaceName(@NonNull LinkProperties target) { + return LinkPropertiesUtils.isIdenticalInterfaceName(target, this); + } + + /** + * Compares this {@code LinkProperties} DHCP server address against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalDhcpServerAddress(@NonNull LinkProperties target) { + return Objects.equals(mDhcpServerAddress, target.mDhcpServerAddress); + } + + /** + * Compares this {@code LinkProperties} interface addresses against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage + public boolean isIdenticalAddresses(@NonNull LinkProperties target) { + return LinkPropertiesUtils.isIdenticalAddresses(target, this); + } + + /** + * Compares this {@code LinkProperties} DNS addresses against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage + public boolean isIdenticalDnses(@NonNull LinkProperties target) { + return LinkPropertiesUtils.isIdenticalDnses(target, this); + } + + /** + * Compares this {@code LinkProperties} private DNS settings against the + * target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalPrivateDns(@NonNull LinkProperties target) { + return (isPrivateDnsActive() == target.isPrivateDnsActive() + && TextUtils.equals(getPrivateDnsServerName(), + target.getPrivateDnsServerName())); + } + + /** + * Compares this {@code LinkProperties} validated private DNS addresses against + * the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalValidatedPrivateDnses(@NonNull LinkProperties target) { + Collection<InetAddress> targetDnses = target.getValidatedPrivateDnsServers(); + return (mValidatedPrivateDnses.size() == targetDnses.size()) + ? mValidatedPrivateDnses.containsAll(targetDnses) : false; + } + + /** + * Compares this {@code LinkProperties} PCSCF addresses against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalPcscfs(@NonNull LinkProperties target) { + Collection<InetAddress> targetPcscfs = target.getPcscfServers(); + return (mPcscfs.size() == targetPcscfs.size()) ? + mPcscfs.containsAll(targetPcscfs) : false; + } + + /** + * Compares this {@code LinkProperties} Routes against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage + public boolean isIdenticalRoutes(@NonNull LinkProperties target) { + return LinkPropertiesUtils.isIdenticalRoutes(target, this); + } + + /** + * Compares this {@code LinkProperties} HttpProxy against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public boolean isIdenticalHttpProxy(@NonNull LinkProperties target) { + return LinkPropertiesUtils.isIdenticalHttpProxy(target, this); + } + + /** + * Compares this {@code LinkProperties} stacked links against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public boolean isIdenticalStackedLinks(@NonNull LinkProperties target) { + if (!mStackedLinks.keySet().equals(target.mStackedLinks.keySet())) { + return false; + } + for (LinkProperties stacked : mStackedLinks.values()) { + // Hashtable values can never be null. + String iface = stacked.getInterfaceName(); + if (!stacked.equals(target.mStackedLinks.get(iface))) { + return false; + } + } + return true; + } + + /** + * Compares this {@code LinkProperties} MTU against the target + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalMtu(@NonNull LinkProperties target) { + return getMtu() == target.getMtu(); + } + + /** + * Compares this {@code LinkProperties} Tcp buffer sizes against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalTcpBufferSizes(@NonNull LinkProperties target) { + return Objects.equals(mTcpBufferSizes, target.mTcpBufferSizes); + } + + /** + * Compares this {@code LinkProperties} NAT64 prefix against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalNat64Prefix(@NonNull LinkProperties target) { + return Objects.equals(mNat64Prefix, target.mNat64Prefix); + } + + /** + * Compares this {@code LinkProperties} WakeOnLan supported against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalWakeOnLan(LinkProperties target) { + return isWakeOnLanSupported() == target.isWakeOnLanSupported(); + } + + /** + * Compares this {@code LinkProperties}'s CaptivePortalApiUrl against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalCaptivePortalApiUrl(LinkProperties target) { + return Objects.equals(mCaptivePortalApiUrl, target.mCaptivePortalApiUrl); + } + + /** + * Compares this {@code LinkProperties}'s CaptivePortalData against the target. + * + * @param target LinkProperties to compare. + * @return {@code true} if both are identical, {@code false} otherwise. + * @hide + */ + public boolean isIdenticalCaptivePortalData(LinkProperties target) { + return Objects.equals(mCaptivePortalData, target.mCaptivePortalData); + } + + /** + * Set whether the network interface supports WakeOnLAN + * + * @param supported WakeOnLAN supported value + * + * @hide + */ + public void setWakeOnLanSupported(boolean supported) { + mWakeOnLanSupported = supported; + } + + /** + * Returns whether the network interface supports WakeOnLAN + * + * @return {@code true} if interface supports WakeOnLAN, {@code false} otherwise. + */ + public boolean isWakeOnLanSupported() { + return mWakeOnLanSupported; + } + + /** + * Set the URL of the captive portal API endpoint to get more information about the network. + * @hide + */ + @SystemApi + public void setCaptivePortalApiUrl(@Nullable Uri url) { + mCaptivePortalApiUrl = url; + } + + /** + * Get the URL of the captive portal API endpoint to get more information about the network. + * + * <p>This is null unless the application has + * {@link android.Manifest.permission.NETWORK_SETTINGS} or + * {@link NetworkStack#PERMISSION_MAINLINE_NETWORK_STACK} permissions, and the network provided + * the URL. + * @hide + */ + @SystemApi + @Nullable + public Uri getCaptivePortalApiUrl() { + return mCaptivePortalApiUrl; + } + + /** + * Set the CaptivePortalData obtained from the captive portal API (RFC7710bis). + * @hide + */ + @SystemApi + public void setCaptivePortalData(@Nullable CaptivePortalData data) { + mCaptivePortalData = data; + } + + /** + * Get the CaptivePortalData obtained from the captive portal API (RFC7710bis). + * + * <p>This is null unless the application has + * {@link android.Manifest.permission.NETWORK_SETTINGS} or + * {@link NetworkStack#PERMISSION_MAINLINE_NETWORK_STACK} permissions. + * @hide + */ + @SystemApi + @Nullable + public CaptivePortalData getCaptivePortalData() { + return mCaptivePortalData; + } + + /** + * Compares this {@code LinkProperties} instance against the target + * LinkProperties in {@code obj}. Two LinkPropertieses are equal if + * all their fields are equal in values. + * + * For collection fields, such as mDnses, containsAll() is used to check + * if two collections contains the same elements, independent of order. + * There are two thoughts regarding containsAll() + * 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal. + * 2. Worst case performance is O(n^2). + * + * @param obj the object to be tested for equality. + * @return {@code true} if both objects are equal, {@code false} otherwise. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + + if (!(obj instanceof LinkProperties)) return false; + + LinkProperties target = (LinkProperties) obj; + /* + * This method does not check that stacked interfaces are equal, because + * stacked interfaces are not so much a property of the link as a + * description of connections between links. + */ + return isIdenticalInterfaceName(target) + && isIdenticalAddresses(target) + && isIdenticalDhcpServerAddress(target) + && isIdenticalDnses(target) + && isIdenticalPrivateDns(target) + && isIdenticalValidatedPrivateDnses(target) + && isIdenticalPcscfs(target) + && isIdenticalRoutes(target) + && isIdenticalHttpProxy(target) + && isIdenticalStackedLinks(target) + && isIdenticalMtu(target) + && isIdenticalTcpBufferSizes(target) + && isIdenticalNat64Prefix(target) + && isIdenticalWakeOnLan(target) + && isIdenticalCaptivePortalApiUrl(target) + && isIdenticalCaptivePortalData(target); + } + + /** + * Generate hashcode based on significant fields + * + * Equal objects must produce the same hash code, while unequal objects + * may have the same hash codes. + */ + @Override + public int hashCode() { + return ((null == mIfaceName) ? 0 : mIfaceName.hashCode() + + mLinkAddresses.size() * 31 + + mDnses.size() * 37 + + mValidatedPrivateDnses.size() * 61 + + ((null == mDomains) ? 0 : mDomains.hashCode()) + + mRoutes.size() * 41 + + ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode()) + + mStackedLinks.hashCode() * 47) + + mMtu * 51 + + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode()) + + (mUsePrivateDns ? 57 : 0) + + ((null == mDhcpServerAddress) ? 0 : mDhcpServerAddress.hashCode()) + + mPcscfs.size() * 67 + + ((null == mPrivateDnsServerName) ? 0 : mPrivateDnsServerName.hashCode()) + + Objects.hash(mNat64Prefix) + + (mWakeOnLanSupported ? 71 : 0) + + Objects.hash(mCaptivePortalApiUrl, mCaptivePortalData); + } + + /** + * Implement the Parcelable interface. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getInterfaceName()); + dest.writeInt(mLinkAddresses.size()); + for (LinkAddress linkAddress : mLinkAddresses) { + dest.writeParcelable(linkAddress, flags); + } + + writeAddresses(dest, mDnses); + writeAddresses(dest, mValidatedPrivateDnses); + dest.writeBoolean(mUsePrivateDns); + dest.writeString(mPrivateDnsServerName); + writeAddresses(dest, mPcscfs); + dest.writeString(mDomains); + writeAddress(dest, mDhcpServerAddress); + dest.writeInt(mMtu); + dest.writeString(mTcpBufferSizes); + dest.writeInt(mRoutes.size()); + for (RouteInfo route : mRoutes) { + dest.writeParcelable(route, flags); + } + + if (mHttpProxy != null) { + dest.writeByte((byte)1); + dest.writeParcelable(mHttpProxy, flags); + } else { + dest.writeByte((byte)0); + } + dest.writeParcelable(mNat64Prefix, 0); + + ArrayList<LinkProperties> stackedLinks = new ArrayList<>(mStackedLinks.values()); + dest.writeList(stackedLinks); + + dest.writeBoolean(mWakeOnLanSupported); + dest.writeParcelable(mParcelSensitiveFields ? mCaptivePortalApiUrl : null, 0); + dest.writeParcelable(mParcelSensitiveFields ? mCaptivePortalData : null, 0); + } + + private static void writeAddresses(@NonNull Parcel dest, @NonNull List<InetAddress> list) { + dest.writeInt(list.size()); + for (InetAddress d : list) { + writeAddress(dest, d); + } + } + + private static void writeAddress(@NonNull Parcel dest, @Nullable InetAddress addr) { + byte[] addressBytes = (addr == null ? null : addr.getAddress()); + dest.writeByteArray(addressBytes); + if (addr instanceof Inet6Address) { + final Inet6Address v6Addr = (Inet6Address) addr; + final boolean hasScopeId = v6Addr.getScopeId() != 0; + dest.writeBoolean(hasScopeId); + if (hasScopeId) dest.writeInt(v6Addr.getScopeId()); + } + } + + @Nullable + private static InetAddress readAddress(@NonNull Parcel p) throws UnknownHostException { + final byte[] addr = p.createByteArray(); + if (addr == null) return null; + + if (addr.length == INET6_ADDR_LENGTH) { + final boolean hasScopeId = p.readBoolean(); + final int scopeId = hasScopeId ? p.readInt() : 0; + return Inet6Address.getByAddress(null /* host */, addr, scopeId); + } + + return InetAddress.getByAddress(addr); + } + + /** + * Implement the Parcelable interface. + */ + public static final @android.annotation.NonNull Creator<LinkProperties> CREATOR = + new Creator<LinkProperties>() { + public LinkProperties createFromParcel(Parcel in) { + LinkProperties netProp = new LinkProperties(); + + String iface = in.readString(); + if (iface != null) { + netProp.setInterfaceName(iface); + } + int addressCount = in.readInt(); + for (int i = 0; i < addressCount; i++) { + netProp.addLinkAddress(in.readParcelable(null)); + } + addressCount = in.readInt(); + for (int i = 0; i < addressCount; i++) { + try { + netProp.addDnsServer(readAddress(in)); + } catch (UnknownHostException e) { } + } + addressCount = in.readInt(); + for (int i = 0; i < addressCount; i++) { + try { + netProp.addValidatedPrivateDnsServer(readAddress(in)); + } catch (UnknownHostException e) { } + } + netProp.setUsePrivateDns(in.readBoolean()); + netProp.setPrivateDnsServerName(in.readString()); + addressCount = in.readInt(); + for (int i = 0; i < addressCount; i++) { + try { + netProp.addPcscfServer(readAddress(in)); + } catch (UnknownHostException e) { } + } + netProp.setDomains(in.readString()); + try { + netProp.setDhcpServerAddress((Inet4Address) InetAddress + .getByAddress(in.createByteArray())); + } catch (UnknownHostException e) { } + netProp.setMtu(in.readInt()); + netProp.setTcpBufferSizes(in.readString()); + addressCount = in.readInt(); + for (int i = 0; i < addressCount; i++) { + netProp.addRoute(in.readParcelable(null)); + } + if (in.readByte() == 1) { + netProp.setHttpProxy(in.readParcelable(null)); + } + netProp.setNat64Prefix(in.readParcelable(null)); + ArrayList<LinkProperties> stackedLinks = new ArrayList<LinkProperties>(); + in.readList(stackedLinks, LinkProperties.class.getClassLoader()); + for (LinkProperties stackedLink: stackedLinks) { + netProp.addStackedLink(stackedLink); + } + netProp.setWakeOnLanSupported(in.readBoolean()); + + netProp.setCaptivePortalApiUrl(in.readParcelable(null)); + netProp.setCaptivePortalData(in.readParcelable(null)); + return netProp; + } + + public LinkProperties[] newArray(int size) { + return new LinkProperties[size]; + } + }; + + /** + * Check the valid MTU range based on IPv4 or IPv6. + * @hide + */ + public static boolean isValidMtu(int mtu, boolean ipv6) { + if (ipv6) { + return mtu >= MIN_MTU_V6 && mtu <= MAX_MTU; + } else { + return mtu >= MIN_MTU && mtu <= MAX_MTU; + } + } +} diff --git a/framework/src/android/net/MacAddress.java b/framework/src/android/net/MacAddress.java new file mode 100644 index 0000000000000000000000000000000000000000..26a504a29c1cf7cbeb4713def7a49154ed6d2c1f --- /dev/null +++ b/framework/src/android/net/MacAddress.java @@ -0,0 +1,400 @@ +/* + * 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. + */ + +package android.net; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.net.wifi.WifiInfo; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.net.module.util.MacAddressUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.Inet6Address; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Objects; + +/** + * Representation of a MAC address. + * + * This class only supports 48 bits long addresses and does not support 64 bits long addresses. + * Instances of this class are immutable. This class provides implementations of hashCode() + * and equals() that make it suitable for use as keys in standard implementations of + * {@link java.util.Map}. + */ +public final class MacAddress implements Parcelable { + + private static final int ETHER_ADDR_LEN = 6; + private static final byte[] ETHER_ADDR_BROADCAST = addr(0xff, 0xff, 0xff, 0xff, 0xff, 0xff); + + /** + * The MacAddress representing the unique broadcast MAC address. + */ + public static final MacAddress BROADCAST_ADDRESS = MacAddress.fromBytes(ETHER_ADDR_BROADCAST); + + /** + * The MacAddress zero MAC address. + * + * <p>Not publicly exposed or treated specially since the OUI 00:00:00 is registered. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static final MacAddress ALL_ZEROS_ADDRESS = new MacAddress(0); + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_UNKNOWN, + TYPE_UNICAST, + TYPE_MULTICAST, + TYPE_BROADCAST, + }) + public @interface MacAddressType { } + + /** @hide Indicates a MAC address of unknown type. */ + public static final int TYPE_UNKNOWN = 0; + /** Indicates a MAC address is a unicast address. */ + public static final int TYPE_UNICAST = 1; + /** Indicates a MAC address is a multicast address. */ + public static final int TYPE_MULTICAST = 2; + /** Indicates a MAC address is the broadcast address. */ + public static final int TYPE_BROADCAST = 3; + + private static final long VALID_LONG_MASK = (1L << 48) - 1; + private static final long LOCALLY_ASSIGNED_MASK = MacAddress.fromString("2:0:0:0:0:0").mAddr; + private static final long MULTICAST_MASK = MacAddress.fromString("1:0:0:0:0:0").mAddr; + private static final long OUI_MASK = MacAddress.fromString("ff:ff:ff:0:0:0").mAddr; + private static final long NIC_MASK = MacAddress.fromString("0:0:0:ff:ff:ff").mAddr; + private static final MacAddress BASE_GOOGLE_MAC = MacAddress.fromString("da:a1:19:0:0:0"); + /** Default wifi MAC address used for a special purpose **/ + private static final MacAddress DEFAULT_MAC_ADDRESS = + MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS); + + // Internal representation of the MAC address as a single 8 byte long. + // The encoding scheme sets the two most significant bytes to 0. The 6 bytes of the + // MAC address are encoded in the 6 least significant bytes of the long, where the first + // byte of the array is mapped to the 3rd highest logical byte of the long, the second + // byte of the array is mapped to the 4th highest logical byte of the long, and so on. + private final long mAddr; + + private MacAddress(long addr) { + mAddr = (VALID_LONG_MASK & addr); + } + + /** + * Returns the type of this address. + * + * @return the int constant representing the MAC address type of this MacAddress. + */ + public @MacAddressType int getAddressType() { + if (equals(BROADCAST_ADDRESS)) { + return TYPE_BROADCAST; + } + if ((mAddr & MULTICAST_MASK) != 0) { + return TYPE_MULTICAST; + } + return TYPE_UNICAST; + } + + /** + * @return true if this MacAddress is a locally assigned address. + */ + public boolean isLocallyAssigned() { + return (mAddr & LOCALLY_ASSIGNED_MASK) != 0; + } + + /** + * Convert this MacAddress to a byte array. + * + * The returned array is in network order. For example, if this MacAddress is 1:2:3:4:5:6, + * the returned array is [1, 2, 3, 4, 5, 6]. + * + * @return a byte array representation of this MacAddress. + */ + public @NonNull byte[] toByteArray() { + return byteAddrFromLongAddr(mAddr); + } + + /** + * Returns a human-readable representation of this MacAddress. + * The exact format is implementation-dependent and should not be assumed to have any + * particular format. + */ + @Override + public @NonNull String toString() { + return stringAddrFromLongAddr(mAddr); + } + + /** + * @return a String representation of the OUI part of this MacAddress made of 3 hexadecimal + * numbers in [0,ff] joined by ':' characters. + */ + public @NonNull String toOuiString() { + return String.format( + "%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff); + } + + @Override + public int hashCode() { + return (int) ((mAddr >> 32) ^ mAddr); + } + + @Override + public boolean equals(@Nullable Object o) { + return (o instanceof MacAddress) && ((MacAddress) o).mAddr == mAddr; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mAddr); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @android.annotation.NonNull Parcelable.Creator<MacAddress> CREATOR = + new Parcelable.Creator<MacAddress>() { + public MacAddress createFromParcel(Parcel in) { + return new MacAddress(in.readLong()); + } + + public MacAddress[] newArray(int size) { + return new MacAddress[size]; + } + }; + + /** + * Returns true if the given byte array is an valid MAC address. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array. + * @return true if the given byte array is not null and has the length of a MAC address. + * + * @hide + */ + public static boolean isMacAddress(byte[] addr) { + return MacAddressUtils.isMacAddress(addr); + } + + /** + * Returns the MAC address type of the MAC address represented by the given byte array, + * or null if the given byte array does not represent a MAC address. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representing a MAC address. + * @return the int constant representing the MAC address type of the MAC address represented + * by the given byte array, or type UNKNOWN if the byte array is not a valid MAC address. + * + * @hide + */ + public static int macAddressType(byte[] addr) { + if (!isMacAddress(addr)) { + return TYPE_UNKNOWN; + } + return MacAddress.fromBytes(addr).getAddressType(); + } + + /** + * Converts a String representation of a MAC address to a byte array representation. + * A valid String representation for a MacAddress is a series of 6 values in the + * range [0,ff] printed in hexadecimal and joined by ':' characters. + * + * @param addr a String representation of a MAC address. + * @return the byte representation of the MAC address. + * @throws IllegalArgumentException if the given String is not a valid representation. + * + * @hide + */ + public static @NonNull byte[] byteAddrFromStringAddr(String addr) { + Objects.requireNonNull(addr); + String[] parts = addr.split(":"); + if (parts.length != ETHER_ADDR_LEN) { + throw new IllegalArgumentException(addr + " was not a valid MAC address"); + } + byte[] bytes = new byte[ETHER_ADDR_LEN]; + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + int x = Integer.valueOf(parts[i], 16); + if (x < 0 || 0xff < x) { + throw new IllegalArgumentException(addr + "was not a valid MAC address"); + } + bytes[i] = (byte) x; + } + return bytes; + } + + /** + * Converts a byte array representation of a MAC address to a String representation made + * of 6 hexadecimal numbers in [0,ff] joined by ':' characters. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representation of a MAC address. + * @return the String representation of the MAC address. + * @throws IllegalArgumentException if the given byte array is not a valid representation. + * + * @hide + */ + public static @NonNull String stringAddrFromByteAddr(byte[] addr) { + if (!isMacAddress(addr)) { + return null; + } + return String.format("%02x:%02x:%02x:%02x:%02x:%02x", + addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]); + } + + private static byte[] byteAddrFromLongAddr(long addr) { + return MacAddressUtils.byteAddrFromLongAddr(addr); + } + + private static long longAddrFromByteAddr(byte[] addr) { + return MacAddressUtils.longAddrFromByteAddr(addr); + } + + // Internal conversion function equivalent to longAddrFromByteAddr(byteAddrFromStringAddr(addr)) + // that avoids the allocation of an intermediary byte[]. + private static long longAddrFromStringAddr(String addr) { + Objects.requireNonNull(addr); + String[] parts = addr.split(":"); + if (parts.length != ETHER_ADDR_LEN) { + throw new IllegalArgumentException(addr + " was not a valid MAC address"); + } + long longAddr = 0; + for (int i = 0; i < parts.length; i++) { + int x = Integer.valueOf(parts[i], 16); + if (x < 0 || 0xff < x) { + throw new IllegalArgumentException(addr + "was not a valid MAC address"); + } + longAddr = x + (longAddr << 8); + } + return longAddr; + } + + // Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr)) + // that avoids the allocation of an intermediary byte[]. + private static @NonNull String stringAddrFromLongAddr(long addr) { + return String.format("%02x:%02x:%02x:%02x:%02x:%02x", + (addr >> 40) & 0xff, + (addr >> 32) & 0xff, + (addr >> 24) & 0xff, + (addr >> 16) & 0xff, + (addr >> 8) & 0xff, + addr & 0xff); + } + + /** + * Creates a MacAddress from the given String representation. A valid String representation + * for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal + * and joined by ':' characters. + * + * @param addr a String representation of a MAC address. + * @return the MacAddress corresponding to the given String representation. + * @throws IllegalArgumentException if the given String is not a valid representation. + */ + public static @NonNull MacAddress fromString(@NonNull String addr) { + return new MacAddress(longAddrFromStringAddr(addr)); + } + + /** + * Creates a MacAddress from the given byte array representation. + * A valid byte array representation for a MacAddress is a non-null array of length 6. + * + * @param addr a byte array representation of a MAC address. + * @return the MacAddress corresponding to the given byte array representation. + * @throws IllegalArgumentException if the given byte array is not a valid representation. + */ + public static @NonNull MacAddress fromBytes(@NonNull byte[] addr) { + return new MacAddress(longAddrFromByteAddr(addr)); + } + + /** + * Returns a generated MAC address whose 24 least significant bits constituting the + * NIC part of the address are randomly selected and has Google OUI base. + * + * The locally assigned bit is always set to 1. The multicast bit is always set to 0. + * + * @return a random locally assigned, unicast MacAddress with Google OUI. + * + * @hide + */ + public static @NonNull MacAddress createRandomUnicastAddressWithGoogleBase() { + return MacAddressUtils.createRandomUnicastAddress(BASE_GOOGLE_MAC, new SecureRandom()); + } + + // Convenience function for working around the lack of byte literals. + private static byte[] addr(int... in) { + if (in.length != ETHER_ADDR_LEN) { + throw new IllegalArgumentException(Arrays.toString(in) + + " was not an array with length equal to " + ETHER_ADDR_LEN); + } + byte[] out = new byte[ETHER_ADDR_LEN]; + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + out[i] = (byte) in[i]; + } + return out; + } + + /** + * Checks if this MAC Address matches the provided range. + * + * @param baseAddress MacAddress representing the base address to compare with. + * @param mask MacAddress representing the mask to use during comparison. + * @return true if this MAC Address matches the given range. + * + */ + public boolean matches(@NonNull MacAddress baseAddress, @NonNull MacAddress mask) { + Objects.requireNonNull(baseAddress); + Objects.requireNonNull(mask); + return (mAddr & mask.mAddr) == (baseAddress.mAddr & mask.mAddr); + } + + /** + * Create a link-local Inet6Address from the MAC address. The EUI-48 MAC address is converted + * to an EUI-64 MAC address per RFC 4291. The resulting EUI-64 is used to construct a link-local + * IPv6 address per RFC 4862. + * + * @return A link-local Inet6Address constructed from the MAC address. + */ + public @Nullable Inet6Address getLinkLocalIpv6FromEui48Mac() { + byte[] macEui48Bytes = toByteArray(); + byte[] addr = new byte[16]; + + addr[0] = (byte) 0xfe; + addr[1] = (byte) 0x80; + addr[8] = (byte) (macEui48Bytes[0] ^ (byte) 0x02); // flip the link-local bit + addr[9] = macEui48Bytes[1]; + addr[10] = macEui48Bytes[2]; + addr[11] = (byte) 0xff; + addr[12] = (byte) 0xfe; + addr[13] = macEui48Bytes[3]; + addr[14] = macEui48Bytes[4]; + addr[15] = macEui48Bytes[5]; + + try { + return Inet6Address.getByAddress(null, addr, 0); + } catch (UnknownHostException e) { + return null; + } + } +} diff --git a/framework/src/android/net/NattKeepalivePacketData.java b/framework/src/android/net/NattKeepalivePacketData.java new file mode 100644 index 0000000000000000000000000000000000000000..c4f8fc281f25dc775cf9d3443f3054479e78164c --- /dev/null +++ b/framework/src/android/net/NattKeepalivePacketData.java @@ -0,0 +1,144 @@ +/* + * 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.net.InvalidPacketException.ERROR_INVALID_IP_ADDRESS; +import static android.net.InvalidPacketException.ERROR_INVALID_PORT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.system.OsConstants; + +import com.android.net.module.util.IpUtils; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Objects; + +/** @hide */ +@SystemApi +public final class NattKeepalivePacketData extends KeepalivePacketData implements Parcelable { + private static final int IPV4_HEADER_LENGTH = 20; + private static final int UDP_HEADER_LENGTH = 8; + + // This should only be constructed via static factory methods, such as + // nattKeepalivePacket + public NattKeepalivePacketData(@NonNull InetAddress srcAddress, int srcPort, + @NonNull InetAddress dstAddress, int dstPort, @NonNull byte[] data) throws + InvalidPacketException { + super(srcAddress, srcPort, dstAddress, dstPort, data); + } + + /** + * Factory method to create Nat-T keepalive packet structure. + * @hide + */ + public static NattKeepalivePacketData nattKeepalivePacket( + InetAddress srcAddress, int srcPort, InetAddress dstAddress, int dstPort) + throws InvalidPacketException { + + if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) { + throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS); + } + + if (dstPort != NattSocketKeepalive.NATT_PORT) { + throw new InvalidPacketException(ERROR_INVALID_PORT); + } + + int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.BIG_ENDIAN); + buf.putShort((short) 0x4500); // IP version and TOS + buf.putShort((short) length); + buf.putInt(0); // ID, flags, offset + buf.put((byte) 64); // TTL + buf.put((byte) OsConstants.IPPROTO_UDP); + int ipChecksumOffset = buf.position(); + buf.putShort((short) 0); // IP checksum + buf.put(srcAddress.getAddress()); + buf.put(dstAddress.getAddress()); + buf.putShort((short) srcPort); + buf.putShort((short) dstPort); + buf.putShort((short) (length - 20)); // UDP length + int udpChecksumOffset = buf.position(); + buf.putShort((short) 0); // UDP checksum + buf.put((byte) 0xff); // NAT-T keepalive + buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0)); + buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH)); + + return new NattKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array()); + } + + /** Parcelable Implementation */ + public int describeContents() { + return 0; + } + + /** Write to parcel */ + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeString(getSrcAddress().getHostAddress()); + out.writeString(getDstAddress().getHostAddress()); + out.writeInt(getSrcPort()); + out.writeInt(getDstPort()); + } + + /** Parcelable Creator */ + public static final @NonNull Parcelable.Creator<NattKeepalivePacketData> CREATOR = + new Parcelable.Creator<NattKeepalivePacketData>() { + public NattKeepalivePacketData createFromParcel(Parcel in) { + final InetAddress srcAddress = + InetAddresses.parseNumericAddress(in.readString()); + final InetAddress dstAddress = + InetAddresses.parseNumericAddress(in.readString()); + final int srcPort = in.readInt(); + final int dstPort = in.readInt(); + try { + return NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, + dstAddress, dstPort); + } catch (InvalidPacketException e) { + throw new IllegalArgumentException( + "Invalid NAT-T keepalive data: " + e.getError()); + } + } + + public NattKeepalivePacketData[] newArray(int size) { + return new NattKeepalivePacketData[size]; + } + }; + + @Override + public boolean equals(@Nullable final Object o) { + if (!(o instanceof NattKeepalivePacketData)) return false; + final NattKeepalivePacketData other = (NattKeepalivePacketData) o; + final InetAddress srcAddress = getSrcAddress(); + final InetAddress dstAddress = getDstAddress(); + return srcAddress.equals(other.getSrcAddress()) + && dstAddress.equals(other.getDstAddress()) + && getSrcPort() == other.getSrcPort() + && getDstPort() == other.getDstPort(); + } + + @Override + public int hashCode() { + return Objects.hash(getSrcAddress(), getDstAddress(), getSrcPort(), getDstPort()); + } +} diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java new file mode 100644 index 0000000000000000000000000000000000000000..a15d165e65e7082e1f58e9742749564c31cb3aaf --- /dev/null +++ b/framework/src/android/net/NattSocketKeepalive.java @@ -0,0 +1,77 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import java.net.InetAddress; +import java.util.concurrent.Executor; + +/** @hide */ +public final class NattSocketKeepalive extends SocketKeepalive { + /** The NAT-T destination port for IPsec */ + public static final int NATT_PORT = 4500; + + @NonNull private final InetAddress mSource; + @NonNull private final InetAddress mDestination; + private final int mResourceId; + + NattSocketKeepalive(@NonNull IConnectivityManager service, + @NonNull Network network, + @NonNull ParcelFileDescriptor pfd, + int resourceId, + @NonNull InetAddress source, + @NonNull InetAddress destination, + @NonNull Executor executor, + @NonNull Callback callback) { + super(service, network, pfd, executor, callback); + mSource = source; + mDestination = destination; + mResourceId = resourceId; + } + + @Override + void startImpl(int intervalSec) { + mExecutor.execute(() -> { + try { + mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId, + intervalSec, mCallback, + mSource.getHostAddress(), mDestination.getHostAddress()); + } catch (RemoteException e) { + Log.e(TAG, "Error starting socket keepalive: ", e); + throw e.rethrowFromSystemServer(); + } + }); + } + + @Override + void stopImpl() { + mExecutor.execute(() -> { + try { + if (mSlot != null) { + mService.stopKeepalive(mNetwork, mSlot); + } + } catch (RemoteException e) { + Log.e(TAG, "Error stopping socket keepalive: ", e); + throw e.rethrowFromSystemServer(); + } + }); + } +} diff --git a/framework/src/android/net/Network.java b/framework/src/android/net/Network.java new file mode 100644 index 0000000000000000000000000000000000000000..1f490337de937eb5ee4ea05ee952411356392d82 --- /dev/null +++ b/framework/src/android/net/Network.java @@ -0,0 +1,530 @@ +/* + * 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 android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import com.android.internal.annotations.GuardedBy; + +import libcore.io.IoUtils; +import libcore.net.http.Dns; +import libcore.net.http.HttpURLConnectionFactory; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import javax.net.SocketFactory; + +/** + * Identifies a {@code Network}. This is supplied to applications via + * {@link ConnectivityManager.NetworkCallback} in response to the active + * {@link ConnectivityManager#requestNetwork} or passive + * {@link ConnectivityManager#registerNetworkCallback} calls. + * It is used to direct traffic to the given {@code Network}, either on a {@link Socket} basis + * through a targeted {@link SocketFactory} or process-wide via + * {@link ConnectivityManager#bindProcessToNetwork}. + */ +public class Network implements Parcelable { + + /** + * The unique id of the network. + * @hide + */ + @UnsupportedAppUsage + public final int netId; + + // Objects used to perform per-network operations such as getSocketFactory + // and openConnection, and a lock to protect access to them. + private volatile NetworkBoundSocketFactory mNetworkBoundSocketFactory = null; + // mUrlConnectionFactory is initialized lazily when it is first needed. + @GuardedBy("mLock") + private HttpURLConnectionFactory mUrlConnectionFactory; + private final Object mLock = new Object(); + + // Default connection pool values. These are evaluated at startup, just + // like the OkHttp code. Also like the OkHttp code, we will throw parse + // exceptions at class loading time if the properties are set but are not + // valid integers. + private static final boolean httpKeepAlive = + Boolean.parseBoolean(System.getProperty("http.keepAlive", "true")); + private static final int httpMaxConnections = + httpKeepAlive ? Integer.parseInt(System.getProperty("http.maxConnections", "5")) : 0; + private static final long httpKeepAliveDurationMs = + Long.parseLong(System.getProperty("http.keepAliveDuration", "300000")); // 5 minutes. + // Value used to obfuscate network handle longs. + // The HANDLE_MAGIC value MUST be kept in sync with the corresponding + // value in the native/android/net.c NDK implementation. + private static final long HANDLE_MAGIC = 0xcafed00dL; + private static final int HANDLE_MAGIC_SIZE = 32; + private static final int USE_LOCAL_NAMESERVERS_FLAG = 0x80000000; + + // A boolean to control how getAllByName()/getByName() behaves in the face + // of Private DNS. + // + // When true, these calls will request that DNS resolution bypass any + // Private DNS that might otherwise apply. Use of this feature is restricted + // and permission checks are made by netd (attempts to bypass Private DNS + // without appropriate permission are silently turned into vanilla DNS + // requests). This only affects DNS queries made using this network object. + // + // It it not parceled to receivers because (a) it can be set or cleared at + // anytime and (b) receivers should be explicit about attempts to bypass + // Private DNS so that the intent of the code is easily determined and + // code search audits are possible. + private final transient boolean mPrivateDnsBypass; + + /** + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public Network(int netId) { + this(netId, false); + } + + /** + * @hide + */ + public Network(int netId, boolean privateDnsBypass) { + this.netId = netId; + this.mPrivateDnsBypass = privateDnsBypass; + } + + /** + * @hide + */ + @SystemApi + public Network(@NonNull Network that) { + this(that.netId, that.mPrivateDnsBypass); + } + + /** + * Operates the same as {@code InetAddress.getAllByName} except that host + * resolution is done on this network. + * + * @param host the hostname or literal IP string to be resolved. + * @return the array of addresses associated with the specified host. + * @throws UnknownHostException if the address lookup fails. + */ + public InetAddress[] getAllByName(String host) throws UnknownHostException { + return InetAddressCompat.getAllByNameOnNet(host, getNetIdForResolv()); + } + + /** + * Operates the same as {@code InetAddress.getByName} except that host + * resolution is done on this network. + * + * @param host the hostname to be resolved to an address or {@code null}. + * @return the {@code InetAddress} instance representing the host. + * @throws UnknownHostException + * if the address lookup fails. + */ + public InetAddress getByName(String host) throws UnknownHostException { + return InetAddressCompat.getByNameOnNet(host, getNetIdForResolv()); + } + + /** + * Obtain a Network object for which Private DNS is to be bypassed when attempting + * to use {@link #getAllByName(String)}/{@link #getByName(String)} methods on the given + * instance for hostname resolution. + * + * @hide + */ + @SystemApi + public @NonNull Network getPrivateDnsBypassingCopy() { + return new Network(netId, true); + } + + /** + * Get the unique id of the network. + * + * @hide + */ + @SystemApi + public int getNetId() { + return netId; + } + + /** + * Returns a netid marked with the Private DNS bypass flag. + * + * This flag must be kept in sync with the NETID_USE_LOCAL_NAMESERVERS flag + * in system/netd/include/NetdClient.h. + * + * @hide + */ + public int getNetIdForResolv() { + return mPrivateDnsBypass + ? (USE_LOCAL_NAMESERVERS_FLAG | netId) // Non-portable DNS resolution flag. + : netId; + } + + /** + * A {@code SocketFactory} that produces {@code Socket}'s bound to this network. + */ + private class NetworkBoundSocketFactory extends SocketFactory { + private Socket connectToHost(String host, int port, SocketAddress localAddress) + throws IOException { + // Lookup addresses only on this Network. + InetAddress[] hostAddresses = getAllByName(host); + // Try all addresses. + for (int i = 0; i < hostAddresses.length; i++) { + try { + Socket socket = createSocket(); + boolean failed = true; + try { + if (localAddress != null) socket.bind(localAddress); + socket.connect(new InetSocketAddress(hostAddresses[i], port)); + failed = false; + return socket; + } finally { + if (failed) IoUtils.closeQuietly(socket); + } + } catch (IOException e) { + if (i == (hostAddresses.length - 1)) throw e; + } + } + throw new UnknownHostException(host); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { + return connectToHost(host, port, new InetSocketAddress(localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, + int localPort) throws IOException { + Socket socket = createSocket(); + boolean failed = true; + try { + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(address, port)); + failed = false; + } finally { + if (failed) IoUtils.closeQuietly(socket); + } + return socket; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + Socket socket = createSocket(); + boolean failed = true; + try { + socket.connect(new InetSocketAddress(host, port)); + failed = false; + } finally { + if (failed) IoUtils.closeQuietly(socket); + } + return socket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return connectToHost(host, port, null); + } + + @Override + public Socket createSocket() throws IOException { + Socket socket = new Socket(); + boolean failed = true; + try { + bindSocket(socket); + failed = false; + } finally { + if (failed) IoUtils.closeQuietly(socket); + } + return socket; + } + } + + /** + * Returns a {@link SocketFactory} bound to this network. Any {@link Socket} created by + * this factory will have its traffic sent over this {@code Network}. Note that if this + * {@code Network} ever disconnects, this factory and any {@link Socket} it produced in the + * past or future will cease to work. + * + * @return a {@link SocketFactory} which produces {@link Socket} instances bound to this + * {@code Network}. + */ + public SocketFactory getSocketFactory() { + if (mNetworkBoundSocketFactory == null) { + synchronized (mLock) { + if (mNetworkBoundSocketFactory == null) { + mNetworkBoundSocketFactory = new NetworkBoundSocketFactory(); + } + } + } + return mNetworkBoundSocketFactory; + } + + private static HttpURLConnectionFactory createUrlConnectionFactory(Dns dnsLookup) { + // Set configuration on the HttpURLConnectionFactory that will be good for all + // connections created by this Network. Configuration that might vary is left + // until openConnection() and passed as arguments. + HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance(); + urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup + // A private connection pool just for this Network. + urlConnectionFactory.setNewConnectionPool(httpMaxConnections, + httpKeepAliveDurationMs, TimeUnit.MILLISECONDS); + return urlConnectionFactory; + } + + /** + * Opens the specified {@link URL} on this {@code Network}, such that all traffic will be sent + * on this Network. The URL protocol must be {@code HTTP} or {@code HTTPS}. + * + * @return a {@code URLConnection} to the resource referred to by this URL. + * @throws MalformedURLException if the URL protocol is not HTTP or HTTPS. + * @throws IOException if an error occurs while opening the connection. + * @see java.net.URL#openConnection() + */ + public URLConnection openConnection(URL url) throws IOException { + final ConnectivityManager cm = ConnectivityManager.getInstanceOrNull(); + if (cm == null) { + throw new IOException("No ConnectivityManager yet constructed, please construct one"); + } + // TODO: Should this be optimized to avoid fetching the global proxy for every request? + final ProxyInfo proxyInfo = cm.getProxyForNetwork(this); + final java.net.Proxy proxy; + if (proxyInfo != null) { + proxy = proxyInfo.makeProxy(); + } else { + proxy = java.net.Proxy.NO_PROXY; + } + return openConnection(url, proxy); + } + + /** + * Opens the specified {@link URL} on this {@code Network}, such that all traffic will be sent + * on this Network. The URL protocol must be {@code HTTP} or {@code HTTPS}. + * + * @param proxy the proxy through which the connection will be established. + * @return a {@code URLConnection} to the resource referred to by this URL. + * @throws MalformedURLException if the URL protocol is not HTTP or HTTPS. + * @throws IllegalArgumentException if the argument proxy is null. + * @throws IOException if an error occurs while opening the connection. + * @see java.net.URL#openConnection() + */ + public URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException { + if (proxy == null) throw new IllegalArgumentException("proxy is null"); + // TODO: This creates a connection pool and host resolver for + // every Network object, instead of one for every NetId. This is + // suboptimal, because an app could potentially have more than one + // Network object for the same NetId, causing increased memory footprint + // and performance penalties due to lack of connection reuse (connection + // setup time, congestion window growth time, etc.). + // + // Instead, investigate only having one connection pool and host resolver + // for every NetId, perhaps by using a static HashMap of NetIds to + // connection pools and host resolvers. The tricky part is deciding when + // to remove a map entry; a WeakHashMap shouldn't be used because whether + // a Network is referenced doesn't correlate with whether a new Network + // will be instantiated in the near future with the same NetID. A good + // solution would involve purging empty (or when all connections are timed + // out) ConnectionPools. + final HttpURLConnectionFactory urlConnectionFactory; + synchronized (mLock) { + if (mUrlConnectionFactory == null) { + Dns dnsLookup = hostname -> Arrays.asList(getAllByName(hostname)); + mUrlConnectionFactory = createUrlConnectionFactory(dnsLookup); + } + urlConnectionFactory = mUrlConnectionFactory; + } + SocketFactory socketFactory = getSocketFactory(); + return urlConnectionFactory.openConnection(url, socketFactory, proxy); + } + + /** + * Binds the specified {@link DatagramSocket} to this {@code Network}. All data traffic on the + * socket will be sent on this {@code Network}, irrespective of any process-wide network binding + * set by {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be + * connected. + */ + public void bindSocket(DatagramSocket socket) throws IOException { + // Query a property of the underlying socket to ensure that the socket's file descriptor + // exists, is available to bind to a network and is not closed. + socket.getReuseAddress(); + final ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket); + bindSocket(pfd.getFileDescriptor()); + // ParcelFileDescriptor.fromSocket() creates a dup of the original fd. The original and the + // dup share the underlying socket in the kernel. The socket is never truly closed until the + // last fd pointing to the socket being closed. So close the dup one after binding the + // socket to control the lifetime of the dup fd. + pfd.close(); + } + + /** + * Binds the specified {@link Socket} to this {@code Network}. All data traffic on the socket + * will be sent on this {@code Network}, irrespective of any process-wide network binding set by + * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected. + */ + public void bindSocket(Socket socket) throws IOException { + // Query a property of the underlying socket to ensure that the socket's file descriptor + // exists, is available to bind to a network and is not closed. + socket.getReuseAddress(); + final ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + bindSocket(pfd.getFileDescriptor()); + // ParcelFileDescriptor.fromSocket() creates a dup of the original fd. The original and the + // dup share the underlying socket in the kernel. The socket is never truly closed until the + // last fd pointing to the socket being closed. So close the dup one after binding the + // socket to control the lifetime of the dup fd. + pfd.close(); + } + + /** + * Binds the specified {@link FileDescriptor} to this {@code Network}. All data traffic on the + * socket represented by this file descriptor will be sent on this {@code Network}, + * irrespective of any process-wide network binding set by + * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected. + */ + public void bindSocket(FileDescriptor fd) throws IOException { + try { + final SocketAddress peer = Os.getpeername(fd); + final InetAddress inetPeer = ((InetSocketAddress) peer).getAddress(); + if (!inetPeer.isAnyLocalAddress()) { + // Apparently, the kernel doesn't update a connected UDP socket's + // routing upon mark changes. + throw new SocketException("Socket is connected"); + } + } catch (ErrnoException e) { + // getpeername() failed. + if (e.errno != OsConstants.ENOTCONN) { + throw e.rethrowAsSocketException(); + } + } catch (ClassCastException e) { + // Wasn't an InetSocketAddress. + throw new SocketException("Only AF_INET/AF_INET6 sockets supported"); + } + + final int err = NetworkUtils.bindSocketToNetwork(fd, netId); + if (err != 0) { + // bindSocketToNetwork returns negative errno. + throw new ErrnoException("Binding socket to network " + netId, -err) + .rethrowAsSocketException(); + } + } + + /** + * Returns a {@link Network} object given a handle returned from {@link #getNetworkHandle}. + * + * @param networkHandle a handle returned from {@link #getNetworkHandle}. + * @return A {@link Network} object derived from {@code networkHandle}. + */ + public static Network fromNetworkHandle(long networkHandle) { + if (networkHandle == 0) { + throw new IllegalArgumentException( + "Network.fromNetworkHandle refusing to instantiate NETID_UNSET Network."); + } + if ((networkHandle & ((1L << HANDLE_MAGIC_SIZE) - 1)) != HANDLE_MAGIC) { + throw new IllegalArgumentException( + "Value passed to fromNetworkHandle() is not a network handle."); + } + final int netIdForResolv = (int) (networkHandle >>> HANDLE_MAGIC_SIZE); + return new Network((netIdForResolv & ~USE_LOCAL_NAMESERVERS_FLAG), + ((netIdForResolv & USE_LOCAL_NAMESERVERS_FLAG) != 0) /* privateDnsBypass */); + } + + /** + * Returns a handle representing this {@code Network}, for use with the NDK API. + */ + public long getNetworkHandle() { + // The network handle is explicitly not the same as the netId. + // + // The netId is an implementation detail which might be changed in the + // future, or which alone (i.e. in the absence of some additional + // context) might not be sufficient to fully identify a Network. + // + // As such, the intention is to prevent accidental misuse of the API + // that might result if a developer assumed that handles and netIds + // were identical and passing a netId to a call expecting a handle + // "just worked". Such accidental misuse, if widely deployed, might + // prevent future changes to the semantics of the netId field or + // inhibit the expansion of state required for Network objects. + // + // This extra layer of indirection might be seen as paranoia, and might + // never end up being necessary, but the added complexity is trivial. + // At some future date it may be desirable to realign the handle with + // Multiple Provisioning Domains API recommendations, as made by the + // IETF mif working group. + if (netId == 0) { + return 0L; // make this zero condition obvious for debugging + } + return (((long) getNetIdForResolv()) << HANDLE_MAGIC_SIZE) | HANDLE_MAGIC; + } + + // implement the Parcelable interface + public int describeContents() { + return 0; + } + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(netId); + } + + public static final @android.annotation.NonNull Creator<Network> CREATOR = + new Creator<Network>() { + public Network createFromParcel(Parcel in) { + int netId = in.readInt(); + + return new Network(netId); + } + + public Network[] newArray(int size) { + return new Network[size]; + } + }; + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof Network)) return false; + Network other = (Network)obj; + return this.netId == other.netId; + } + + @Override + public int hashCode() { + return netId * 11; + } + + @Override + public String toString() { + return Integer.toString(netId); + } +} diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java new file mode 100644 index 0000000000000000000000000000000000000000..adcf338ba1b3dfe446e95fb86cfbd3766aa172d7 --- /dev/null +++ b/framework/src/android/net/NetworkAgent.java @@ -0,0 +1,1324 @@ +/* + * 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 android.net; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A utility class for handling for communicating between bearer-specific + * code and ConnectivityService. + * + * An agent manages the life cycle of a network. A network starts its + * life cycle when {@link register} is called on NetworkAgent. The network + * is then connecting. When full L3 connectivity has been established, + * the agent should call {@link markConnected} to inform the system that + * this network is ready to use. When the network disconnects its life + * ends and the agent should call {@link unregister}, at which point the + * system will clean up and free resources. + * Any reconnection becomes a new logical network, so after a network + * is disconnected the agent cannot be used any more. Network providers + * should create a new NetworkAgent instance to handle new connections. + * + * A bearer may have more than one NetworkAgent if it can simultaneously + * support separate networks (IMS / Internet / MMS Apns on cellular, or + * perhaps connections with different SSID or P2P for Wi-Fi). + * + * This class supports methods to start and stop sending keepalive packets. + * Keepalive packets are typically sent at periodic intervals over a network + * with NAT when there is no other traffic to avoid the network forcefully + * closing the connection. NetworkAgents that manage technologies that + * have hardware support for keepalive should implement the related + * methods to save battery life. NetworkAgent that cannot get support + * without waking up the CPU should not, as this would be prohibitive in + * terms of battery - these agents should simply not override the related + * methods, which results in the implementation returning + * {@link SocketKeepalive.ERROR_UNSUPPORTED} as appropriate. + * + * Keepalive packets need to be sent at relatively frequent intervals + * (a few seconds to a few minutes). As the contents of keepalive packets + * depend on the current network status, hardware needs to be configured + * to send them and has a limited amount of memory to do so. The HAL + * formalizes this as slots that an implementation can configure to send + * the correct packets. Devices typically have a small number of slots + * per radio technology, and the specific number of slots for each + * technology is specified in configuration files. + * {@see SocketKeepalive} for details. + * + * @hide + */ +@SystemApi +public abstract class NetworkAgent { + /** + * The {@link Network} corresponding to this object. + */ + @Nullable + private volatile Network mNetwork; + + @Nullable + private volatile INetworkAgentRegistry mRegistry; + + private interface RegistryAction { + void execute(@NonNull INetworkAgentRegistry registry) throws RemoteException; + } + + private final Handler mHandler; + private final String LOG_TAG; + private static final boolean DBG = true; + private static final boolean VDBG = false; + /** @hide */ + @TestApi + public static final int MIN_LINGER_TIMER_MS = 2000; + private final ArrayList<RegistryAction> mPreConnectedQueue = new ArrayList<>(); + private volatile long mLastBwRefreshTime = 0; + private static final long BW_REFRESH_MIN_WIN_MS = 500; + private boolean mBandwidthUpdateScheduled = false; + private AtomicBoolean mBandwidthUpdatePending = new AtomicBoolean(false); + @NonNull + private NetworkInfo mNetworkInfo; + @NonNull + private final Object mRegisterLock = new Object(); + + /** + * The ID of the {@link NetworkProvider} that created this object, or + * {@link NetworkProvider#ID_NONE} if unknown. + * @hide + */ + public final int providerId; + + // ConnectivityService parses message constants from itself and NetworkAgent with MessageUtils + // for debugging purposes, and crashes if some messages have the same values. + // TODO: have ConnectivityService store message names in different maps and remove this base + private static final int BASE = 200; + + /** + * Sent by ConnectivityService to the NetworkAgent to inform it of + * suspected connectivity problems on its network. The NetworkAgent + * should take steps to verify and correct connectivity. + * @hide + */ + public static final int CMD_SUSPECT_BAD = BASE; + + /** + * Sent by the NetworkAgent (note the EVENT vs CMD prefix) to + * ConnectivityService to pass the current NetworkInfo (connection state). + * Sent when the NetworkInfo changes, mainly due to change of state. + * obj = NetworkInfo + * @hide + */ + public static final int EVENT_NETWORK_INFO_CHANGED = BASE + 1; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * NetworkCapabilties. + * obj = NetworkCapabilities + * @hide + */ + public static final int EVENT_NETWORK_CAPABILITIES_CHANGED = BASE + 2; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * NetworkProperties. + * obj = NetworkProperties + * @hide + */ + public static final int EVENT_NETWORK_PROPERTIES_CHANGED = BASE + 3; + + /** + * Centralize the place where base network score, and network score scaling, will be + * stored, so as we can consistently compare apple and oranges, or wifi, ethernet and LTE + * @hide + */ + public static final int WIFI_BASE_SCORE = 60; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * network score. + * arg1 = network score int + * @hide + */ + public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * list of underlying networks. + * obj = array of Network objects + * @hide + */ + public static final int EVENT_UNDERLYING_NETWORKS_CHANGED = BASE + 5; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current value of the teardown + * delay. + * arg1 = teardown delay in milliseconds + * @hide + */ + public static final int EVENT_TEARDOWN_DELAY_CHANGED = BASE + 6; + + /** + * The maximum value for the teardown delay, in milliseconds. + * @hide + */ + public static final int MAX_TEARDOWN_DELAY_MS = 5000; + + /** + * Sent by ConnectivityService to the NetworkAgent to inform the agent of the + * networks status - whether we could use the network or could not, due to + * either a bad network configuration (no internet link) or captive portal. + * + * arg1 = either {@code VALID_NETWORK} or {@code INVALID_NETWORK} + * obj = Bundle containing map from {@code REDIRECT_URL_KEY} to {@code String} + * representing URL that Internet probe was redirect to, if it was redirected, + * or mapping to {@code null} otherwise. + * @hide + */ + public static final int CMD_REPORT_NETWORK_STATUS = BASE + 7; + + /** + * Network validation suceeded. + * Corresponds to {@link NetworkCapabilities.NET_CAPABILITY_VALIDATED}. + */ + public static final int VALIDATION_STATUS_VALID = 1; + + /** + * Network validation was attempted and failed. This may be received more than once as + * subsequent validation attempts are made. + */ + public static final int VALIDATION_STATUS_NOT_VALID = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "VALIDATION_STATUS_" }, value = { + VALIDATION_STATUS_VALID, + VALIDATION_STATUS_NOT_VALID + }) + public @interface ValidationStatus {} + + // TODO: remove. + /** @hide */ + public static final int VALID_NETWORK = 1; + /** @hide */ + public static final int INVALID_NETWORK = 2; + + /** + * The key for the redirect URL in the Bundle argument of {@code CMD_REPORT_NETWORK_STATUS}. + * @hide + */ + public static final String REDIRECT_URL_KEY = "redirect URL"; + + /** + * Sent by the NetworkAgent to ConnectivityService to indicate this network was + * explicitly selected. This should be sent before the NetworkInfo is marked + * CONNECTED so it can be given special treatment at that time. + * + * obj = boolean indicating whether to use this network even if unvalidated + * @hide + */ + public static final int EVENT_SET_EXPLICITLY_SELECTED = BASE + 8; + + /** + * Sent by ConnectivityService to the NetworkAgent to inform the agent of + * whether the network should in the future be used even if not validated. + * This decision is made by the user, but it is the network transport's + * responsibility to remember it. + * + * arg1 = 1 if true, 0 if false + * @hide + */ + public static final int CMD_SAVE_ACCEPT_UNVALIDATED = BASE + 9; + + /** + * Sent by ConnectivityService to the NetworkAgent to inform the agent to pull + * the underlying network connection for updated bandwidth information. + * @hide + */ + public static final int CMD_REQUEST_BANDWIDTH_UPDATE = BASE + 10; + + /** + * Sent by ConnectivityService to the NetworkAgent to request that the specified packet be sent + * periodically on the given interval. + * + * arg1 = the hardware slot number of the keepalive to start + * arg2 = interval in seconds + * obj = KeepalivePacketData object describing the data to be sent + * + * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics. + * @hide + */ + public static final int CMD_START_SOCKET_KEEPALIVE = BASE + 11; + + /** + * Requests that the specified keepalive packet be stopped. + * + * arg1 = hardware slot number of the keepalive to stop. + * + * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics. + * @hide + */ + public static final int CMD_STOP_SOCKET_KEEPALIVE = BASE + 12; + + /** + * Sent by the NetworkAgent to ConnectivityService to provide status on a socket keepalive + * request. This may either be the reply to a CMD_START_SOCKET_KEEPALIVE, or an asynchronous + * error notification. + * + * This is also sent by KeepaliveTracker to the app's {@link SocketKeepalive}, + * so that the app's {@link SocketKeepalive.Callback} methods can be called. + * + * arg1 = hardware slot number of the keepalive + * arg2 = error code + * @hide + */ + public static final int EVENT_SOCKET_KEEPALIVE = BASE + 13; + + /** + * Sent by ConnectivityService to inform this network transport of signal strength thresholds + * that when crossed should trigger a system wakeup and a NetworkCapabilities update. + * + * obj = int[] describing signal strength thresholds. + * @hide + */ + public static final int CMD_SET_SIGNAL_STRENGTH_THRESHOLDS = BASE + 14; + + /** + * Sent by ConnectivityService to the NeworkAgent to inform the agent to avoid + * automatically reconnecting to this network (e.g. via autojoin). Happens + * when user selects "No" option on the "Stay connected?" dialog box. + * @hide + */ + public static final int CMD_PREVENT_AUTOMATIC_RECONNECT = BASE + 15; + + /** + * Sent by the KeepaliveTracker to NetworkAgent to add a packet filter. + * + * For TCP keepalive offloads, keepalive packets are sent by the firmware. However, because the + * remote site will send ACK packets in response to the keepalive packets, the firmware also + * needs to be configured to properly filter the ACKs to prevent the system from waking up. + * This does not happen with UDP, so this message is TCP-specific. + * arg1 = hardware slot number of the keepalive to filter for. + * obj = the keepalive packet to send repeatedly. + * @hide + */ + public static final int CMD_ADD_KEEPALIVE_PACKET_FILTER = BASE + 16; + + /** + * Sent by the KeepaliveTracker to NetworkAgent to remove a packet filter. See + * {@link #CMD_ADD_KEEPALIVE_PACKET_FILTER}. + * arg1 = hardware slot number of the keepalive packet filter to remove. + * @hide + */ + public static final int CMD_REMOVE_KEEPALIVE_PACKET_FILTER = BASE + 17; + + /** + * Sent by ConnectivityService to the NetworkAgent to complete the bidirectional connection. + * obj = INetworkAgentRegistry + */ + private static final int EVENT_AGENT_CONNECTED = BASE + 18; + + /** + * Sent by ConnectivityService to the NetworkAgent to inform the agent that it was disconnected. + */ + private static final int EVENT_AGENT_DISCONNECTED = BASE + 19; + + /** + * Sent by QosCallbackTracker to {@link NetworkAgent} to register a new filter with + * callback. + * + * arg1 = QoS agent callback ID + * obj = {@link QosFilter} + * @hide + */ + public static final int CMD_REGISTER_QOS_CALLBACK = BASE + 20; + + /** + * Sent by QosCallbackTracker to {@link NetworkAgent} to unregister a callback. + * + * arg1 = QoS agent callback ID + * @hide + */ + public static final int CMD_UNREGISTER_QOS_CALLBACK = BASE + 21; + + /** + * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent that its native + * network was created and the Network object is now valid. + * + * @hide + */ + public static final int CMD_NETWORK_CREATED = BASE + 22; + + /** + * Sent by ConnectivityService to {@link NetworkAgent} to inform the agent that its native + * network was destroyed. + * + * @hide + */ + public static final int CMD_NETWORK_DESTROYED = BASE + 23; + + /** + * Sent by the NetworkAgent to ConnectivityService to set the linger duration for this network + * agent. + * arg1 = the linger duration, represents by {@link Duration}. + * + * @hide + */ + public static final int EVENT_LINGER_DURATION_CHANGED = BASE + 24; + + private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) { + final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType, + config.legacyTypeName, config.legacySubTypeName); + ni.setIsAvailable(true); + ni.setDetailedState(NetworkInfo.DetailedState.CONNECTING, null /* reason */, + config.getLegacyExtraInfo()); + return ni; + } + + // Temporary backward compatibility constructor + public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag, + @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp, int score, + @NonNull NetworkAgentConfig config, @Nullable NetworkProvider provider) { + this(context, looper, logTag, nc, lp, + new NetworkScore.Builder().setLegacyInt(score).build(), config, provider); + } + + /** + * Create a new network agent. + * @param context a {@link Context} to get system services from. + * @param looper the {@link Looper} on which to invoke the callbacks. + * @param logTag the tag for logs + * @param nc the initial {@link NetworkCapabilities} of this network. Update with + * sendNetworkCapabilities. + * @param lp the initial {@link LinkProperties} of this network. Update with sendLinkProperties. + * @param score the initial score of this network. Update with sendNetworkScore. + * @param config an immutable {@link NetworkAgentConfig} for this agent. + * @param provider the {@link NetworkProvider} managing this agent. + */ + public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag, + @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp, + @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, + @Nullable NetworkProvider provider) { + this(looper, context, logTag, nc, lp, score, config, + provider == null ? NetworkProvider.ID_NONE : provider.getProviderId(), + getLegacyNetworkInfo(config)); + } + + private static class InitialConfiguration { + public final Context context; + public final NetworkCapabilities capabilities; + public final LinkProperties properties; + public final NetworkScore score; + public final NetworkAgentConfig config; + public final NetworkInfo info; + InitialConfiguration(@NonNull Context context, @NonNull NetworkCapabilities capabilities, + @NonNull LinkProperties properties, @NonNull NetworkScore score, + @NonNull NetworkAgentConfig config, @NonNull NetworkInfo info) { + this.context = context; + this.capabilities = capabilities; + this.properties = properties; + this.score = score; + this.config = config; + this.info = info; + } + } + private volatile InitialConfiguration mInitialConfiguration; + + private NetworkAgent(@NonNull Looper looper, @NonNull Context context, @NonNull String logTag, + @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp, + @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId, + @NonNull NetworkInfo ni) { + mHandler = new NetworkAgentHandler(looper); + LOG_TAG = logTag; + mNetworkInfo = new NetworkInfo(ni); + this.providerId = providerId; + if (ni == null || nc == null || lp == null) { + throw new IllegalArgumentException(); + } + + mInitialConfiguration = new InitialConfiguration(context, + new NetworkCapabilities(nc, NetworkCapabilities.REDACT_NONE), + new LinkProperties(lp), score, config, ni); + } + + private class NetworkAgentHandler extends Handler { + NetworkAgentHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_AGENT_CONNECTED: { + if (mRegistry != null) { + log("Received new connection while already connected!"); + } else { + if (VDBG) log("NetworkAgent fully connected"); + synchronized (mPreConnectedQueue) { + final INetworkAgentRegistry registry = (INetworkAgentRegistry) msg.obj; + mRegistry = registry; + for (RegistryAction a : mPreConnectedQueue) { + try { + a.execute(registry); + } catch (RemoteException e) { + Log.wtf(LOG_TAG, "Communication error with registry", e); + // Fall through + } + } + mPreConnectedQueue.clear(); + } + } + break; + } + case EVENT_AGENT_DISCONNECTED: { + if (DBG) log("NetworkAgent channel lost"); + // let the client know CS is done with us. + onNetworkUnwanted(); + synchronized (mPreConnectedQueue) { + mRegistry = null; + } + break; + } + case CMD_SUSPECT_BAD: { + log("Unhandled Message " + msg); + break; + } + case CMD_REQUEST_BANDWIDTH_UPDATE: { + long currentTimeMs = System.currentTimeMillis(); + if (VDBG) { + log("CMD_REQUEST_BANDWIDTH_UPDATE request received."); + } + if (currentTimeMs >= (mLastBwRefreshTime + BW_REFRESH_MIN_WIN_MS)) { + mBandwidthUpdateScheduled = false; + if (!mBandwidthUpdatePending.getAndSet(true)) { + onBandwidthUpdateRequested(); + } + } else { + // deliver the request at a later time rather than discard it completely. + if (!mBandwidthUpdateScheduled) { + long waitTime = mLastBwRefreshTime + BW_REFRESH_MIN_WIN_MS + - currentTimeMs + 1; + mBandwidthUpdateScheduled = sendEmptyMessageDelayed( + CMD_REQUEST_BANDWIDTH_UPDATE, waitTime); + } + } + break; + } + case CMD_REPORT_NETWORK_STATUS: { + String redirectUrl = ((Bundle) msg.obj).getString(REDIRECT_URL_KEY); + if (VDBG) { + log("CMD_REPORT_NETWORK_STATUS(" + + (msg.arg1 == VALID_NETWORK ? "VALID, " : "INVALID, ") + + redirectUrl); + } + Uri uri = null; + try { + if (null != redirectUrl) { + uri = Uri.parse(redirectUrl); + } + } catch (Exception e) { + Log.wtf(LOG_TAG, "Surprising URI : " + redirectUrl, e); + } + onValidationStatus(msg.arg1 /* status */, uri); + break; + } + case CMD_SAVE_ACCEPT_UNVALIDATED: { + onSaveAcceptUnvalidated(msg.arg1 != 0); + break; + } + case CMD_START_SOCKET_KEEPALIVE: { + onStartSocketKeepalive(msg.arg1 /* slot */, + Duration.ofSeconds(msg.arg2) /* interval */, + (KeepalivePacketData) msg.obj /* packet */); + break; + } + case CMD_STOP_SOCKET_KEEPALIVE: { + onStopSocketKeepalive(msg.arg1 /* slot */); + break; + } + + case CMD_SET_SIGNAL_STRENGTH_THRESHOLDS: { + onSignalStrengthThresholdsUpdated((int[]) msg.obj); + break; + } + case CMD_PREVENT_AUTOMATIC_RECONNECT: { + onAutomaticReconnectDisabled(); + break; + } + case CMD_ADD_KEEPALIVE_PACKET_FILTER: { + onAddKeepalivePacketFilter(msg.arg1 /* slot */, + (KeepalivePacketData) msg.obj /* packet */); + break; + } + case CMD_REMOVE_KEEPALIVE_PACKET_FILTER: { + onRemoveKeepalivePacketFilter(msg.arg1 /* slot */); + break; + } + case CMD_REGISTER_QOS_CALLBACK: { + onQosCallbackRegistered( + msg.arg1 /* QoS callback id */, + (QosFilter) msg.obj /* QoS filter */); + break; + } + case CMD_UNREGISTER_QOS_CALLBACK: { + onQosCallbackUnregistered( + msg.arg1 /* QoS callback id */); + break; + } + case CMD_NETWORK_CREATED: { + onNetworkCreated(); + break; + } + case CMD_NETWORK_DESTROYED: { + onNetworkDestroyed(); + break; + } + } + } + } + + /** + * Register this network agent with ConnectivityService. + * + * This method can only be called once per network agent. + * + * @return the Network associated with this network agent (which can also be obtained later + * by calling getNetwork() on this agent). + * @throws IllegalStateException thrown by the system server if this network agent is + * already registered. + */ + @NonNull + public Network register() { + if (VDBG) log("Registering NetworkAgent"); + synchronized (mRegisterLock) { + if (mNetwork != null) { + throw new IllegalStateException("Agent already registered"); + } + final ConnectivityManager cm = (ConnectivityManager) mInitialConfiguration.context + .getSystemService(Context.CONNECTIVITY_SERVICE); + mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler), + new NetworkInfo(mInitialConfiguration.info), + mInitialConfiguration.properties, mInitialConfiguration.capabilities, + mInitialConfiguration.score, mInitialConfiguration.config, providerId); + mInitialConfiguration = null; // All this memory can now be GC'd + } + return mNetwork; + } + + private static class NetworkAgentBinder extends INetworkAgent.Stub { + private static final String LOG_TAG = NetworkAgentBinder.class.getSimpleName(); + + private final Handler mHandler; + + private NetworkAgentBinder(Handler handler) { + mHandler = handler; + } + + @Override + public void onRegistered(@NonNull INetworkAgentRegistry registry) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_AGENT_CONNECTED, registry)); + } + + @Override + public void onDisconnected() { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_AGENT_DISCONNECTED)); + } + + @Override + public void onBandwidthUpdateRequested() { + mHandler.sendMessage(mHandler.obtainMessage(CMD_REQUEST_BANDWIDTH_UPDATE)); + } + + @Override + public void onValidationStatusChanged( + int validationStatus, @Nullable String captivePortalUrl) { + // TODO: consider using a parcelable as argument when the interface is structured + Bundle redirectUrlBundle = new Bundle(); + redirectUrlBundle.putString(NetworkAgent.REDIRECT_URL_KEY, captivePortalUrl); + mHandler.sendMessage(mHandler.obtainMessage(CMD_REPORT_NETWORK_STATUS, + validationStatus, 0, redirectUrlBundle)); + } + + @Override + public void onSaveAcceptUnvalidated(boolean acceptUnvalidated) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_SAVE_ACCEPT_UNVALIDATED, + acceptUnvalidated ? 1 : 0, 0)); + } + + @Override + public void onStartNattSocketKeepalive(int slot, int intervalDurationMs, + @NonNull NattKeepalivePacketData packetData) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, + slot, intervalDurationMs, packetData)); + } + + @Override + public void onStartTcpSocketKeepalive(int slot, int intervalDurationMs, + @NonNull TcpKeepalivePacketData packetData) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, + slot, intervalDurationMs, packetData)); + } + + @Override + public void onStopSocketKeepalive(int slot) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_STOP_SOCKET_KEEPALIVE, slot, 0)); + } + + @Override + public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) { + mHandler.sendMessage(mHandler.obtainMessage( + CMD_SET_SIGNAL_STRENGTH_THRESHOLDS, thresholds)); + } + + @Override + public void onPreventAutomaticReconnect() { + mHandler.sendMessage(mHandler.obtainMessage(CMD_PREVENT_AUTOMATIC_RECONNECT)); + } + + @Override + public void onAddNattKeepalivePacketFilter(int slot, + @NonNull NattKeepalivePacketData packetData) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, + slot, 0, packetData)); + } + + @Override + public void onAddTcpKeepalivePacketFilter(int slot, + @NonNull TcpKeepalivePacketData packetData) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, + slot, 0, packetData)); + } + + @Override + public void onRemoveKeepalivePacketFilter(int slot) { + mHandler.sendMessage(mHandler.obtainMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, + slot, 0)); + } + + @Override + public void onQosFilterCallbackRegistered(final int qosCallbackId, + final QosFilterParcelable qosFilterParcelable) { + if (qosFilterParcelable.getQosFilter() != null) { + mHandler.sendMessage( + mHandler.obtainMessage(CMD_REGISTER_QOS_CALLBACK, qosCallbackId, 0, + qosFilterParcelable.getQosFilter())); + return; + } + + Log.wtf(LOG_TAG, "onQosFilterCallbackRegistered: qos filter is null."); + } + + @Override + public void onQosCallbackUnregistered(final int qosCallbackId) { + mHandler.sendMessage(mHandler.obtainMessage( + CMD_UNREGISTER_QOS_CALLBACK, qosCallbackId, 0, null)); + } + + @Override + public void onNetworkCreated() { + mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_CREATED)); + } + + @Override + public void onNetworkDestroyed() { + mHandler.sendMessage(mHandler.obtainMessage(CMD_NETWORK_DESTROYED)); + } + } + + /** + * Register this network agent with a testing harness. + * + * The returned Messenger sends messages to the Handler. This allows a test to send + * this object {@code CMD_*} messages as if they came from ConnectivityService, which + * is useful for testing the behavior. + * + * @hide + */ + public INetworkAgent registerForTest(final Network network) { + log("Registering NetworkAgent for test"); + synchronized (mRegisterLock) { + mNetwork = network; + mInitialConfiguration = null; + } + return new NetworkAgentBinder(mHandler); + } + + /** + * Waits for the handler to be idle. + * This is useful for testing, and has smaller scope than an accessor to mHandler. + * TODO : move the implementation in common library with the tests + * @hide + */ + @VisibleForTesting + public boolean waitForIdle(final long timeoutMs) { + final ConditionVariable cv = new ConditionVariable(false); + mHandler.post(cv::open); + return cv.block(timeoutMs); + } + + /** + * @return The Network associated with this agent, or null if it's not registered yet. + */ + @Nullable + public Network getNetwork() { + return mNetwork; + } + + private void queueOrSendMessage(@NonNull RegistryAction action) { + synchronized (mPreConnectedQueue) { + if (mRegistry != null) { + try { + action.execute(mRegistry); + } catch (RemoteException e) { + Log.wtf(LOG_TAG, "Error executing registry action", e); + // Fall through: the channel is asynchronous and does not report errors back + } + } else { + mPreConnectedQueue.add(action); + } + } + } + + /** + * Must be called by the agent when the network's {@link LinkProperties} change. + * @param linkProperties the new LinkProperties. + */ + public final void sendLinkProperties(@NonNull LinkProperties linkProperties) { + Objects.requireNonNull(linkProperties); + final LinkProperties lp = new LinkProperties(linkProperties); + queueOrSendMessage(reg -> reg.sendLinkProperties(lp)); + } + + /** + * Must be called by the agent when the network's underlying networks change. + * + * <p>{@code networks} is one of the following: + * <ul> + * <li><strong>a non-empty array</strong>: an array of one or more {@link Network}s, in + * decreasing preference order. For example, if this VPN uses both wifi and mobile (cellular) + * networks to carry app traffic, but prefers or uses wifi more than mobile, wifi should appear + * first in the array.</li> + * <li><strong>an empty array</strong>: a zero-element array, meaning that the VPN has no + * underlying network connection, and thus, app traffic will not be sent or received.</li> + * <li><strong>null</strong>: (default) signifies that the VPN uses whatever is the system's + * default network. I.e., it doesn't use the {@code bindSocket} or {@code bindDatagramSocket} + * APIs mentioned above to send traffic over specific channels.</li> + * </ul> + * + * @param underlyingNetworks the new list of underlying networks. + * @see {@link VpnService.Builder#setUnderlyingNetworks(Network[])} + */ + public final void setUnderlyingNetworks( + @SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) { + final ArrayList<Network> underlyingArray = (underlyingNetworks != null) + ? new ArrayList<>(underlyingNetworks) : null; + queueOrSendMessage(reg -> reg.sendUnderlyingNetworks(underlyingArray)); + } + + /** + * Inform ConnectivityService that this agent has now connected. + * Call {@link #unregister} to disconnect. + */ + public void markConnected() { + mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null /* reason */, + mNetworkInfo.getExtraInfo()); + queueOrSendNetworkInfo(mNetworkInfo); + } + + /** + * Unregister this network agent. + * + * This signals the network has disconnected and ends its lifecycle. After this is called, + * the network is torn down and this agent can no longer be used. + */ + public void unregister() { + // When unregistering an agent nobody should use the extrainfo (or reason) any more. + mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, null /* reason */, + null /* extraInfo */); + queueOrSendNetworkInfo(mNetworkInfo); + } + + /** + * Sets the value of the teardown delay. + * + * The teardown delay is the time between when the network disconnects and when the native + * network corresponding to this {@code NetworkAgent} is destroyed. By default, the native + * network is destroyed immediately. If {@code teardownDelayMs} is non-zero, then when this + * network disconnects, the system will instead immediately mark the network as restricted + * and unavailable to unprivileged apps, but will defer destroying the native network until the + * teardown delay timer expires. + * + * The interfaces in use by this network will remain in use until the native network is + * destroyed and cannot be reused until {@link #onNetworkDestroyed()} is called. + * + * This method may be called at any time while the network is connected. It has no effect if + * the network is already disconnected and the teardown delay timer is running. + * + * @param teardownDelayMillis the teardown delay to set, or 0 to disable teardown delay. + */ + public void setTeardownDelayMillis( + @IntRange(from = 0, to = MAX_TEARDOWN_DELAY_MS) int teardownDelayMillis) { + queueOrSendMessage(reg -> reg.sendTeardownDelayMs(teardownDelayMillis)); + } + + /** + * Change the legacy subtype of this network agent. + * + * This is only for backward compatibility and should not be used by non-legacy network agents, + * or agents that did not use to set a subtype. As such, only TYPE_MOBILE type agents can use + * this and others will be thrown an exception if they try. + * + * @deprecated this is for backward compatibility only. + * @param legacySubtype the legacy subtype. + * @hide + */ + @Deprecated + @SystemApi + public void setLegacySubtype(final int legacySubtype, @NonNull final String legacySubtypeName) { + mNetworkInfo.setSubtype(legacySubtype, legacySubtypeName); + queueOrSendNetworkInfo(mNetworkInfo); + } + + /** + * Set the ExtraInfo of this network agent. + * + * This sets the ExtraInfo field inside the NetworkInfo returned by legacy public API and the + * broadcasts about the corresponding Network. + * This is only for backward compatibility and should not be used by non-legacy network agents, + * who will be thrown an exception if they try. The extra info should only be : + * <ul> + * <li>For cellular agents, the APN name.</li> + * <li>For ethernet agents, the interface name.</li> + * </ul> + * + * @deprecated this is for backward compatibility only. + * @param extraInfo the ExtraInfo. + * @hide + */ + @Deprecated + public void setLegacyExtraInfo(@Nullable final String extraInfo) { + mNetworkInfo.setExtraInfo(extraInfo); + queueOrSendNetworkInfo(mNetworkInfo); + } + + /** + * Must be called by the agent when it has a new NetworkInfo object. + * @hide TODO: expose something better. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public final void sendNetworkInfo(NetworkInfo networkInfo) { + queueOrSendNetworkInfo(new NetworkInfo(networkInfo)); + } + + private void queueOrSendNetworkInfo(NetworkInfo networkInfo) { + queueOrSendMessage(reg -> reg.sendNetworkInfo(networkInfo)); + } + + /** + * Must be called by the agent when the network's {@link NetworkCapabilities} change. + * @param networkCapabilities the new NetworkCapabilities. + */ + public final void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) { + Objects.requireNonNull(networkCapabilities); + mBandwidthUpdatePending.set(false); + mLastBwRefreshTime = System.currentTimeMillis(); + final NetworkCapabilities nc = + new NetworkCapabilities(networkCapabilities, NetworkCapabilities.REDACT_NONE); + queueOrSendMessage(reg -> reg.sendNetworkCapabilities(nc)); + } + + /** + * Must be called by the agent to update the score of this network. + * + * @param score the new score. + */ + public final void sendNetworkScore(@NonNull NetworkScore score) { + Objects.requireNonNull(score); + queueOrSendMessage(reg -> reg.sendScore(score)); + } + + /** + * Must be called by the agent to update the score of this network. + * + * @param score the new score, between 0 and 99. + * deprecated use sendNetworkScore(NetworkScore) TODO : remove in S. + */ + public final void sendNetworkScore(@IntRange(from = 0, to = 99) int score) { + sendNetworkScore(new NetworkScore.Builder().setLegacyInt(score).build()); + } + + /** + * Must be called by the agent to indicate this network was manually selected by the user. + * This should be called before the NetworkInfo is marked CONNECTED so that this + * Network can be given special treatment at that time. If {@code acceptUnvalidated} is + * {@code true}, then the system will switch to this network. If it is {@code false} and the + * network cannot be validated, the system will ask the user whether to switch to this network. + * If the user confirms and selects "don't ask again", then the system will call + * {@link #saveAcceptUnvalidated} to persist the user's choice. Thus, if the transport ever + * calls this method with {@code acceptUnvalidated} set to {@code false}, it must also implement + * {@link #saveAcceptUnvalidated} to respect the user's choice. + * @hide should move to NetworkAgentConfig. + */ + public void explicitlySelected(boolean acceptUnvalidated) { + explicitlySelected(true /* explicitlySelected */, acceptUnvalidated); + } + + /** + * Must be called by the agent to indicate whether the network was manually selected by the + * user. This should be called before the network becomes connected, so it can be given + * special treatment when it does. + * + * If {@code explicitlySelected} is {@code true}, and {@code acceptUnvalidated} is {@code true}, + * then the system will switch to this network. If {@code explicitlySelected} is {@code true} + * and {@code acceptUnvalidated} is {@code false}, and the network cannot be validated, the + * system will ask the user whether to switch to this network. If the user confirms and selects + * "don't ask again", then the system will call {@link #saveAcceptUnvalidated} to persist the + * user's choice. Thus, if the transport ever calls this method with {@code explicitlySelected} + * set to {@code true} and {@code acceptUnvalidated} set to {@code false}, it must also + * implement {@link #saveAcceptUnvalidated} to respect the user's choice. + * + * If {@code explicitlySelected} is {@code false} and {@code acceptUnvalidated} is + * {@code true}, the system will interpret this as the user having accepted partial connectivity + * on this network. Thus, the system will switch to the network and consider it validated even + * if it only provides partial connectivity, but the network is not otherwise treated specially. + * @hide should move to NetworkAgentConfig. + */ + public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) { + queueOrSendMessage(reg -> reg.sendExplicitlySelected( + explicitlySelected, acceptUnvalidated)); + } + + /** + * Called when ConnectivityService has indicated they no longer want this network. + * The parent factory should (previously) have received indication of the change + * as well, either canceling NetworkRequests or altering their score such that this + * network won't be immediately requested again. + */ + public void onNetworkUnwanted() { + unwanted(); + } + /** @hide TODO delete once subclasses have moved to onNetworkUnwanted. */ + protected void unwanted() { + } + + /** + * Called when ConnectivityService request a bandwidth update. The parent factory + * shall try to overwrite this method and produce a bandwidth update if capable. + * @hide + */ + @SystemApi + public void onBandwidthUpdateRequested() { + pollLceData(); + } + /** @hide TODO delete once subclasses have moved to onBandwidthUpdateRequested. */ + protected void pollLceData() { + } + + /** + * Called when the system determines the usefulness of this network. + * + * The system attempts to validate Internet connectivity on networks that provide the + * {@link NetworkCapabilities#NET_CAPABILITY_INTERNET} capability. + * + * Currently there are two possible values: + * {@code VALIDATION_STATUS_VALID} if Internet connectivity was validated, + * {@code VALIDATION_STATUS_NOT_VALID} if Internet connectivity was not validated. + * + * This is guaranteed to be called again when the network status changes, but the system + * may also call this multiple times even if the status does not change. + * + * @param status one of {@code VALIDATION_STATUS_VALID} or {@code VALIDATION_STATUS_NOT_VALID}. + * @param redirectUri If Internet connectivity is being redirected (e.g., on a captive portal), + * this is the destination the probes are being redirected to, otherwise {@code null}. + */ + public void onValidationStatus(@ValidationStatus int status, @Nullable Uri redirectUri) { + networkStatus(status, null == redirectUri ? "" : redirectUri.toString()); + } + /** @hide TODO delete once subclasses have moved to onValidationStatus */ + protected void networkStatus(int status, String redirectUrl) { + } + + + /** + * Called when the user asks to remember the choice to use this network even if unvalidated. + * The transport is responsible for remembering the choice, and the next time the user connects + * to the network, should explicitlySelected with {@code acceptUnvalidated} set to {@code true}. + * This method will only be called if {@link #explicitlySelected} was called with + * {@code acceptUnvalidated} set to {@code false}. + * @param accept whether the user wants to use the network even if unvalidated. + */ + public void onSaveAcceptUnvalidated(boolean accept) { + saveAcceptUnvalidated(accept); + } + /** @hide TODO delete once subclasses have moved to onSaveAcceptUnvalidated */ + protected void saveAcceptUnvalidated(boolean accept) { + } + + /** + * Called when ConnectivityService has successfully created this NetworkAgent's native network. + */ + public void onNetworkCreated() {} + + + /** + * Called when ConnectivityService has successfully destroy this NetworkAgent's native network. + */ + public void onNetworkDestroyed() {} + + /** + * Requests that the network hardware send the specified packet at the specified interval. + * + * @param slot the hardware slot on which to start the keepalive. + * @param interval the interval between packets, between 10 and 3600. Note that this API + * does not support sub-second precision and will round off the request. + * @param packet the packet to send. + */ + // seconds is from SocketKeepalive.MIN_INTERVAL_SEC to MAX_INTERVAL_SEC, but these should + // not be exposed as constants because they may change in the future (API guideline 4.8) + // and should have getters if exposed at all. Getters can't be used in the annotation, + // so the values unfortunately need to be copied. + public void onStartSocketKeepalive(int slot, @NonNull Duration interval, + @NonNull KeepalivePacketData packet) { + final long intervalSeconds = interval.getSeconds(); + if (intervalSeconds < SocketKeepalive.MIN_INTERVAL_SEC + || intervalSeconds > SocketKeepalive.MAX_INTERVAL_SEC) { + throw new IllegalArgumentException("Interval needs to be comprised between " + + SocketKeepalive.MIN_INTERVAL_SEC + " and " + SocketKeepalive.MAX_INTERVAL_SEC + + " but was " + intervalSeconds); + } + final Message msg = mHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, slot, + (int) intervalSeconds, packet); + startSocketKeepalive(msg); + msg.recycle(); + } + /** @hide TODO delete once subclasses have moved to onStartSocketKeepalive */ + protected void startSocketKeepalive(Message msg) { + onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_UNSUPPORTED); + } + + /** + * Requests that the network hardware stop a previously-started keepalive. + * + * @param slot the hardware slot on which to stop the keepalive. + */ + public void onStopSocketKeepalive(int slot) { + Message msg = mHandler.obtainMessage(CMD_STOP_SOCKET_KEEPALIVE, slot, 0, null); + stopSocketKeepalive(msg); + msg.recycle(); + } + /** @hide TODO delete once subclasses have moved to onStopSocketKeepalive */ + protected void stopSocketKeepalive(Message msg) { + onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_UNSUPPORTED); + } + + /** + * Must be called by the agent when a socket keepalive event occurs. + * + * @param slot the hardware slot on which the event occurred. + * @param event the event that occurred, as one of the SocketKeepalive.ERROR_* + * or SocketKeepalive.SUCCESS constants. + */ + public final void sendSocketKeepaliveEvent(int slot, + @SocketKeepalive.KeepaliveEvent int event) { + queueOrSendMessage(reg -> reg.sendSocketKeepaliveEvent(slot, event)); + } + /** @hide TODO delete once callers have moved to sendSocketKeepaliveEvent */ + public void onSocketKeepaliveEvent(int slot, int reason) { + sendSocketKeepaliveEvent(slot, reason); + } + + /** + * Called by ConnectivityService to add specific packet filter to network hardware to block + * replies (e.g., TCP ACKs) matching the sent keepalive packets. Implementations that support + * this feature must override this method. + * + * @param slot the hardware slot on which the keepalive should be sent. + * @param packet the packet that is being sent. + */ + public void onAddKeepalivePacketFilter(int slot, @NonNull KeepalivePacketData packet) { + Message msg = mHandler.obtainMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER, slot, 0, packet); + addKeepalivePacketFilter(msg); + msg.recycle(); + } + /** @hide TODO delete once subclasses have moved to onAddKeepalivePacketFilter */ + protected void addKeepalivePacketFilter(Message msg) { + } + + /** + * Called by ConnectivityService to remove a packet filter installed with + * {@link #addKeepalivePacketFilter(Message)}. Implementations that support this feature + * must override this method. + * + * @param slot the hardware slot on which the keepalive is being sent. + */ + public void onRemoveKeepalivePacketFilter(int slot) { + Message msg = mHandler.obtainMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, slot, 0, null); + removeKeepalivePacketFilter(msg); + msg.recycle(); + } + /** @hide TODO delete once subclasses have moved to onRemoveKeepalivePacketFilter */ + protected void removeKeepalivePacketFilter(Message msg) { + } + + /** + * Called by ConnectivityService to inform this network agent of signal strength thresholds + * that when crossed should trigger a system wakeup and a NetworkCapabilities update. + * + * When the system updates the list of thresholds that should wake up the CPU for a + * given agent it will call this method on the agent. The agent that implement this + * should implement it in hardware so as to ensure the CPU will be woken up on breach. + * Agents are expected to react to a breach by sending an updated NetworkCapabilities + * object with the appropriate signal strength to sendNetworkCapabilities. + * + * The specific units are bearer-dependent. See details on the units and requests in + * {@link NetworkCapabilities.Builder#setSignalStrength}. + * + * @param thresholds the array of thresholds that should trigger wakeups. + */ + public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) { + setSignalStrengthThresholds(thresholds); + } + /** @hide TODO delete once subclasses have moved to onSetSignalStrengthThresholds */ + protected void setSignalStrengthThresholds(int[] thresholds) { + } + + /** + * Called when the user asks to not stay connected to this network because it was found to not + * provide Internet access. Usually followed by call to {@code unwanted}. The transport is + * responsible for making sure the device does not automatically reconnect to the same network + * after the {@code unwanted} call. + */ + public void onAutomaticReconnectDisabled() { + preventAutomaticReconnect(); + } + /** @hide TODO delete once subclasses have moved to onAutomaticReconnectDisabled */ + protected void preventAutomaticReconnect() { + } + + /** + * Called when a qos callback is registered with a filter. + * @param qosCallbackId the id for the callback registered + * @param filter the filter being registered + */ + public void onQosCallbackRegistered(final int qosCallbackId, final @NonNull QosFilter filter) { + } + + /** + * Called when a qos callback is registered with a filter. + * <p/> + * Any QoS events that are sent with the same callback id after this method is called + * are a no-op. + * + * @param qosCallbackId the id for the callback being unregistered + */ + public void onQosCallbackUnregistered(final int qosCallbackId) { + } + + + /** + * Sends the attributes of Qos Session back to the Application + * + * @param qosCallbackId the callback id that the session belongs to + * @param sessionId the unique session id across all Qos Sessions + * @param attributes the attributes of the Qos Session + */ + public final void sendQosSessionAvailable(final int qosCallbackId, final int sessionId, + @NonNull final QosSessionAttributes attributes) { + Objects.requireNonNull(attributes, "The attributes must be non-null"); + if (attributes instanceof EpsBearerQosSessionAttributes) { + queueOrSendMessage(ra -> ra.sendEpsQosSessionAvailable(qosCallbackId, + new QosSession(sessionId, QosSession.TYPE_EPS_BEARER), + (EpsBearerQosSessionAttributes)attributes)); + } else if (attributes instanceof NrQosSessionAttributes) { + queueOrSendMessage(ra -> ra.sendNrQosSessionAvailable(qosCallbackId, + new QosSession(sessionId, QosSession.TYPE_NR_BEARER), + (NrQosSessionAttributes)attributes)); + } + } + + /** + * Sends event that the Qos Session was lost. + * + * @param qosCallbackId the callback id that the session belongs to + * @param sessionId the unique session id across all Qos Sessions + * @param qosSessionType the session type {@code QosSesson#QosSessionType} + */ + public final void sendQosSessionLost(final int qosCallbackId, + final int sessionId, final int qosSessionType) { + queueOrSendMessage(ra -> ra.sendQosSessionLost(qosCallbackId, + new QosSession(sessionId, qosSessionType))); + } + + /** + * Sends the exception type back to the application. + * + * The NetworkAgent should not send anymore messages with this id. + * + * @param qosCallbackId the callback id this exception belongs to + * @param exceptionType the type of exception + */ + public final void sendQosCallbackError(final int qosCallbackId, + @QosCallbackException.ExceptionType final int exceptionType) { + queueOrSendMessage(ra -> ra.sendQosCallbackError(qosCallbackId, exceptionType)); + } + + /** + * Set the linger duration for this network agent. + * @param duration the delay between the moment the network becomes unneeded and the + * moment the network is disconnected or moved into the background. + * Note that If this duration has greater than millisecond precision, then + * the internal implementation will drop any excess precision. + */ + public void setLingerDuration(@NonNull final Duration duration) { + Objects.requireNonNull(duration); + final long durationMs = duration.toMillis(); + if (durationMs < MIN_LINGER_TIMER_MS || durationMs > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Duration must be within [" + + MIN_LINGER_TIMER_MS + "," + Integer.MAX_VALUE + "]ms"); + } + queueOrSendMessage(ra -> ra.sendLingerDuration((int) durationMs)); + } + + /** @hide */ + protected void log(final String s) { + Log.d(LOG_TAG, "NetworkAgent: " + s); + } +} diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..ad8396b06ddfa467f3e8e078d31c587ac252cb94 --- /dev/null +++ b/framework/src/android/net/NetworkAgentConfig.java @@ -0,0 +1,505 @@ +/* + * 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 android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Allows a network transport to provide the system with policy and configuration information about + * a particular network when registering a {@link NetworkAgent}. This information cannot change once the agent is registered. + * + * @hide + */ +@SystemApi +public final class NetworkAgentConfig implements Parcelable { + + /** + * If the {@link Network} is a VPN, whether apps are allowed to bypass the + * VPN. This is set by a {@link VpnService} and used by + * {@link ConnectivityManager} when creating a VPN. + * + * @hide + */ + public boolean allowBypass; + + /** + * Set if the network was manually/explicitly connected to by the user either from settings + * or a 3rd party app. For example, turning on cell data is not explicit but tapping on a wifi + * ap in the wifi settings to trigger a connection is explicit. A 3rd party app asking to + * connect to a particular access point is also explicit, though this may change in the future + * as we want apps to use the multinetwork apis. + * TODO : this is a bad name, because it sounds like the user just tapped on the network. + * It's not necessarily the case ; auto-reconnection to WiFi has this true for example. + * @hide + */ + public boolean explicitlySelected; + + /** + * @return whether this network was explicitly selected by the user. + */ + public boolean isExplicitlySelected() { + return explicitlySelected; + } + + /** + * @return whether this VPN connection can be bypassed by the apps. + * + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public boolean isBypassableVpn() { + return allowBypass; + } + + /** + * Set if the user desires to use this network even if it is unvalidated. This field has meaning + * only if {@link explicitlySelected} is true. If it is, this field must also be set to the + * appropriate value based on previous user choice. + * + * TODO : rename this field to match its accessor + * @hide + */ + public boolean acceptUnvalidated; + + /** + * @return whether the system should accept this network even if it doesn't validate. + */ + public boolean isUnvalidatedConnectivityAcceptable() { + return acceptUnvalidated; + } + + /** + * Whether the user explicitly set that this network should be validated even if presence of + * only partial internet connectivity. + * + * TODO : rename this field to match its accessor + * @hide + */ + public boolean acceptPartialConnectivity; + + /** + * @return whether the system should validate this network even if it only offers partial + * Internet connectivity. + */ + public boolean isPartialConnectivityAcceptable() { + return acceptPartialConnectivity; + } + + /** + * Set to avoid surfacing the "Sign in to network" notification. + * if carrier receivers/apps are registered to handle the carrier-specific provisioning + * procedure, a carrier specific provisioning notification will be placed. + * only one notification should be displayed. This field is set based on + * which notification should be used for provisioning. + * + * @hide + */ + public boolean provisioningNotificationDisabled; + + /** + * + * @return whether the sign in to network notification is enabled by this configuration. + * @hide + */ + public boolean isProvisioningNotificationEnabled() { + return !provisioningNotificationDisabled; + } + + /** + * For mobile networks, this is the subscriber ID (such as IMSI). + * + * @hide + */ + public String subscriberId; + + /** + * @return the subscriber ID, or null if none. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + @Nullable + public String getSubscriberId() { + return subscriberId; + } + + /** + * Set to skip 464xlat. This means the device will treat the network as IPv6-only and + * will not attempt to detect a NAT64 via RFC 7050 DNS lookups. + * + * @hide + */ + public boolean skip464xlat; + + /** + * @return whether NAT64 prefix detection is enabled. + * @hide + */ + public boolean isNat64DetectionEnabled() { + return !skip464xlat; + } + + /** + * The legacy type of this network agent, or TYPE_NONE if unset. + * @hide + */ + public int legacyType = ConnectivityManager.TYPE_NONE; + + /** + * @return the legacy type + */ + @ConnectivityManager.LegacyNetworkType + public int getLegacyType() { + return legacyType; + } + + /** + * The legacy Sub type of this network agent, or TYPE_NONE if unset. + * @hide + */ + public int legacySubType = ConnectivityManager.TYPE_NONE; + + /** + * Set to true if the PRIVATE_DNS_BROKEN notification has shown for this network. + * Reset this bit when private DNS mode is changed from strict mode to opportunistic/off mode. + * + * This is not parceled, because it would not make sense. + * + * @hide + */ + public transient boolean hasShownBroken; + + /** + * The name of the legacy network type. It's a free-form string used in logging. + * @hide + */ + @NonNull + public String legacyTypeName = ""; + + /** + * @return the name of the legacy network type. It's a free-form string used in logging. + */ + @NonNull + public String getLegacyTypeName() { + return legacyTypeName; + } + + /** + * The name of the legacy Sub network type. It's a free-form string. + * @hide + */ + @NonNull + public String legacySubTypeName = ""; + + /** + * The legacy extra info of the agent. The extra info should only be : + * <ul> + * <li>For cellular agents, the APN name.</li> + * <li>For ethernet agents, the interface name.</li> + * </ul> + * @hide + */ + @NonNull + private String mLegacyExtraInfo = ""; + + /** + * The legacy extra info of the agent. + * @hide + */ + @NonNull + public String getLegacyExtraInfo() { + return mLegacyExtraInfo; + } + + /** @hide */ + public NetworkAgentConfig() { + } + + /** @hide */ + public NetworkAgentConfig(@Nullable NetworkAgentConfig nac) { + if (nac != null) { + allowBypass = nac.allowBypass; + explicitlySelected = nac.explicitlySelected; + acceptUnvalidated = nac.acceptUnvalidated; + acceptPartialConnectivity = nac.acceptPartialConnectivity; + subscriberId = nac.subscriberId; + provisioningNotificationDisabled = nac.provisioningNotificationDisabled; + skip464xlat = nac.skip464xlat; + legacyType = nac.legacyType; + legacyTypeName = nac.legacyTypeName; + legacySubType = nac.legacySubType; + legacySubTypeName = nac.legacySubTypeName; + mLegacyExtraInfo = nac.mLegacyExtraInfo; + } + } + + /** + * Builder class to facilitate constructing {@link NetworkAgentConfig} objects. + */ + public static final class Builder { + private final NetworkAgentConfig mConfig = new NetworkAgentConfig(); + + /** + * Sets whether the network was explicitly selected by the user. + * + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setExplicitlySelected(final boolean explicitlySelected) { + mConfig.explicitlySelected = explicitlySelected; + return this; + } + + /** + * Sets whether the system should validate this network even if it is found not to offer + * Internet connectivity. + * + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setUnvalidatedConnectivityAcceptable( + final boolean unvalidatedConnectivityAcceptable) { + mConfig.acceptUnvalidated = unvalidatedConnectivityAcceptable; + return this; + } + + /** + * Sets whether the system should validate this network even if it is found to only offer + * partial Internet connectivity. + * + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setPartialConnectivityAcceptable( + final boolean partialConnectivityAcceptable) { + mConfig.acceptPartialConnectivity = partialConnectivityAcceptable; + return this; + } + + /** + * Sets the subscriber ID for this network. + * + * @return this builder, to facilitate chaining. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + public Builder setSubscriberId(@Nullable String subscriberId) { + mConfig.subscriberId = subscriberId; + return this; + } + + /** + * Enables or disables active detection of NAT64 (e.g., via RFC 7050 DNS lookups). Used to + * save power and reduce idle traffic on networks that are known to be IPv6-only without a + * NAT64. By default, NAT64 detection is enabled. + * + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setNat64DetectionEnabled(boolean enabled) { + mConfig.skip464xlat = !enabled; + return this; + } + + /** + * Enables or disables the "Sign in to network" notification. Used if the network transport + * will perform its own carrier-specific provisioning procedure. By default, the + * notification is enabled. + * + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setProvisioningNotificationEnabled(boolean enabled) { + mConfig.provisioningNotificationDisabled = !enabled; + return this; + } + + /** + * Sets the legacy type for this network. + * + * @param legacyType the type + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setLegacyType(int legacyType) { + mConfig.legacyType = legacyType; + return this; + } + + /** + * Sets the legacy sub-type for this network. + * + * @param legacySubType the type + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setLegacySubType(final int legacySubType) { + mConfig.legacySubType = legacySubType; + return this; + } + + /** + * Sets the name of the legacy type of the agent. It's a free-form string used in logging. + * @param legacyTypeName the name + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setLegacyTypeName(@NonNull String legacyTypeName) { + mConfig.legacyTypeName = legacyTypeName; + return this; + } + + /** + * Sets the name of the legacy Sub-type of the agent. It's a free-form string. + * @param legacySubTypeName the name + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setLegacySubTypeName(@NonNull String legacySubTypeName) { + mConfig.legacySubTypeName = legacySubTypeName; + return this; + } + + /** + * Sets the legacy extra info of the agent. + * @param legacyExtraInfo the legacy extra info. + * @return this builder, to facilitate chaining. + */ + @NonNull + public Builder setLegacyExtraInfo(@NonNull String legacyExtraInfo) { + mConfig.mLegacyExtraInfo = legacyExtraInfo; + return this; + } + + /** + * Sets whether the apps can bypass the VPN connection. + * + * @return this builder, to facilitate chaining. + * @hide + */ + @NonNull + @SystemApi(client = MODULE_LIBRARIES) + public Builder setBypassableVpn(boolean allowBypass) { + mConfig.allowBypass = allowBypass; + return this; + } + + /** + * Returns the constructed {@link NetworkAgentConfig} object. + */ + @NonNull + public NetworkAgentConfig build() { + return mConfig; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final NetworkAgentConfig that = (NetworkAgentConfig) o; + return allowBypass == that.allowBypass + && explicitlySelected == that.explicitlySelected + && acceptUnvalidated == that.acceptUnvalidated + && acceptPartialConnectivity == that.acceptPartialConnectivity + && provisioningNotificationDisabled == that.provisioningNotificationDisabled + && skip464xlat == that.skip464xlat + && legacyType == that.legacyType + && Objects.equals(subscriberId, that.subscriberId) + && Objects.equals(legacyTypeName, that.legacyTypeName) + && Objects.equals(mLegacyExtraInfo, that.mLegacyExtraInfo); + } + + @Override + public int hashCode() { + return Objects.hash(allowBypass, explicitlySelected, acceptUnvalidated, + acceptPartialConnectivity, provisioningNotificationDisabled, subscriberId, + skip464xlat, legacyType, legacyTypeName, mLegacyExtraInfo); + } + + @Override + public String toString() { + return "NetworkAgentConfig {" + + " allowBypass = " + allowBypass + + ", explicitlySelected = " + explicitlySelected + + ", acceptUnvalidated = " + acceptUnvalidated + + ", acceptPartialConnectivity = " + acceptPartialConnectivity + + ", provisioningNotificationDisabled = " + provisioningNotificationDisabled + + ", subscriberId = '" + subscriberId + '\'' + + ", skip464xlat = " + skip464xlat + + ", legacyType = " + legacyType + + ", hasShownBroken = " + hasShownBroken + + ", legacyTypeName = '" + legacyTypeName + '\'' + + ", legacyExtraInfo = '" + mLegacyExtraInfo + '\'' + + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeInt(allowBypass ? 1 : 0); + out.writeInt(explicitlySelected ? 1 : 0); + out.writeInt(acceptUnvalidated ? 1 : 0); + out.writeInt(acceptPartialConnectivity ? 1 : 0); + out.writeString(subscriberId); + out.writeInt(provisioningNotificationDisabled ? 1 : 0); + out.writeInt(skip464xlat ? 1 : 0); + out.writeInt(legacyType); + out.writeString(legacyTypeName); + out.writeInt(legacySubType); + out.writeString(legacySubTypeName); + out.writeString(mLegacyExtraInfo); + } + + public static final @NonNull Creator<NetworkAgentConfig> CREATOR = + new Creator<NetworkAgentConfig>() { + @Override + public NetworkAgentConfig createFromParcel(Parcel in) { + NetworkAgentConfig networkAgentConfig = new NetworkAgentConfig(); + networkAgentConfig.allowBypass = in.readInt() != 0; + networkAgentConfig.explicitlySelected = in.readInt() != 0; + networkAgentConfig.acceptUnvalidated = in.readInt() != 0; + networkAgentConfig.acceptPartialConnectivity = in.readInt() != 0; + networkAgentConfig.subscriberId = in.readString(); + networkAgentConfig.provisioningNotificationDisabled = in.readInt() != 0; + networkAgentConfig.skip464xlat = in.readInt() != 0; + networkAgentConfig.legacyType = in.readInt(); + networkAgentConfig.legacyTypeName = in.readString(); + networkAgentConfig.legacySubType = in.readInt(); + networkAgentConfig.legacySubTypeName = in.readString(); + networkAgentConfig.mLegacyExtraInfo = in.readString(); + return networkAgentConfig; + } + + @Override + public NetworkAgentConfig[] newArray(int size) { + return new NetworkAgentConfig[size]; + } + }; +} diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..49329524342082656126e51fc5d3b40a670055e3 --- /dev/null +++ b/framework/src/android/net/NetworkCapabilities.java @@ -0,0 +1,2779 @@ +/* + * 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 android.net; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; + +import android.annotation.IntDef; +import android.annotation.LongDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.wifi.WifiNetworkSuggestion; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Range; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.NetworkCapabilitiesUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; + +/** + * Representation of the capabilities of an active network. Instances are + * typically obtained through + * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)} + * or {@link ConnectivityManager#getNetworkCapabilities(Network)}. + * <p> + * This replaces the old {@link ConnectivityManager#TYPE_MOBILE} method of + * network selection. Rather than indicate a need for Wi-Fi because an + * application needs high bandwidth and risk obsolescence when a new, fast + * network appears (like LTE), the application should specify it needs high + * bandwidth. Similarly if an application needs an unmetered network for a bulk + * transfer it can specify that rather than assuming all cellular based + * connections are metered and all Wi-Fi based connections are not. + */ +public final class NetworkCapabilities implements Parcelable { + private static final String TAG = "NetworkCapabilities"; + + /** + * Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific + * app permissions. + **/ + /** + * Don't redact any fields since the receiving app holds all the necessary permissions. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final long REDACT_NONE = 0; + + /** + * Redact any fields that need {@link android.Manifest.permission#ACCESS_FINE_LOCATION} + * permission since the receiving app does not hold this permission or the location toggle + * is off. + * + * @see android.Manifest.permission#ACCESS_FINE_LOCATION + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final long REDACT_FOR_ACCESS_FINE_LOCATION = 1 << 0; + + /** + * Redact any fields that need {@link android.Manifest.permission#LOCAL_MAC_ADDRESS} + * permission since the receiving app does not hold this permission. + * + * @see android.Manifest.permission#LOCAL_MAC_ADDRESS + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final long REDACT_FOR_LOCAL_MAC_ADDRESS = 1 << 1; + + /** + * + * Redact any fields that need {@link android.Manifest.permission#NETWORK_SETTINGS} + * permission since the receiving app does not hold this permission. + * + * @see android.Manifest.permission#NETWORK_SETTINGS + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final long REDACT_FOR_NETWORK_SETTINGS = 1 << 2; + + /** + * Redact all fields in this object that require any relevant permission. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final long REDACT_ALL = -1L; + + /** @hide */ + @LongDef(flag = true, prefix = { "REDACT_" }, value = { + REDACT_NONE, + REDACT_FOR_ACCESS_FINE_LOCATION, + REDACT_FOR_LOCAL_MAC_ADDRESS, + REDACT_FOR_NETWORK_SETTINGS, + REDACT_ALL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RedactionType {} + + // Set to true when private DNS is broken. + private boolean mPrivateDnsBroken; + + /** + * Uid of the app making the request. + */ + private int mRequestorUid; + + /** + * Package name of the app making the request. + */ + private String mRequestorPackageName; + + public NetworkCapabilities() { + clearAll(); + mNetworkCapabilities = DEFAULT_CAPABILITIES; + } + + public NetworkCapabilities(NetworkCapabilities nc) { + this(nc, REDACT_NONE); + } + + /** + * Make a copy of NetworkCapabilities. + * + * @param nc Original NetworkCapabilities + * @param redactions bitmask of redactions that needs to be performed on this new instance of + * {@link NetworkCapabilities}. + * @hide + */ + public NetworkCapabilities(@Nullable NetworkCapabilities nc, @RedactionType long redactions) { + if (nc != null) { + set(nc); + } + if (mTransportInfo != null) { + mTransportInfo = nc.mTransportInfo.makeCopy(redactions); + } + } + + /** + * Completely clears the contents of this object, removing even the capabilities that are set + * by default when the object is constructed. + * @hide + */ + public void clearAll() { + mNetworkCapabilities = mTransportTypes = mForbiddenNetworkCapabilities = 0; + mLinkUpBandwidthKbps = mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED; + mNetworkSpecifier = null; + mTransportInfo = null; + mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED; + mUids = null; + mAdministratorUids = new int[0]; + mOwnerUid = Process.INVALID_UID; + mSSID = null; + mPrivateDnsBroken = false; + mRequestorUid = Process.INVALID_UID; + mRequestorPackageName = null; + mSubIds = new ArraySet<>(); + } + + /** + * Set all contents of this object to the contents of a NetworkCapabilities. + * + * @param nc Original NetworkCapabilities + * @hide + */ + public void set(@NonNull NetworkCapabilities nc) { + mNetworkCapabilities = nc.mNetworkCapabilities; + mTransportTypes = nc.mTransportTypes; + mLinkUpBandwidthKbps = nc.mLinkUpBandwidthKbps; + mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps; + mNetworkSpecifier = nc.mNetworkSpecifier; + if (nc.getTransportInfo() != null) { + setTransportInfo(nc.getTransportInfo()); + } else { + setTransportInfo(null); + } + mSignalStrength = nc.mSignalStrength; + mUids = (nc.mUids == null) ? null : new ArraySet<>(nc.mUids); + setAdministratorUids(nc.getAdministratorUids()); + mOwnerUid = nc.mOwnerUid; + mForbiddenNetworkCapabilities = nc.mForbiddenNetworkCapabilities; + mSSID = nc.mSSID; + mPrivateDnsBroken = nc.mPrivateDnsBroken; + mRequestorUid = nc.mRequestorUid; + mRequestorPackageName = nc.mRequestorPackageName; + mSubIds = new ArraySet<>(nc.mSubIds); + } + + /** + * Represents the network's capabilities. If any are specified they will be satisfied + * by any Network that matches all of them. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private long mNetworkCapabilities; + + /** + * If any capabilities specified here they must not exist in the matching Network. + */ + private long mForbiddenNetworkCapabilities; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "NET_CAPABILITY_" }, value = { + NET_CAPABILITY_MMS, + NET_CAPABILITY_SUPL, + NET_CAPABILITY_DUN, + NET_CAPABILITY_FOTA, + NET_CAPABILITY_IMS, + NET_CAPABILITY_CBS, + NET_CAPABILITY_WIFI_P2P, + NET_CAPABILITY_IA, + NET_CAPABILITY_RCS, + NET_CAPABILITY_XCAP, + NET_CAPABILITY_EIMS, + NET_CAPABILITY_NOT_METERED, + NET_CAPABILITY_INTERNET, + NET_CAPABILITY_NOT_RESTRICTED, + NET_CAPABILITY_TRUSTED, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_VALIDATED, + NET_CAPABILITY_CAPTIVE_PORTAL, + NET_CAPABILITY_NOT_ROAMING, + NET_CAPABILITY_FOREGROUND, + NET_CAPABILITY_NOT_CONGESTED, + NET_CAPABILITY_NOT_SUSPENDED, + NET_CAPABILITY_OEM_PAID, + NET_CAPABILITY_MCX, + NET_CAPABILITY_PARTIAL_CONNECTIVITY, + NET_CAPABILITY_TEMPORARILY_NOT_METERED, + NET_CAPABILITY_OEM_PRIVATE, + NET_CAPABILITY_VEHICLE_INTERNAL, + NET_CAPABILITY_NOT_VCN_MANAGED, + NET_CAPABILITY_ENTERPRISE, + NET_CAPABILITY_VSIM, + NET_CAPABILITY_BIP, + NET_CAPABILITY_HEAD_UNIT, + }) + public @interface NetCapability { } + + /** + * Indicates this is a network that has the ability to reach the + * carrier's MMSC for sending and receiving MMS messages. + */ + public static final int NET_CAPABILITY_MMS = 0; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * SUPL server, used to retrieve GPS information. + */ + public static final int NET_CAPABILITY_SUPL = 1; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * DUN or tethering gateway. + */ + public static final int NET_CAPABILITY_DUN = 2; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * FOTA portal, used for over the air updates. + */ + public static final int NET_CAPABILITY_FOTA = 3; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * IMS servers, used for network registration and signaling. + */ + public static final int NET_CAPABILITY_IMS = 4; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * CBS servers, used for carrier specific services. + */ + public static final int NET_CAPABILITY_CBS = 5; + + /** + * Indicates this is a network that has the ability to reach a Wi-Fi direct + * peer. + */ + public static final int NET_CAPABILITY_WIFI_P2P = 6; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * Initial Attach servers. + */ + public static final int NET_CAPABILITY_IA = 7; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * RCS servers, used for Rich Communication Services. + */ + public static final int NET_CAPABILITY_RCS = 8; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * XCAP servers, used for configuration and control. + */ + public static final int NET_CAPABILITY_XCAP = 9; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * Emergency IMS servers or other services, used for network signaling + * during emergency calls. + */ + public static final int NET_CAPABILITY_EIMS = 10; + + /** + * Indicates that this network is unmetered. + */ + public static final int NET_CAPABILITY_NOT_METERED = 11; + + /** + * Indicates that this network should be able to reach the internet. + */ + public static final int NET_CAPABILITY_INTERNET = 12; + + /** + * Indicates that this network is available for general use. If this is not set + * applications should not attempt to communicate on this network. Note that this + * is simply informative and not enforcement - enforcement is handled via other means. + * Set by default. + */ + public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; + + /** + * Indicates that the user has indicated implicit trust of this network. This + * generally means it's a sim-selected carrier, a plugged in ethernet, a paired + * BT device or a wifi the user asked to connect to. Untrusted networks + * are probably limited to unknown wifi AP. Set by default. + */ + public static final int NET_CAPABILITY_TRUSTED = 14; + + /** + * Indicates that this network is not a VPN. This capability is set by default and should be + * explicitly cleared for VPN networks. + */ + public static final int NET_CAPABILITY_NOT_VPN = 15; + + /** + * Indicates that connectivity on this network was successfully validated. For example, for a + * network with NET_CAPABILITY_INTERNET, it means that Internet connectivity was successfully + * detected. + */ + public static final int NET_CAPABILITY_VALIDATED = 16; + + /** + * Indicates that this network was found to have a captive portal in place last time it was + * probed. + */ + public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17; + + /** + * Indicates that this network is not roaming. + */ + public static final int NET_CAPABILITY_NOT_ROAMING = 18; + + /** + * Indicates that this network is available for use by apps, and not a network that is being + * kept up in the background to facilitate fast network switching. + */ + public static final int NET_CAPABILITY_FOREGROUND = 19; + + /** + * Indicates that this network is not congested. + * <p> + * When a network is congested, applications should defer network traffic + * that can be done at a later time, such as uploading analytics. + */ + public static final int NET_CAPABILITY_NOT_CONGESTED = 20; + + /** + * Indicates that this network is not currently suspended. + * <p> + * When a network is suspended, the network's IP addresses and any connections + * established on the network remain valid, but the network is temporarily unable + * to transfer data. This can happen, for example, if a cellular network experiences + * a temporary loss of signal, such as when driving through a tunnel, etc. + * A network with this capability is not suspended, so is expected to be able to + * transfer data. + */ + public static final int NET_CAPABILITY_NOT_SUSPENDED = 21; + + /** + * Indicates that traffic that goes through this network is paid by oem. For example, + * this network can be used by system apps to upload telemetry data. + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_OEM_PAID = 22; + + /** + * Indicates this is a network that has the ability to reach a carrier's Mission Critical + * servers. + */ + public static final int NET_CAPABILITY_MCX = 23; + + /** + * Indicates that this network was tested to only provide partial connectivity. + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24; + + /** + * Indicates that this network is temporarily unmetered. + * <p> + * This capability will be set for networks that are generally metered, but are currently + * unmetered, e.g., because the user is in a particular area. This capability can be changed at + * any time. When it is removed, applications are responsible for stopping any data transfer + * that should not occur on a metered network. + * Note that most apps should use {@link #NET_CAPABILITY_NOT_METERED} instead. For more + * information, see https://developer.android.com/about/versions/11/features/5g#meteredness. + */ + public static final int NET_CAPABILITY_TEMPORARILY_NOT_METERED = 25; + + /** + * Indicates that this network is private to the OEM and meant only for OEM use. + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_OEM_PRIVATE = 26; + + /** + * Indicates this is an internal vehicle network, meant to communicate with other + * automotive systems. + * + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_VEHICLE_INTERNAL = 27; + + /** + * Indicates that this network is not subsumed by a Virtual Carrier Network (VCN). + * <p> + * To provide an experience on a VCN similar to a single traditional carrier network, in + * some cases the system sets this bit is set by default in application's network requests, + * and may choose to remove it at its own discretion when matching the request to a network. + * <p> + * Applications that want to know about a Virtual Carrier Network's underlying networks, + * for example to use them for multipath purposes, should remove this bit from their network + * requests ; the system will not add it back once removed. + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_NOT_VCN_MANAGED = 28; + + /** + * Indicates that this network is intended for enterprise use. + * <p> + * 5G URSP rules may indicate that all data should use a connection dedicated for enterprise + * use. If the enterprise capability is requested, all enterprise traffic will be routed over + * the connection with this capability. + */ + public static final int NET_CAPABILITY_ENTERPRISE = 29; + + /** + * Indicates that this network has ability to access the carrier's Virtual Sim service. + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_VSIM = 30; + + /** + * Indicates that this network has ability to support Bearer Independent Protol. + * @hide + */ + @SystemApi + public static final int NET_CAPABILITY_BIP = 31; + + /** + * Indicates that this network is connected to an automotive head unit. + */ + public static final int NET_CAPABILITY_HEAD_UNIT = 32; + + private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS; + private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_HEAD_UNIT; + + /** + * Network capabilities that are expected to be mutable, i.e., can change while a particular + * network is connected. + */ + private static final long MUTABLE_CAPABILITIES = + // TRUSTED can change when user explicitly connects to an untrusted network in Settings. + // http://b/18206275 + (1 << NET_CAPABILITY_TRUSTED) + | (1 << NET_CAPABILITY_VALIDATED) + | (1 << NET_CAPABILITY_CAPTIVE_PORTAL) + | (1 << NET_CAPABILITY_NOT_ROAMING) + | (1 << NET_CAPABILITY_FOREGROUND) + | (1 << NET_CAPABILITY_NOT_CONGESTED) + | (1 << NET_CAPABILITY_NOT_SUSPENDED) + | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY) + | (1 << NET_CAPABILITY_TEMPORARILY_NOT_METERED) + | (1 << NET_CAPABILITY_NOT_VCN_MANAGED) + // The value of NET_CAPABILITY_HEAD_UNIT is 32, which cannot use int to do bit shift, + // otherwise there will be an overflow. Use long to do bit shift instead. + | (1L << NET_CAPABILITY_HEAD_UNIT); + + /** + * Network capabilities that are not allowed in NetworkRequests. This exists because the + * NetworkFactory / NetworkAgent model does not deal well with the situation where a + * capability's presence cannot be known in advance. If such a capability is requested, then we + * can get into a cycle where the NetworkFactory endlessly churns out NetworkAgents that then + * get immediately torn down because they do not have the requested capability. + */ + // Note that as a historical exception, the TRUSTED and NOT_VCN_MANAGED capabilities + // are mutable but requestable. Factories are responsible for not getting + // in an infinite loop about these. + private static final long NON_REQUESTABLE_CAPABILITIES = + MUTABLE_CAPABILITIES + & ~(1 << NET_CAPABILITY_TRUSTED) + & ~(1 << NET_CAPABILITY_NOT_VCN_MANAGED); + + /** + * Capabilities that are set by default when the object is constructed. + */ + private static final long DEFAULT_CAPABILITIES = + (1 << NET_CAPABILITY_NOT_RESTRICTED) + | (1 << NET_CAPABILITY_TRUSTED) + | (1 << NET_CAPABILITY_NOT_VPN); + + /** + * Capabilities that are managed by ConnectivityService. + */ + private static final long CONNECTIVITY_MANAGED_CAPABILITIES = + (1 << NET_CAPABILITY_VALIDATED) + | (1 << NET_CAPABILITY_CAPTIVE_PORTAL) + | (1 << NET_CAPABILITY_FOREGROUND) + | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY); + + /** + * Capabilities that are allowed for test networks. This list must be set so that it is safe + * for an unprivileged user to create a network with these capabilities via shell. As such, + * it must never contain capabilities that are generally useful to the system, such as + * INTERNET, IMS, SUPL, etc. + */ + private static final long TEST_NETWORKS_ALLOWED_CAPABILITIES = + (1 << NET_CAPABILITY_NOT_METERED) + | (1 << NET_CAPABILITY_TEMPORARILY_NOT_METERED) + | (1 << NET_CAPABILITY_NOT_RESTRICTED) + | (1 << NET_CAPABILITY_NOT_VPN) + | (1 << NET_CAPABILITY_NOT_ROAMING) + | (1 << NET_CAPABILITY_NOT_CONGESTED) + | (1 << NET_CAPABILITY_NOT_SUSPENDED) + | (1 << NET_CAPABILITY_NOT_VCN_MANAGED); + + /** + * Adds the given capability to this {@code NetworkCapability} instance. + * Note that when searching for a network to satisfy a request, all capabilities + * requested must be satisfied. + * + * @param capability the capability to be added. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) { + // If the given capability was previously added to the list of forbidden capabilities + // then the capability will also be removed from the list of forbidden capabilities. + // TODO: Consider adding forbidden capabilities to the public API and mention this + // in the documentation. + checkValidCapability(capability); + mNetworkCapabilities |= 1L << capability; + // remove from forbidden capability list + mForbiddenNetworkCapabilities &= ~(1L << capability); + return this; + } + + /** + * Adds the given capability to the list of forbidden capabilities of this + * {@code NetworkCapability} instance. Note that when searching for a network to + * satisfy a request, the network must not contain any capability from forbidden capability + * list. + * <p> + * If the capability was previously added to the list of required capabilities (for + * example, it was there by default or added using {@link #addCapability(int)} method), then + * it will be removed from the list of required capabilities as well. + * + * @see #addCapability(int) + * @hide + */ + public void addForbiddenCapability(@NetCapability int capability) { + checkValidCapability(capability); + mForbiddenNetworkCapabilities |= 1L << capability; + mNetworkCapabilities &= ~(1L << capability); // remove from requested capabilities + } + + /** + * Removes (if found) the given capability from this {@code NetworkCapability} + * instance that were added via addCapability(int) or setCapabilities(int[], int[]). + * + * @param capability the capability to be removed. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities removeCapability(@NetCapability int capability) { + checkValidCapability(capability); + final long mask = ~(1L << capability); + mNetworkCapabilities &= mask; + return this; + } + + /** + * Removes (if found) the given forbidden capability from this {@code NetworkCapability} + * instance that were added via addForbiddenCapability(int) or setCapabilities(int[], int[]). + * + * @param capability the capability to be removed. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities removeForbiddenCapability(@NetCapability int capability) { + checkValidCapability(capability); + mForbiddenNetworkCapabilities &= ~(1L << capability); + return this; + } + + /** + * Sets (or clears) the given capability on this {@link NetworkCapabilities} + * instance. + * @hide + */ + public @NonNull NetworkCapabilities setCapability(@NetCapability int capability, + boolean value) { + if (value) { + addCapability(capability); + } else { + removeCapability(capability); + } + return this; + } + + /** + * Gets all the capabilities set on this {@code NetworkCapability} instance. + * + * @return an array of capability values for this instance. + */ + public @NonNull @NetCapability int[] getCapabilities() { + return NetworkCapabilitiesUtils.unpackBits(mNetworkCapabilities); + } + + /** + * Gets all the forbidden capabilities set on this {@code NetworkCapability} instance. + * + * @return an array of forbidden capability values for this instance. + * @hide + */ + public @NetCapability int[] getForbiddenCapabilities() { + return NetworkCapabilitiesUtils.unpackBits(mForbiddenNetworkCapabilities); + } + + + /** + * Sets all the capabilities set on this {@code NetworkCapability} instance. + * This overwrites any existing capabilities. + * + * @hide + */ + public void setCapabilities(@NetCapability int[] capabilities, + @NetCapability int[] forbiddenCapabilities) { + mNetworkCapabilities = NetworkCapabilitiesUtils.packBits(capabilities); + mForbiddenNetworkCapabilities = NetworkCapabilitiesUtils.packBits(forbiddenCapabilities); + } + + /** + * @deprecated use {@link #setCapabilities(int[], int[])} + * @hide + */ + @Deprecated + public void setCapabilities(@NetCapability int[] capabilities) { + setCapabilities(capabilities, new int[] {}); + } + + /** + * Tests for the presence of a capability on this instance. + * + * @param capability the capabilities to be tested for. + * @return {@code true} if set on this instance. + */ + public boolean hasCapability(@NetCapability int capability) { + return isValidCapability(capability) + && ((mNetworkCapabilities & (1L << capability)) != 0); + } + + /** @hide */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public boolean hasForbiddenCapability(@NetCapability int capability) { + return isValidCapability(capability) + && ((mForbiddenNetworkCapabilities & (1L << capability)) != 0); + } + + /** + * Check if this NetworkCapabilities has system managed capabilities or not. + * @hide + */ + public boolean hasConnectivityManagedCapability() { + return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0); + } + + /** + * Get the name of the given capability that carriers use. + * If the capability does not have a carrier-name, returns null. + * + * @param capability The capability to get the carrier-name of. + * @return The carrier-name of the capability, or null if it doesn't exist. + * @hide + */ + @SystemApi + public static @Nullable String getCapabilityCarrierName(@NetCapability int capability) { + if (capability == NET_CAPABILITY_ENTERPRISE) { + return capabilityNameOf(capability); + } else { + return null; + } + } + + private void combineNetCapabilities(@NonNull NetworkCapabilities nc) { + final long wantedCaps = this.mNetworkCapabilities | nc.mNetworkCapabilities; + final long forbiddenCaps = + this.mForbiddenNetworkCapabilities | nc.mForbiddenNetworkCapabilities; + if ((wantedCaps & forbiddenCaps) != 0) { + throw new IllegalArgumentException( + "Cannot have the same capability in wanted and forbidden lists."); + } + this.mNetworkCapabilities = wantedCaps; + this.mForbiddenNetworkCapabilities = forbiddenCaps; + } + + /** + * Convenience function that returns a human-readable description of the first mutable + * capability we find. Used to present an error message to apps that request mutable + * capabilities. + * + * @hide + */ + public @Nullable String describeFirstNonRequestableCapability() { + final long nonRequestable = (mNetworkCapabilities | mForbiddenNetworkCapabilities) + & NON_REQUESTABLE_CAPABILITIES; + + if (nonRequestable != 0) { + return capabilityNameOf(NetworkCapabilitiesUtils.unpackBits(nonRequestable)[0]); + } + if (mLinkUpBandwidthKbps != 0 || mLinkDownBandwidthKbps != 0) return "link bandwidth"; + if (hasSignalStrength()) return "signalStrength"; + if (isPrivateDnsBroken()) { + return "privateDnsBroken"; + } + return null; + } + + private boolean satisfiedByNetCapabilities(@NonNull NetworkCapabilities nc, + boolean onlyImmutable) { + long requestedCapabilities = mNetworkCapabilities; + long requestedForbiddenCapabilities = mForbiddenNetworkCapabilities; + long providedCapabilities = nc.mNetworkCapabilities; + + if (onlyImmutable) { + requestedCapabilities &= ~MUTABLE_CAPABILITIES; + requestedForbiddenCapabilities &= ~MUTABLE_CAPABILITIES; + } + return ((providedCapabilities & requestedCapabilities) == requestedCapabilities) + && ((requestedForbiddenCapabilities & providedCapabilities) == 0); + } + + /** @hide */ + public boolean equalsNetCapabilities(@NonNull NetworkCapabilities nc) { + return (nc.mNetworkCapabilities == this.mNetworkCapabilities) + && (nc.mForbiddenNetworkCapabilities == this.mForbiddenNetworkCapabilities); + } + + private boolean equalsNetCapabilitiesRequestable(@NonNull NetworkCapabilities that) { + return ((this.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES) + == (that.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES)) + && ((this.mForbiddenNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES) + == (that.mForbiddenNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES)); + } + + /** + * Removes the NET_CAPABILITY_NOT_RESTRICTED capability if inferring the network is restricted. + * + * @hide + */ + public void maybeMarkCapabilitiesRestricted() { + if (NetworkCapabilitiesUtils.inferRestrictedCapability(this)) { + removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + } + } + + /** + * Test networks have strong restrictions on what capabilities they can have. Enforce these + * restrictions. + * @hide + */ + public void restrictCapabilitesForTestNetwork(int creatorUid) { + final long originalCapabilities = mNetworkCapabilities; + final long originalTransportTypes = mTransportTypes; + final NetworkSpecifier originalSpecifier = mNetworkSpecifier; + final int originalSignalStrength = mSignalStrength; + final int originalOwnerUid = getOwnerUid(); + final int[] originalAdministratorUids = getAdministratorUids(); + final TransportInfo originalTransportInfo = getTransportInfo(); + clearAll(); + if (0 != (originalCapabilities & NET_CAPABILITY_NOT_RESTRICTED)) { + // If the test network is not restricted, then it is only allowed to declare some + // specific transports. This is to minimize impact on running apps in case an app + // run from the shell creates a test a network. + mTransportTypes = + (originalTransportTypes & UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS) + | (1 << TRANSPORT_TEST); + } else { + // If the test transport is restricted, then it may declare any transport. + mTransportTypes = (originalTransportTypes | (1 << TRANSPORT_TEST)); + } + mNetworkCapabilities = originalCapabilities & TEST_NETWORKS_ALLOWED_CAPABILITIES; + mNetworkSpecifier = originalSpecifier; + mSignalStrength = originalSignalStrength; + mTransportInfo = originalTransportInfo; + + // Only retain the owner and administrator UIDs if they match the app registering the remote + // caller that registered the network. + if (originalOwnerUid == creatorUid) { + setOwnerUid(creatorUid); + } + if (CollectionUtils.contains(originalAdministratorUids, creatorUid)) { + setAdministratorUids(new int[] {creatorUid}); + } + // There is no need to clear the UIDs, they have already been cleared by clearAll() above. + } + + /** + * Representing the transport type. Apps should generally not care about transport. A + * request for a fast internet connection could be satisfied by a number of different + * transports. If any are specified here it will be satisfied a Network that matches + * any of them. If a caller doesn't care about the transport it should not specify any. + */ + private long mTransportTypes; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "TRANSPORT_" }, value = { + TRANSPORT_CELLULAR, + TRANSPORT_WIFI, + TRANSPORT_BLUETOOTH, + TRANSPORT_ETHERNET, + TRANSPORT_VPN, + TRANSPORT_WIFI_AWARE, + TRANSPORT_LOWPAN, + TRANSPORT_TEST, + TRANSPORT_USB, + }) + public @interface Transport { } + + /** + * Indicates this network uses a Cellular transport. + */ + public static final int TRANSPORT_CELLULAR = 0; + + /** + * Indicates this network uses a Wi-Fi transport. + */ + public static final int TRANSPORT_WIFI = 1; + + /** + * Indicates this network uses a Bluetooth transport. + */ + public static final int TRANSPORT_BLUETOOTH = 2; + + /** + * Indicates this network uses an Ethernet transport. + */ + public static final int TRANSPORT_ETHERNET = 3; + + /** + * Indicates this network uses a VPN transport. + */ + public static final int TRANSPORT_VPN = 4; + + /** + * Indicates this network uses a Wi-Fi Aware transport. + */ + public static final int TRANSPORT_WIFI_AWARE = 5; + + /** + * Indicates this network uses a LoWPAN transport. + */ + public static final int TRANSPORT_LOWPAN = 6; + + /** + * Indicates this network uses a Test-only virtual interface as a transport. + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final int TRANSPORT_TEST = 7; + + /** + * Indicates this network uses a USB transport. + */ + public static final int TRANSPORT_USB = 8; + + /** @hide */ + public static final int MIN_TRANSPORT = TRANSPORT_CELLULAR; + /** @hide */ + public static final int MAX_TRANSPORT = TRANSPORT_USB; + + /** @hide */ + public static boolean isValidTransport(@Transport int transportType) { + return (MIN_TRANSPORT <= transportType) && (transportType <= MAX_TRANSPORT); + } + + private static final String[] TRANSPORT_NAMES = { + "CELLULAR", + "WIFI", + "BLUETOOTH", + "ETHERNET", + "VPN", + "WIFI_AWARE", + "LOWPAN", + "TEST", + "USB" + }; + + /** + * Allowed transports on an unrestricted test network (in addition to TRANSPORT_TEST). + */ + private static final int UNRESTRICTED_TEST_NETWORKS_ALLOWED_TRANSPORTS = + 1 << TRANSPORT_TEST + // Test ethernet networks can be created with EthernetManager#setIncludeTestInterfaces + | 1 << TRANSPORT_ETHERNET + // Test VPN networks can be created but their UID ranges must be empty. + | 1 << TRANSPORT_VPN; + + /** + * Adds the given transport type to this {@code NetworkCapability} instance. + * Multiple transports may be applied. Note that when searching + * for a network to satisfy a request, any listed in the request will satisfy the request. + * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a + * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network + * to be selected. This is logically different than + * {@code NetworkCapabilities.NET_CAPABILITY_*} listed above. + * + * @param transportType the transport type to be added. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities addTransportType(@Transport int transportType) { + checkValidTransportType(transportType); + mTransportTypes |= 1 << transportType; + setNetworkSpecifier(mNetworkSpecifier); // used for exception checking + return this; + } + + /** + * Removes (if found) the given transport from this {@code NetworkCapability} instance. + * + * @param transportType the transport type to be removed. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities removeTransportType(@Transport int transportType) { + checkValidTransportType(transportType); + mTransportTypes &= ~(1 << transportType); + setNetworkSpecifier(mNetworkSpecifier); // used for exception checking + return this; + } + + /** + * Sets (or clears) the given transport on this {@link NetworkCapabilities} + * instance. + * + * @hide + */ + public @NonNull NetworkCapabilities setTransportType(@Transport int transportType, + boolean value) { + if (value) { + addTransportType(transportType); + } else { + removeTransportType(transportType); + } + return this; + } + + /** + * Gets all the transports set on this {@code NetworkCapability} instance. + * + * @return an array of transport type values for this instance. + * @hide + */ + @SystemApi + @NonNull public @Transport int[] getTransportTypes() { + return NetworkCapabilitiesUtils.unpackBits(mTransportTypes); + } + + /** + * Sets all the transports set on this {@code NetworkCapability} instance. + * This overwrites any existing transports. + * + * @hide + */ + public void setTransportTypes(@Transport int[] transportTypes) { + mTransportTypes = NetworkCapabilitiesUtils.packBits(transportTypes); + } + + /** + * Tests for the presence of a transport on this instance. + * + * @param transportType the transport type to be tested for. + * @return {@code true} if set on this instance. + */ + public boolean hasTransport(@Transport int transportType) { + return isValidTransport(transportType) && ((mTransportTypes & (1 << transportType)) != 0); + } + + private void combineTransportTypes(NetworkCapabilities nc) { + this.mTransportTypes |= nc.mTransportTypes; + } + + private boolean satisfiedByTransportTypes(NetworkCapabilities nc) { + return ((this.mTransportTypes == 0) + || ((this.mTransportTypes & nc.mTransportTypes) != 0)); + } + + /** @hide */ + public boolean equalsTransportTypes(NetworkCapabilities nc) { + return (nc.mTransportTypes == this.mTransportTypes); + } + + /** + * UID of the app that owns this network, or Process#INVALID_UID if none/unknown. + * + * <p>This field keeps track of the UID of the app that created this network and is in charge of + * its lifecycle. This could be the UID of apps such as the Wifi network suggestor, the running + * VPN, or Carrier Service app managing a cellular data connection. + * + * <p>For NetworkCapability instances being sent from ConnectivityService, this value MUST be + * reset to Process.INVALID_UID unless all the following conditions are met: + * + * <p>The caller is the network owner, AND one of the following sets of requirements is met: + * + * <ol> + * <li>The described Network is a VPN + * </ol> + * + * <p>OR: + * + * <ol> + * <li>The calling app is the network owner + * <li>The calling app has the ACCESS_FINE_LOCATION permission granted + * <li>The user's location toggle is on + * </ol> + * + * This is because the owner UID is location-sensitive. The apps that request a network could + * know where the device is if they can tell for sure the system has connected to the network + * they requested. + * + * <p>This is populated by the network agents and for the NetworkCapabilities instance sent by + * an app to the System Server, the value MUST be reset to Process.INVALID_UID by the system + * server. + */ + private int mOwnerUid = Process.INVALID_UID; + + /** + * Set the UID of the owner app. + * @hide + */ + public @NonNull NetworkCapabilities setOwnerUid(final int uid) { + mOwnerUid = uid; + return this; + } + + /** + * Retrieves the UID of the app that owns this network. + * + * <p>For user privacy reasons, this field will only be populated if the following conditions + * are met: + * + * <p>The caller is the network owner, AND one of the following sets of requirements is met: + * + * <ol> + * <li>The described Network is a VPN + * </ol> + * + * <p>OR: + * + * <ol> + * <li>The calling app is the network owner + * <li>The calling app has the ACCESS_FINE_LOCATION permission granted + * <li>The user's location toggle is on + * </ol> + * + * Instances of NetworkCapabilities sent to apps without the appropriate permissions will have + * this field cleared out. + * + * <p> + * This field will only be populated for VPN and wifi network suggestor apps (i.e using + * {@link WifiNetworkSuggestion}), and only for the network they own. + * In the case of wifi network suggestors apps, this field is also location sensitive, so the + * app needs to hold {@link android.Manifest.permission#ACCESS_FINE_LOCATION} permission. If the + * app targets SDK version greater than or equal to {@link Build.VERSION_CODES#S}, then they + * also need to use {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} to get the info in their + * callback. If the apps targets SDK version equal to {{@link Build.VERSION_CODES#R}, this field + * will always be included. The app will be blamed for location access if this field is + * included. + * </p> + */ + public int getOwnerUid() { + return mOwnerUid; + } + + private boolean equalsOwnerUid(@NonNull final NetworkCapabilities nc) { + return mOwnerUid == nc.mOwnerUid; + } + + /** + * UIDs of packages that are administrators of this network, or empty if none. + * + * <p>This field tracks the UIDs of packages that have permission to manage this network. + * + * <p>Network owners will also be listed as administrators. + * + * <p>For NetworkCapability instances being sent from the System Server, this value MUST be + * empty unless the destination is 1) the System Server, or 2) Telephony. In either case, the + * receiving entity must have the ACCESS_FINE_LOCATION permission and target R+. + * + * <p>When received from an app in a NetworkRequest this is always cleared out by the system + * server. This field is never used for matching NetworkRequests to NetworkAgents. + */ + @NonNull private int[] mAdministratorUids = new int[0]; + + /** + * Sets the int[] of UIDs that are administrators of this network. + * + * <p>UIDs included in administratorUids gain administrator privileges over this Network. + * Examples of UIDs that should be included in administratorUids are: + * + * <ul> + * <li>Carrier apps with privileges for the relevant subscription + * <li>Active VPN apps + * <li>Other application groups with a particular Network-related role + * </ul> + * + * <p>In general, user-supplied networks (such as WiFi networks) do not have an administrator. + * + * <p>An app is granted owner privileges over Networks that it supplies. The owner UID MUST + * always be included in administratorUids. + * + * <p>The administrator UIDs are set by network agents. + * + * @param administratorUids the UIDs to be set as administrators of this Network. + * @throws IllegalArgumentException if duplicate UIDs are contained in administratorUids + * @see #mAdministratorUids + * @hide + */ + @NonNull + public NetworkCapabilities setAdministratorUids(@NonNull final int[] administratorUids) { + mAdministratorUids = Arrays.copyOf(administratorUids, administratorUids.length); + Arrays.sort(mAdministratorUids); + for (int i = 0; i < mAdministratorUids.length - 1; i++) { + if (mAdministratorUids[i] >= mAdministratorUids[i + 1]) { + throw new IllegalArgumentException("All administrator UIDs must be unique"); + } + } + return this; + } + + /** + * Retrieves the UIDs that are administrators of this Network. + * + * <p>This is only populated in NetworkCapabilities objects that come from network agents for + * networks that are managed by specific apps on the system, such as carrier privileged apps or + * wifi suggestion apps. This will include the network owner. + * + * @return the int[] of UIDs that are administrators of this Network + * @see #mAdministratorUids + * @hide + */ + @NonNull + @SystemApi + public int[] getAdministratorUids() { + return Arrays.copyOf(mAdministratorUids, mAdministratorUids.length); + } + + /** + * Tests if the set of administrator UIDs of this network is the same as that of the passed one. + * + * <p>The administrator UIDs must be in sorted order. + * + * <p>nc is assumed non-null. Else, NPE. + * + * @hide + */ + @VisibleForTesting(visibility = PRIVATE) + public boolean equalsAdministratorUids(@NonNull final NetworkCapabilities nc) { + return Arrays.equals(mAdministratorUids, nc.mAdministratorUids); + } + + /** + * Combine the administrator UIDs of the capabilities. + * + * <p>This is only legal if either of the administrators lists are empty, or if they are equal. + * Combining administrator UIDs is only possible for combining non-overlapping sets of UIDs. + * + * <p>If both administrator lists are non-empty but not equal, they conflict with each other. In + * this case, it would not make sense to add them together. + */ + private void combineAdministratorUids(@NonNull final NetworkCapabilities nc) { + if (nc.mAdministratorUids.length == 0) return; + if (mAdministratorUids.length == 0) { + mAdministratorUids = Arrays.copyOf(nc.mAdministratorUids, nc.mAdministratorUids.length); + return; + } + if (!equalsAdministratorUids(nc)) { + throw new IllegalStateException("Can't combine two different administrator UID lists"); + } + } + + /** + * Value indicating that link bandwidth is unspecified. + * @hide + */ + public static final int LINK_BANDWIDTH_UNSPECIFIED = 0; + + /** + * Passive link bandwidth. This is a rough guide of the expected peak bandwidth + * for the first hop on the given transport. It is not measured, but may take into account + * link parameters (Radio technology, allocated channels, etc). + */ + private int mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED; + private int mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED; + + /** + * Sets the upstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * <p> + * {@see Builder#setLinkUpstreamBandwidthKbps} + * + * @param upKbps the estimated first hop upstream (device to network) bandwidth. + * @hide + */ + public @NonNull NetworkCapabilities setLinkUpstreamBandwidthKbps(int upKbps) { + mLinkUpBandwidthKbps = upKbps; + return this; + } + + /** + * Retrieves the upstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * + * @return The estimated first hop upstream (device to network) bandwidth. + */ + public int getLinkUpstreamBandwidthKbps() { + return mLinkUpBandwidthKbps; + } + + /** + * Sets the downstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * <p> + * {@see Builder#setLinkUpstreamBandwidthKbps} + * + * @param downKbps the estimated first hop downstream (network to device) bandwidth. + * @hide + */ + public @NonNull NetworkCapabilities setLinkDownstreamBandwidthKbps(int downKbps) { + mLinkDownBandwidthKbps = downKbps; + return this; + } + + /** + * Retrieves the downstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * + * @return The estimated first hop downstream (network to device) bandwidth. + */ + public int getLinkDownstreamBandwidthKbps() { + return mLinkDownBandwidthKbps; + } + + private void combineLinkBandwidths(NetworkCapabilities nc) { + this.mLinkUpBandwidthKbps = + Math.max(this.mLinkUpBandwidthKbps, nc.mLinkUpBandwidthKbps); + this.mLinkDownBandwidthKbps = + Math.max(this.mLinkDownBandwidthKbps, nc.mLinkDownBandwidthKbps); + } + private boolean satisfiedByLinkBandwidths(NetworkCapabilities nc) { + return !(this.mLinkUpBandwidthKbps > nc.mLinkUpBandwidthKbps + || this.mLinkDownBandwidthKbps > nc.mLinkDownBandwidthKbps); + } + private boolean equalsLinkBandwidths(NetworkCapabilities nc) { + return (this.mLinkUpBandwidthKbps == nc.mLinkUpBandwidthKbps + && this.mLinkDownBandwidthKbps == nc.mLinkDownBandwidthKbps); + } + /** @hide */ + public static int minBandwidth(int a, int b) { + if (a == LINK_BANDWIDTH_UNSPECIFIED) { + return b; + } else if (b == LINK_BANDWIDTH_UNSPECIFIED) { + return a; + } else { + return Math.min(a, b); + } + } + /** @hide */ + public static int maxBandwidth(int a, int b) { + return Math.max(a, b); + } + + private NetworkSpecifier mNetworkSpecifier = null; + private TransportInfo mTransportInfo = null; + + /** + * Sets the optional bearer specific network specifier. + * This has no meaning if a single transport is also not specified, so calling + * this without a single transport set will generate an exception, as will + * subsequently adding or removing transports after this is set. + * </p> + * + * @param networkSpecifier A concrete, parcelable framework class that extends + * NetworkSpecifier. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities setNetworkSpecifier( + @NonNull NetworkSpecifier networkSpecifier) { + if (networkSpecifier != null && Long.bitCount(mTransportTypes) != 1) { + throw new IllegalStateException("Must have a single transport specified to use " + + "setNetworkSpecifier"); + } + + mNetworkSpecifier = networkSpecifier; + + return this; + } + + /** + * Sets the optional transport specific information. + * + * @param transportInfo A concrete, parcelable framework class that extends + * {@link TransportInfo}. + * @return This NetworkCapabilities instance, to facilitate chaining. + * @hide + */ + public @NonNull NetworkCapabilities setTransportInfo(@NonNull TransportInfo transportInfo) { + mTransportInfo = transportInfo; + return this; + } + + /** + * Gets the optional bearer specific network specifier. May be {@code null} if not set. + * + * @return The optional {@link NetworkSpecifier} specifying the bearer specific network + * specifier or {@code null}. + */ + public @Nullable NetworkSpecifier getNetworkSpecifier() { + return mNetworkSpecifier; + } + + /** + * Returns a transport-specific information container. The application may cast this + * container to a concrete sub-class based on its knowledge of the network request. The + * application should be able to deal with a {@code null} return value or an invalid case, + * e.g. use {@code instanceof} operator to verify expected type. + * + * @return A concrete implementation of the {@link TransportInfo} class or null if not + * available for the network. + */ + @Nullable public TransportInfo getTransportInfo() { + return mTransportInfo; + } + + private void combineSpecifiers(NetworkCapabilities nc) { + if (mNetworkSpecifier != null && !mNetworkSpecifier.equals(nc.mNetworkSpecifier)) { + throw new IllegalStateException("Can't combine two networkSpecifiers"); + } + setNetworkSpecifier(nc.mNetworkSpecifier); + } + + private boolean satisfiedBySpecifier(NetworkCapabilities nc) { + return mNetworkSpecifier == null || mNetworkSpecifier.canBeSatisfiedBy(nc.mNetworkSpecifier) + || nc.mNetworkSpecifier instanceof MatchAllNetworkSpecifier; + } + + private boolean equalsSpecifier(NetworkCapabilities nc) { + return Objects.equals(mNetworkSpecifier, nc.mNetworkSpecifier); + } + + private void combineTransportInfos(NetworkCapabilities nc) { + if (mTransportInfo != null && !mTransportInfo.equals(nc.mTransportInfo)) { + throw new IllegalStateException("Can't combine two TransportInfos"); + } + setTransportInfo(nc.mTransportInfo); + } + + private boolean equalsTransportInfo(NetworkCapabilities nc) { + return Objects.equals(mTransportInfo, nc.mTransportInfo); + } + + /** + * Magic value that indicates no signal strength provided. A request specifying this value is + * always satisfied. + */ + public static final int SIGNAL_STRENGTH_UNSPECIFIED = Integer.MIN_VALUE; + + /** + * Signal strength. This is a signed integer, and higher values indicate better signal. + * The exact units are bearer-dependent. For example, Wi-Fi uses RSSI. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + private int mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED; + + /** + * Sets the signal strength. This is a signed integer, with higher values indicating a stronger + * signal. The exact units are bearer-dependent. For example, Wi-Fi uses the same RSSI units + * reported by wifi code. + * <p> + * Note that when used to register a network callback, this specifies the minimum acceptable + * signal strength. When received as the state of an existing network it specifies the current + * value. A value of code SIGNAL_STRENGTH_UNSPECIFIED} means no value when received and has no + * effect when requesting a callback. + * + * @param signalStrength the bearer-specific signal strength. + * @hide + */ + public @NonNull NetworkCapabilities setSignalStrength(int signalStrength) { + mSignalStrength = signalStrength; + return this; + } + + /** + * Returns {@code true} if this object specifies a signal strength. + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public boolean hasSignalStrength() { + return mSignalStrength > SIGNAL_STRENGTH_UNSPECIFIED; + } + + /** + * Retrieves the signal strength. + * + * @return The bearer-specific signal strength. + */ + public int getSignalStrength() { + return mSignalStrength; + } + + private void combineSignalStrength(NetworkCapabilities nc) { + this.mSignalStrength = Math.max(this.mSignalStrength, nc.mSignalStrength); + } + + private boolean satisfiedBySignalStrength(NetworkCapabilities nc) { + return this.mSignalStrength <= nc.mSignalStrength; + } + + private boolean equalsSignalStrength(NetworkCapabilities nc) { + return this.mSignalStrength == nc.mSignalStrength; + } + + /** + * List of UIDs this network applies to. No restriction if null. + * <p> + * For networks, mUids represent the list of network this applies to, and null means this + * network applies to all UIDs. + * For requests, mUids is the list of UIDs this network MUST apply to to match ; ALL UIDs + * must be included in a network so that they match. As an exception to the general rule, + * a null mUids field for requests mean "no requirements" rather than what the general rule + * would suggest ("must apply to all UIDs") : this is because this has shown to be what users + * of this API expect in practice. A network that must match all UIDs can still be + * expressed with a set ranging the entire set of possible UIDs. + * <p> + * mUids is typically (and at this time, only) used by VPN. This network is only available to + * the UIDs in this list, and it is their default network. Apps in this list that wish to + * bypass the VPN can do so iff the VPN app allows them to or if they are privileged. If this + * member is null, then the network is not restricted by app UID. If it's an empty list, then + * it means nobody can use it. + * As a special exception, the app managing this network (as identified by its UID stored in + * mOwnerUid) can always see this network. This is embodied by a special check in + * satisfiedByUids. That still does not mean the network necessarily <strong>applies</strong> + * to the app that manages it as determined by #appliesToUid. + * <p> + * Please note that in principle a single app can be associated with multiple UIDs because + * each app will have a different UID when it's run as a different (macro-)user. A single + * macro user can only have a single active VPN app at any given time however. + * <p> + * Also please be aware this class does not try to enforce any normalization on this. Callers + * can only alter the UIDs by setting them wholesale : this class does not provide any utility + * to add or remove individual UIDs or ranges. If callers have any normalization needs on + * their own (like requiring sortedness or no overlap) they need to enforce it + * themselves. Some of the internal methods also assume this is normalized as in no adjacent + * or overlapping ranges are present. + * + * @hide + */ + private ArraySet<UidRange> mUids = null; + + /** + * Convenience method to set the UIDs this network applies to to a single UID. + * @hide + */ + public @NonNull NetworkCapabilities setSingleUid(int uid) { + mUids = new ArraySet<>(1); + mUids.add(new UidRange(uid, uid)); + return this; + } + + /** + * Set the list of UIDs this network applies to. + * This makes a copy of the set so that callers can't modify it after the call. + * @hide + */ + public @NonNull NetworkCapabilities setUids(@Nullable Set<Range<Integer>> uids) { + mUids = UidRange.fromIntRanges(uids); + return this; + } + + /** + * Get the list of UIDs this network applies to. + * This returns a copy of the set so that callers can't modify the original object. + * + * @return the list of UIDs this network applies to. If {@code null}, then the network applies + * to all UIDs. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SuppressLint("NullableCollection") + public @Nullable Set<Range<Integer>> getUids() { + return UidRange.toIntRanges(mUids); + } + + /** + * Get the list of UIDs this network applies to. + * This returns a copy of the set so that callers can't modify the original object. + * @hide + */ + public @Nullable Set<UidRange> getUidRanges() { + if (mUids == null) return null; + + return new ArraySet<>(mUids); + } + + /** + * Test whether this network applies to this UID. + * @hide + */ + public boolean appliesToUid(int uid) { + if (null == mUids) return true; + for (UidRange range : mUids) { + if (range.contains(uid)) { + return true; + } + } + return false; + } + + /** + * Tests if the set of UIDs that this network applies to is the same as the passed network. + * <p> + * This test only checks whether equal range objects are in both sets. It will + * return false if the ranges are not exactly the same, even if the covered UIDs + * are for an equivalent result. + * <p> + * Note that this method is not very optimized, which is fine as long as it's not used very + * often. + * <p> + * nc is assumed nonnull. + * + * @hide + */ + @VisibleForTesting + public boolean equalsUids(@NonNull NetworkCapabilities nc) { + Set<UidRange> comparedUids = nc.mUids; + if (null == comparedUids) return null == mUids; + if (null == mUids) return false; + // Make a copy so it can be mutated to check that all ranges in mUids + // also are in uids. + final Set<UidRange> uids = new ArraySet<>(mUids); + for (UidRange range : comparedUids) { + if (!uids.contains(range)) { + return false; + } + uids.remove(range); + } + return uids.isEmpty(); + } + + /** + * Test whether the passed NetworkCapabilities satisfies the UIDs this capabilities require. + * + * This method is called on the NetworkCapabilities embedded in a request with the + * capabilities of an available network. It checks whether all the UIDs from this listen + * (representing the UIDs that must have access to the network) are satisfied by the UIDs + * in the passed nc (representing the UIDs that this network is available to). + * <p> + * As a special exception, the UID that created the passed network (as represented by its + * mOwnerUid field) always satisfies a NetworkRequest requiring it (of LISTEN + * or REQUEST types alike), even if the network does not apply to it. That is so a VPN app + * can see its own network when it listens for it. + * <p> + * nc is assumed nonnull. Else, NPE. + * @see #appliesToUid + * @hide + */ + public boolean satisfiedByUids(@NonNull NetworkCapabilities nc) { + if (null == nc.mUids || null == mUids) return true; // The network satisfies everything. + for (UidRange requiredRange : mUids) { + if (requiredRange.contains(nc.mOwnerUid)) return true; + if (!nc.appliesToUidRange(requiredRange)) { + return false; + } + } + return true; + } + + /** + * Returns whether this network applies to the passed ranges. + * This assumes that to apply, the passed range has to be entirely contained + * within one of the ranges this network applies to. If the ranges are not normalized, + * this method may return false even though all required UIDs are covered because no + * single range contained them all. + * @hide + */ + @VisibleForTesting + public boolean appliesToUidRange(@Nullable UidRange requiredRange) { + if (null == mUids) return true; + for (UidRange uidRange : mUids) { + if (uidRange.containsRange(requiredRange)) { + return true; + } + } + return false; + } + + /** + * Combine the UIDs this network currently applies to with the UIDs the passed + * NetworkCapabilities apply to. + * nc is assumed nonnull. + */ + private void combineUids(@NonNull NetworkCapabilities nc) { + if (null == nc.mUids || null == mUids) { + mUids = null; + return; + } + mUids.addAll(nc.mUids); + } + + + /** + * The SSID of the network, or null if not applicable or unknown. + * <p> + * This is filled in by wifi code. + * @hide + */ + private String mSSID; + + /** + * Sets the SSID of this network. + * @hide + */ + public @NonNull NetworkCapabilities setSSID(@Nullable String ssid) { + mSSID = ssid; + return this; + } + + /** + * Gets the SSID of this network, or null if none or unknown. + * @hide + */ + @SystemApi + public @Nullable String getSsid() { + return mSSID; + } + + /** + * Tests if the SSID of this network is the same as the SSID of the passed network. + * @hide + */ + public boolean equalsSSID(@NonNull NetworkCapabilities nc) { + return Objects.equals(mSSID, nc.mSSID); + } + + /** + * Check if the SSID requirements of this object are matched by the passed object. + * @hide + */ + public boolean satisfiedBySSID(@NonNull NetworkCapabilities nc) { + return mSSID == null || mSSID.equals(nc.mSSID); + } + + /** + * Combine SSIDs of the capabilities. + * <p> + * This is only legal if either the SSID of this object is null, or both SSIDs are + * equal. + * @hide + */ + private void combineSSIDs(@NonNull NetworkCapabilities nc) { + if (mSSID != null && !mSSID.equals(nc.mSSID)) { + throw new IllegalStateException("Can't combine two SSIDs"); + } + setSSID(nc.mSSID); + } + + /** + * Combine a set of Capabilities to this one. Useful for coming up with the complete set. + * <p> + * Note that this method may break an invariant of having a particular capability in either + * wanted or forbidden lists but never in both. Requests that have the same capability in + * both lists will never be satisfied. + * @hide + */ + public void combineCapabilities(@NonNull NetworkCapabilities nc) { + combineNetCapabilities(nc); + combineTransportTypes(nc); + combineLinkBandwidths(nc); + combineSpecifiers(nc); + combineTransportInfos(nc); + combineSignalStrength(nc); + combineUids(nc); + combineSSIDs(nc); + combineRequestor(nc); + combineAdministratorUids(nc); + combineSubscriptionIds(nc); + } + + /** + * Check if our requirements are satisfied by the given {@code NetworkCapabilities}. + * + * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements. + * @param onlyImmutable if {@code true}, do not consider mutable requirements such as link + * bandwidth, signal strength, or validation / captive portal status. + * + * @hide + */ + private boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc, boolean onlyImmutable) { + return (nc != null + && satisfiedByNetCapabilities(nc, onlyImmutable) + && satisfiedByTransportTypes(nc) + && (onlyImmutable || satisfiedByLinkBandwidths(nc)) + && satisfiedBySpecifier(nc) + && (onlyImmutable || satisfiedBySignalStrength(nc)) + && (onlyImmutable || satisfiedByUids(nc)) + && (onlyImmutable || satisfiedBySSID(nc)) + && (onlyImmutable || satisfiedByRequestor(nc)) + && (onlyImmutable || satisfiedBySubscriptionIds(nc))); + } + + /** + * Check if our requirements are satisfied by the given {@code NetworkCapabilities}. + * + * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements. + * + * @hide + */ + @SystemApi + public boolean satisfiedByNetworkCapabilities(@Nullable NetworkCapabilities nc) { + return satisfiedByNetworkCapabilities(nc, false); + } + + /** + * Check if our immutable requirements are satisfied by the given {@code NetworkCapabilities}. + * + * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements. + * + * @hide + */ + public boolean satisfiedByImmutableNetworkCapabilities(@Nullable NetworkCapabilities nc) { + return satisfiedByNetworkCapabilities(nc, true); + } + + /** + * Checks that our immutable capabilities are the same as those of the given + * {@code NetworkCapabilities} and return a String describing any difference. + * The returned String is empty if there is no difference. + * + * @hide + */ + public String describeImmutableDifferences(@Nullable NetworkCapabilities that) { + if (that == null) { + return "other NetworkCapabilities was null"; + } + + StringJoiner joiner = new StringJoiner(", "); + + // Ignore NOT_METERED being added or removed as it is effectively dynamic. http://b/63326103 + // TODO: properly support NOT_METERED as a mutable and requestable capability. + final long mask = ~MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_NOT_METERED); + long oldImmutableCapabilities = this.mNetworkCapabilities & mask; + long newImmutableCapabilities = that.mNetworkCapabilities & mask; + if (oldImmutableCapabilities != newImmutableCapabilities) { + String before = capabilityNamesOf(NetworkCapabilitiesUtils.unpackBits( + oldImmutableCapabilities)); + String after = capabilityNamesOf(NetworkCapabilitiesUtils.unpackBits( + newImmutableCapabilities)); + joiner.add(String.format("immutable capabilities changed: %s -> %s", before, after)); + } + + if (!equalsSpecifier(that)) { + NetworkSpecifier before = this.getNetworkSpecifier(); + NetworkSpecifier after = that.getNetworkSpecifier(); + joiner.add(String.format("specifier changed: %s -> %s", before, after)); + } + + if (!equalsTransportTypes(that)) { + String before = transportNamesOf(this.getTransportTypes()); + String after = transportNamesOf(that.getTransportTypes()); + joiner.add(String.format("transports changed: %s -> %s", before, after)); + } + + return joiner.toString(); + } + + /** + * Checks that our requestable capabilities are the same as those of the given + * {@code NetworkCapabilities}. + * + * @hide + */ + public boolean equalRequestableCapabilities(@Nullable NetworkCapabilities nc) { + if (nc == null) return false; + return (equalsNetCapabilitiesRequestable(nc) + && equalsTransportTypes(nc) + && equalsSpecifier(nc)); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null || (obj instanceof NetworkCapabilities == false)) return false; + NetworkCapabilities that = (NetworkCapabilities) obj; + return equalsNetCapabilities(that) + && equalsTransportTypes(that) + && equalsLinkBandwidths(that) + && equalsSignalStrength(that) + && equalsSpecifier(that) + && equalsTransportInfo(that) + && equalsUids(that) + && equalsSSID(that) + && equalsOwnerUid(that) + && equalsPrivateDnsBroken(that) + && equalsRequestor(that) + && equalsAdministratorUids(that) + && equalsSubscriptionIds(that); + } + + @Override + public int hashCode() { + return (int) (mNetworkCapabilities & 0xFFFFFFFF) + + ((int) (mNetworkCapabilities >> 32) * 3) + + ((int) (mForbiddenNetworkCapabilities & 0xFFFFFFFF) * 5) + + ((int) (mForbiddenNetworkCapabilities >> 32) * 7) + + ((int) (mTransportTypes & 0xFFFFFFFF) * 11) + + ((int) (mTransportTypes >> 32) * 13) + + mLinkUpBandwidthKbps * 17 + + mLinkDownBandwidthKbps * 19 + + Objects.hashCode(mNetworkSpecifier) * 23 + + mSignalStrength * 29 + + mOwnerUid * 31 + + Objects.hashCode(mUids) * 37 + + Objects.hashCode(mSSID) * 41 + + Objects.hashCode(mTransportInfo) * 43 + + Objects.hashCode(mPrivateDnsBroken) * 47 + + Objects.hashCode(mRequestorUid) * 53 + + Objects.hashCode(mRequestorPackageName) * 59 + + Arrays.hashCode(mAdministratorUids) * 61 + + Objects.hashCode(mSubIds) * 67; + } + + @Override + public int describeContents() { + return 0; + } + + private <T extends Parcelable> void writeParcelableArraySet(Parcel in, + @Nullable ArraySet<T> val, int flags) { + final int size = (val != null) ? val.size() : -1; + in.writeInt(size); + for (int i = 0; i < size; i++) { + in.writeParcelable(val.valueAt(i), flags); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mNetworkCapabilities); + dest.writeLong(mForbiddenNetworkCapabilities); + dest.writeLong(mTransportTypes); + dest.writeInt(mLinkUpBandwidthKbps); + dest.writeInt(mLinkDownBandwidthKbps); + dest.writeParcelable((Parcelable) mNetworkSpecifier, flags); + dest.writeParcelable((Parcelable) mTransportInfo, flags); + dest.writeInt(mSignalStrength); + writeParcelableArraySet(dest, mUids, flags); + dest.writeString(mSSID); + dest.writeBoolean(mPrivateDnsBroken); + dest.writeIntArray(getAdministratorUids()); + dest.writeInt(mOwnerUid); + dest.writeInt(mRequestorUid); + dest.writeString(mRequestorPackageName); + dest.writeIntArray(CollectionUtils.toIntArray(mSubIds)); + } + + public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR = + new Creator<NetworkCapabilities>() { + @Override + public NetworkCapabilities createFromParcel(Parcel in) { + NetworkCapabilities netCap = new NetworkCapabilities(); + + netCap.mNetworkCapabilities = in.readLong(); + netCap.mForbiddenNetworkCapabilities = in.readLong(); + netCap.mTransportTypes = in.readLong(); + netCap.mLinkUpBandwidthKbps = in.readInt(); + netCap.mLinkDownBandwidthKbps = in.readInt(); + netCap.mNetworkSpecifier = in.readParcelable(null); + netCap.mTransportInfo = in.readParcelable(null); + netCap.mSignalStrength = in.readInt(); + netCap.mUids = readParcelableArraySet(in, null /* ClassLoader, null for default */); + netCap.mSSID = in.readString(); + netCap.mPrivateDnsBroken = in.readBoolean(); + netCap.setAdministratorUids(in.createIntArray()); + netCap.mOwnerUid = in.readInt(); + netCap.mRequestorUid = in.readInt(); + netCap.mRequestorPackageName = in.readString(); + netCap.mSubIds = new ArraySet<>(); + final int[] subIdInts = Objects.requireNonNull(in.createIntArray()); + for (int i = 0; i < subIdInts.length; i++) { + netCap.mSubIds.add(subIdInts[i]); + } + return netCap; + } + @Override + public NetworkCapabilities[] newArray(int size) { + return new NetworkCapabilities[size]; + } + + private @Nullable <T extends Parcelable> ArraySet<T> readParcelableArraySet(Parcel in, + @Nullable ClassLoader loader) { + final int size = in.readInt(); + if (size < 0) { + return null; + } + final ArraySet<T> result = new ArraySet<>(size); + for (int i = 0; i < size; i++) { + final T value = in.readParcelable(loader); + result.add(value); + } + return result; + } + }; + + @Override + public @NonNull String toString() { + final StringBuilder sb = new StringBuilder("["); + if (0 != mTransportTypes) { + sb.append(" Transports: "); + appendStringRepresentationOfBitMaskToStringBuilder(sb, mTransportTypes, + NetworkCapabilities::transportNameOf, "|"); + } + if (0 != mNetworkCapabilities) { + sb.append(" Capabilities: "); + appendStringRepresentationOfBitMaskToStringBuilder(sb, mNetworkCapabilities, + NetworkCapabilities::capabilityNameOf, "&"); + } + if (0 != mForbiddenNetworkCapabilities) { + sb.append(" Forbidden: "); + appendStringRepresentationOfBitMaskToStringBuilder(sb, mForbiddenNetworkCapabilities, + NetworkCapabilities::capabilityNameOf, "&"); + } + if (mLinkUpBandwidthKbps > 0) { + sb.append(" LinkUpBandwidth>=").append(mLinkUpBandwidthKbps).append("Kbps"); + } + if (mLinkDownBandwidthKbps > 0) { + sb.append(" LinkDnBandwidth>=").append(mLinkDownBandwidthKbps).append("Kbps"); + } + if (mNetworkSpecifier != null) { + sb.append(" Specifier: <").append(mNetworkSpecifier).append(">"); + } + if (mTransportInfo != null) { + sb.append(" TransportInfo: <").append(mTransportInfo).append(">"); + } + if (hasSignalStrength()) { + sb.append(" SignalStrength: ").append(mSignalStrength); + } + + if (null != mUids) { + if ((1 == mUids.size()) && (mUids.valueAt(0).count() == 1)) { + sb.append(" Uid: ").append(mUids.valueAt(0).start); + } else { + sb.append(" Uids: <").append(mUids).append(">"); + } + } + if (mOwnerUid != Process.INVALID_UID) { + sb.append(" OwnerUid: ").append(mOwnerUid); + } + + if (mAdministratorUids != null && mAdministratorUids.length != 0) { + sb.append(" AdminUids: ").append(Arrays.toString(mAdministratorUids)); + } + + if (mRequestorUid != Process.INVALID_UID) { + sb.append(" RequestorUid: ").append(mRequestorUid); + } + + if (mRequestorPackageName != null) { + sb.append(" RequestorPkg: ").append(mRequestorPackageName); + } + + if (null != mSSID) { + sb.append(" SSID: ").append(mSSID); + } + + if (mPrivateDnsBroken) { + sb.append(" PrivateDnsBroken"); + } + + if (!mSubIds.isEmpty()) { + sb.append(" SubscriptionIds: ").append(mSubIds); + } + + sb.append("]"); + return sb.toString(); + } + + + private interface NameOf { + String nameOf(int value); + } + + /** + * @hide + */ + public static void appendStringRepresentationOfBitMaskToStringBuilder(@NonNull StringBuilder sb, + long bitMask, @NonNull NameOf nameFetcher, @NonNull String separator) { + int bitPos = 0; + boolean firstElementAdded = false; + while (bitMask != 0) { + if ((bitMask & 1) != 0) { + if (firstElementAdded) { + sb.append(separator); + } else { + firstElementAdded = true; + } + sb.append(nameFetcher.nameOf(bitPos)); + } + bitMask >>= 1; + ++bitPos; + } + } + + /** + * @hide + */ + public static @NonNull String capabilityNamesOf(@Nullable @NetCapability int[] capabilities) { + StringJoiner joiner = new StringJoiner("|"); + if (capabilities != null) { + for (int c : capabilities) { + joiner.add(capabilityNameOf(c)); + } + } + return joiner.toString(); + } + + /** + * @hide + */ + public static @NonNull String capabilityNameOf(@NetCapability int capability) { + switch (capability) { + case NET_CAPABILITY_MMS: return "MMS"; + case NET_CAPABILITY_SUPL: return "SUPL"; + case NET_CAPABILITY_DUN: return "DUN"; + case NET_CAPABILITY_FOTA: return "FOTA"; + case NET_CAPABILITY_IMS: return "IMS"; + case NET_CAPABILITY_CBS: return "CBS"; + case NET_CAPABILITY_WIFI_P2P: return "WIFI_P2P"; + case NET_CAPABILITY_IA: return "IA"; + case NET_CAPABILITY_RCS: return "RCS"; + case NET_CAPABILITY_XCAP: return "XCAP"; + case NET_CAPABILITY_EIMS: return "EIMS"; + case NET_CAPABILITY_NOT_METERED: return "NOT_METERED"; + case NET_CAPABILITY_INTERNET: return "INTERNET"; + case NET_CAPABILITY_NOT_RESTRICTED: return "NOT_RESTRICTED"; + case NET_CAPABILITY_TRUSTED: return "TRUSTED"; + case NET_CAPABILITY_NOT_VPN: return "NOT_VPN"; + case NET_CAPABILITY_VALIDATED: return "VALIDATED"; + case NET_CAPABILITY_CAPTIVE_PORTAL: return "CAPTIVE_PORTAL"; + case NET_CAPABILITY_NOT_ROAMING: return "NOT_ROAMING"; + case NET_CAPABILITY_FOREGROUND: return "FOREGROUND"; + case NET_CAPABILITY_NOT_CONGESTED: return "NOT_CONGESTED"; + case NET_CAPABILITY_NOT_SUSPENDED: return "NOT_SUSPENDED"; + case NET_CAPABILITY_OEM_PAID: return "OEM_PAID"; + case NET_CAPABILITY_MCX: return "MCX"; + case NET_CAPABILITY_PARTIAL_CONNECTIVITY: return "PARTIAL_CONNECTIVITY"; + case NET_CAPABILITY_TEMPORARILY_NOT_METERED: return "TEMPORARILY_NOT_METERED"; + case NET_CAPABILITY_OEM_PRIVATE: return "OEM_PRIVATE"; + case NET_CAPABILITY_VEHICLE_INTERNAL: return "VEHICLE_INTERNAL"; + case NET_CAPABILITY_NOT_VCN_MANAGED: return "NOT_VCN_MANAGED"; + case NET_CAPABILITY_ENTERPRISE: return "ENTERPRISE"; + case NET_CAPABILITY_VSIM: return "VSIM"; + case NET_CAPABILITY_BIP: return "BIP"; + case NET_CAPABILITY_HEAD_UNIT: return "HEAD_UNIT"; + default: return Integer.toString(capability); + } + } + + /** + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static @NonNull String transportNamesOf(@Nullable @Transport int[] types) { + StringJoiner joiner = new StringJoiner("|"); + if (types != null) { + for (int t : types) { + joiner.add(transportNameOf(t)); + } + } + return joiner.toString(); + } + + /** + * @hide + */ + public static @NonNull String transportNameOf(@Transport int transport) { + if (!isValidTransport(transport)) { + return "UNKNOWN"; + } + return TRANSPORT_NAMES[transport]; + } + + private static void checkValidTransportType(@Transport int transport) { + if (!isValidTransport(transport)) { + throw new IllegalArgumentException("Invalid TransportType " + transport); + } + } + + private static boolean isValidCapability(@NetworkCapabilities.NetCapability int capability) { + return capability >= MIN_NET_CAPABILITY && capability <= MAX_NET_CAPABILITY; + } + + private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) { + if (!isValidCapability(capability)) { + throw new IllegalArgumentException("NetworkCapability " + capability + "out of range"); + } + } + + /** + * Check if this {@code NetworkCapability} instance is metered. + * + * @return {@code true} if {@code NET_CAPABILITY_NOT_METERED} is not set on this instance. + * @hide + */ + public boolean isMetered() { + return !hasCapability(NET_CAPABILITY_NOT_METERED); + } + + /** + * Check if private dns is broken. + * + * @return {@code true} if private DNS is broken on this network. + * @hide + */ + @SystemApi + public boolean isPrivateDnsBroken() { + return mPrivateDnsBroken; + } + + /** + * Set mPrivateDnsBroken to true when private dns is broken. + * + * @param broken the status of private DNS to be set. + * @hide + */ + public void setPrivateDnsBroken(boolean broken) { + mPrivateDnsBroken = broken; + } + + private boolean equalsPrivateDnsBroken(NetworkCapabilities nc) { + return mPrivateDnsBroken == nc.mPrivateDnsBroken; + } + + /** + * Set the UID of the app making the request. + * + * For instances of NetworkCapabilities representing a request, sets the + * UID of the app making the request. For a network created by the system, + * sets the UID of the only app whose requests can match this network. + * This can be set to {@link Process#INVALID_UID} if there is no such app, + * or if this instance of NetworkCapabilities is about to be sent to a + * party that should not learn about this. + * + * @param uid UID of the app. + * @hide + */ + public @NonNull NetworkCapabilities setRequestorUid(int uid) { + mRequestorUid = uid; + return this; + } + + /** + * Returns the UID of the app making the request. + * + * For a NetworkRequest being made by an app, contains the app's UID. For a network + * created by the system, contains the UID of the only app whose requests can match + * this network, or {@link Process#INVALID_UID} if none or if the + * caller does not have permission to learn about this. + * + * @return the uid of the app making the request. + * @hide + */ + public int getRequestorUid() { + return mRequestorUid; + } + + /** + * Set the package name of the app making the request. + * + * For instances of NetworkCapabilities representing a request, sets the + * package name of the app making the request. For a network created by the system, + * sets the package name of the only app whose requests can match this network. + * This can be set to null if there is no such app, or if this instance of + * NetworkCapabilities is about to be sent to a party that should not learn about this. + * + * @param packageName package name of the app. + * @hide + */ + public @NonNull NetworkCapabilities setRequestorPackageName(@NonNull String packageName) { + mRequestorPackageName = packageName; + return this; + } + + /** + * Returns the package name of the app making the request. + * + * For a NetworkRequest being made by an app, contains the app's package name. For a + * network created by the system, contains the package name of the only app whose + * requests can match this network, or null if none or if the caller does not have + * permission to learn about this. + * + * @return the package name of the app making the request. + * @hide + */ + @Nullable + public String getRequestorPackageName() { + return mRequestorPackageName; + } + + /** + * Set the uid and package name of the app causing this network to exist. + * + * {@see #setRequestorUid} and {@link #setRequestorPackageName} + * + * @param uid UID of the app. + * @param packageName package name of the app. + * @hide + */ + public @NonNull NetworkCapabilities setRequestorUidAndPackageName( + int uid, @NonNull String packageName) { + return setRequestorUid(uid).setRequestorPackageName(packageName); + } + + /** + * Test whether the passed NetworkCapabilities satisfies the requestor restrictions of this + * capabilities. + * + * This method is called on the NetworkCapabilities embedded in a request with the + * capabilities of an available network. If the available network, sets a specific + * requestor (by uid and optionally package name), then this will only match a request from the + * same app. If either of the capabilities have an unset uid or package name, then it matches + * everything. + * <p> + * nc is assumed nonnull. Else, NPE. + */ + private boolean satisfiedByRequestor(NetworkCapabilities nc) { + // No uid set, matches everything. + if (mRequestorUid == Process.INVALID_UID || nc.mRequestorUid == Process.INVALID_UID) { + return true; + } + // uids don't match. + if (mRequestorUid != nc.mRequestorUid) return false; + // No package names set, matches everything + if (null == nc.mRequestorPackageName || null == mRequestorPackageName) return true; + // check for package name match. + return TextUtils.equals(mRequestorPackageName, nc.mRequestorPackageName); + } + + /** + * Combine requestor info of the capabilities. + * <p> + * This is only legal if either the requestor info of this object is reset, or both info are + * equal. + * nc is assumed nonnull. + */ + private void combineRequestor(@NonNull NetworkCapabilities nc) { + if (mRequestorUid != Process.INVALID_UID && mRequestorUid != nc.mOwnerUid) { + throw new IllegalStateException("Can't combine two uids"); + } + if (mRequestorPackageName != null + && !mRequestorPackageName.equals(nc.mRequestorPackageName)) { + throw new IllegalStateException("Can't combine two package names"); + } + setRequestorUid(nc.mRequestorUid); + setRequestorPackageName(nc.mRequestorPackageName); + } + + private boolean equalsRequestor(NetworkCapabilities nc) { + return mRequestorUid == nc.mRequestorUid + && TextUtils.equals(mRequestorPackageName, nc.mRequestorPackageName); + } + + /** + * Set of the subscription IDs that identifies the network or request, empty if none. + */ + @NonNull + private ArraySet<Integer> mSubIds = new ArraySet<>(); + + /** + * Sets the subscription ID set that associated to this network or request. + * + * @hide + */ + @NonNull + public NetworkCapabilities setSubscriptionIds(@NonNull Set<Integer> subIds) { + mSubIds = new ArraySet(Objects.requireNonNull(subIds)); + return this; + } + + /** + * Gets the subscription ID set that associated to this network or request. + * + * <p>Instances of NetworkCapabilities will only have this field populated by the system if the + * receiver holds the NETWORK_FACTORY permission. In all other cases, it will be the empty set. + * + * @return + * @hide + */ + @NonNull + @SystemApi + public Set<Integer> getSubscriptionIds() { + return new ArraySet<>(mSubIds); + } + + /** + * Tests if the subscription ID set of this network is the same as that of the passed one. + */ + private boolean equalsSubscriptionIds(@NonNull NetworkCapabilities nc) { + return Objects.equals(mSubIds, nc.mSubIds); + } + + /** + * Check if the subscription ID set requirements of this object are matched by the passed one. + * If specified in the request, the passed one need to have at least one subId and at least + * one of them needs to be in the request set. + */ + private boolean satisfiedBySubscriptionIds(@NonNull NetworkCapabilities nc) { + if (mSubIds.isEmpty()) return true; + if (nc.mSubIds.isEmpty()) return false; + for (final Integer subId : nc.mSubIds) { + if (mSubIds.contains(subId)) return true; + } + return false; + } + + /** + * Combine subscription ID set of the capabilities. + * + * <p>This is only legal if the subscription Ids are equal. + * + * <p>If both subscription IDs are not equal, they belong to different subscription + * (or no subscription). In this case, it would not make sense to add them together. + */ + private void combineSubscriptionIds(@NonNull NetworkCapabilities nc) { + if (!Objects.equals(mSubIds, nc.mSubIds)) { + throw new IllegalStateException("Can't combine two subscription ID sets"); + } + } + + /** + * Returns a bitmask of all the applicable redactions (based on the permissions held by the + * receiving app) to be performed on this object. + * + * @return bitmask of redactions applicable on this instance. + * @hide + */ + public @RedactionType long getApplicableRedactions() { + // Currently, there are no fields redacted in NetworkCapabilities itself, so we just + // passthrough the redactions required by the embedded TransportInfo. If this changes + // in the future, modify this method. + if (mTransportInfo == null) { + return NetworkCapabilities.REDACT_NONE; + } + return mTransportInfo.getApplicableRedactions(); + } + + private NetworkCapabilities removeDefaultCapabilites() { + mNetworkCapabilities &= ~DEFAULT_CAPABILITIES; + return this; + } + + /** + * Builder class for NetworkCapabilities. + * + * This class is mainly for for {@link NetworkAgent} instances to use. Many fields in + * the built class require holding a signature permission to use - mostly + * {@link android.Manifest.permission.NETWORK_FACTORY}, but refer to the specific + * description of each setter. As this class lives entirely in app space it does not + * enforce these restrictions itself but the system server clears out the relevant + * fields when receiving a NetworkCapabilities object from a caller without the + * appropriate permission. + * + * Apps don't use this builder directly. Instead, they use {@link NetworkRequest} via + * its builder object. + * + * @hide + */ + @SystemApi + public static final class Builder { + private final NetworkCapabilities mCaps; + + /** + * Creates a new Builder to construct NetworkCapabilities objects. + */ + public Builder() { + mCaps = new NetworkCapabilities(); + } + + /** + * Creates a new Builder of NetworkCapabilities from an existing instance. + */ + public Builder(@NonNull final NetworkCapabilities nc) { + Objects.requireNonNull(nc); + mCaps = new NetworkCapabilities(nc); + } + + /** + * Creates a new Builder without the default capabilities. + */ + @NonNull + public static Builder withoutDefaultCapabilities() { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.removeDefaultCapabilites(); + return new Builder(nc); + } + + /** + * Adds the given transport type. + * + * Multiple transports may be added. Note that when searching for a network to satisfy a + * request, satisfying any of the transports listed in the request will satisfy the request. + * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a + * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network + * to be selected. This is logically different than + * {@code NetworkCapabilities.NET_CAPABILITY_*}. Also note that multiple networks with the + * same transport type may be active concurrently. + * + * @param transportType the transport type to be added or removed. + * @return this builder + */ + @NonNull + public Builder addTransportType(@Transport int transportType) { + checkValidTransportType(transportType); + mCaps.addTransportType(transportType); + return this; + } + + /** + * Removes the given transport type. + * + * {@see #addTransportType}. + * + * @param transportType the transport type to be added or removed. + * @return this builder + */ + @NonNull + public Builder removeTransportType(@Transport int transportType) { + checkValidTransportType(transportType); + mCaps.removeTransportType(transportType); + return this; + } + + /** + * Adds the given capability. + * + * @param capability the capability + * @return this builder + */ + @NonNull + public Builder addCapability(@NetCapability final int capability) { + mCaps.setCapability(capability, true); + return this; + } + + /** + * Removes the given capability. + * + * @param capability the capability + * @return this builder + */ + @NonNull + public Builder removeCapability(@NetCapability final int capability) { + mCaps.setCapability(capability, false); + return this; + } + + /** + * Sets the owner UID. + * + * The default value is {@link Process#INVALID_UID}. Pass this value to reset. + * + * Note: for security the system will clear out this field when received from a + * non-privileged source. + * + * @param ownerUid the owner UID + * @return this builder + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public Builder setOwnerUid(final int ownerUid) { + mCaps.setOwnerUid(ownerUid); + return this; + } + + /** + * Sets the list of UIDs that are administrators of this network. + * + * <p>UIDs included in administratorUids gain administrator privileges over this + * Network. Examples of UIDs that should be included in administratorUids are: + * <ul> + * <li>Carrier apps with privileges for the relevant subscription + * <li>Active VPN apps + * <li>Other application groups with a particular Network-related role + * </ul> + * + * <p>In general, user-supplied networks (such as WiFi networks) do not have + * administrators. + * + * <p>An app is granted owner privileges over Networks that it supplies. The owner + * UID MUST always be included in administratorUids. + * + * The default value is the empty array. Pass an empty array to reset. + * + * Note: for security the system will clear out this field when received from a + * non-privileged source, such as an app using reflection to call this or + * mutate the member in the built object. + * + * @param administratorUids the UIDs to be set as administrators of this Network. + * @return this builder + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public Builder setAdministratorUids(@NonNull final int[] administratorUids) { + Objects.requireNonNull(administratorUids); + mCaps.setAdministratorUids(administratorUids); + return this; + } + + /** + * Sets the upstream bandwidth of the link. + * + * Sets the upstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * <p> + * Note that when used to request a network, this specifies the minimum acceptable. + * When received as the state of an existing network this specifies the typical + * first hop bandwidth expected. This is never measured, but rather is inferred + * from technology type and other link parameters. It could be used to differentiate + * between very slow 1xRTT cellular links and other faster networks or even between + * 802.11b vs 802.11AC wifi technologies. It should not be used to differentiate between + * fast backhauls and slow backhauls. + * + * @param upKbps the estimated first hop upstream (device to network) bandwidth. + * @return this builder + */ + @NonNull + public Builder setLinkUpstreamBandwidthKbps(final int upKbps) { + mCaps.setLinkUpstreamBandwidthKbps(upKbps); + return this; + } + + /** + * Sets the downstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * <p> + * Note that when used to request a network, this specifies the minimum acceptable. + * When received as the state of an existing network this specifies the typical + * first hop bandwidth expected. This is never measured, but rather is inferred + * from technology type and other link parameters. It could be used to differentiate + * between very slow 1xRTT cellular links and other faster networks or even between + * 802.11b vs 802.11AC wifi technologies. It should not be used to differentiate between + * fast backhauls and slow backhauls. + * + * @param downKbps the estimated first hop downstream (network to device) bandwidth. + * @return this builder + */ + @NonNull + public Builder setLinkDownstreamBandwidthKbps(final int downKbps) { + mCaps.setLinkDownstreamBandwidthKbps(downKbps); + return this; + } + + /** + * Sets the optional bearer specific network specifier. + * This has no meaning if a single transport is also not specified, so calling + * this without a single transport set will generate an exception, as will + * subsequently adding or removing transports after this is set. + * </p> + * + * @param specifier a concrete, parcelable framework class that extends NetworkSpecifier, + * or null to clear it. + * @return this builder + */ + @NonNull + public Builder setNetworkSpecifier(@Nullable final NetworkSpecifier specifier) { + mCaps.setNetworkSpecifier(specifier); + return this; + } + + /** + * Sets the optional transport specific information. + * + * @param info A concrete, parcelable framework class that extends {@link TransportInfo}, + * or null to clear it. + * @return this builder + */ + @NonNull + public Builder setTransportInfo(@Nullable final TransportInfo info) { + mCaps.setTransportInfo(info); + return this; + } + + /** + * Sets the signal strength. This is a signed integer, with higher values indicating a + * stronger signal. The exact units are bearer-dependent. For example, Wi-Fi uses the + * same RSSI units reported by wifi code. + * <p> + * Note that when used to register a network callback, this specifies the minimum + * acceptable signal strength. When received as the state of an existing network it + * specifies the current value. A value of code SIGNAL_STRENGTH_UNSPECIFIED} means + * no value when received and has no effect when requesting a callback. + * + * Note: for security the system will throw if it receives a NetworkRequest where + * the underlying NetworkCapabilities has this member set from a source that does + * not hold the {@link android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP} + * permission. Apps with this permission can use this indirectly through + * {@link android.net.NetworkRequest}. + * + * @param signalStrength the bearer-specific signal strength. + * @return this builder + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) + public Builder setSignalStrength(final int signalStrength) { + mCaps.setSignalStrength(signalStrength); + return this; + } + + /** + * Sets the SSID of this network. + * + * Note: for security the system will clear out this field when received from a + * non-privileged source, like an app using reflection to set this. + * + * @param ssid the SSID, or null to clear it. + * @return this builder + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public Builder setSsid(@Nullable final String ssid) { + mCaps.setSSID(ssid); + return this; + } + + /** + * Set the uid of the app causing this network to exist. + * + * Note: for security the system will clear out this field when received from a + * non-privileged source. + * + * @param uid UID of the app. + * @return this builder + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public Builder setRequestorUid(final int uid) { + mCaps.setRequestorUid(uid); + return this; + } + + /** + * Set the package name of the app causing this network to exist. + * + * Note: for security the system will clear out this field when received from a + * non-privileged source. + * + * @param packageName package name of the app, or null to clear it. + * @return this builder + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public Builder setRequestorPackageName(@Nullable final String packageName) { + mCaps.setRequestorPackageName(packageName); + return this; + } + + /** + * Set the subscription ID set. + * + * <p>SubIds are populated in NetworkCapability instances from the system only for callers + * that hold the NETWORK_FACTORY permission. Similarly, the system will reject any + * NetworkRequests filed with a non-empty set of subIds unless the caller holds the + * NETWORK_FACTORY permission. + * + * @param subIds a set that represent the subscription IDs. Empty if clean up. + * @return this builder. + * @hide + */ + @NonNull + @SystemApi + public Builder setSubscriptionIds(@NonNull final Set<Integer> subIds) { + mCaps.setSubscriptionIds(subIds); + return this; + } + + /** + * Set the list of UIDs this network applies to. + * + * @param uids the list of UIDs this network applies to, or {@code null} if this network + * applies to all UIDs. + * @return this builder + * @hide + */ + @NonNull + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public Builder setUids(@Nullable Set<Range<Integer>> uids) { + mCaps.setUids(uids); + return this; + } + + /** + * Builds the instance of the capabilities. + * + * @return the built instance of NetworkCapabilities. + */ + @NonNull + public NetworkCapabilities build() { + if (mCaps.getOwnerUid() != Process.INVALID_UID) { + if (!CollectionUtils.contains(mCaps.getAdministratorUids(), mCaps.getOwnerUid())) { + throw new IllegalStateException("The owner UID must be included in " + + " administrator UIDs."); + } + } + return new NetworkCapabilities(mCaps); + } + } +} diff --git a/framework/src/android/net/NetworkConfig.java b/framework/src/android/net/NetworkConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..32a2cda00370a3b5fa779fa2cb145e1ec08f2aed --- /dev/null +++ b/framework/src/android/net/NetworkConfig.java @@ -0,0 +1,80 @@ +/* + * 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; + +import java.util.Locale; + +/** + * Describes the buildtime configuration of a network. + * Holds settings read from resources. + * @hide + */ +public class NetworkConfig { + /** + * Human readable string + */ + public String name; + + /** + * Type from ConnectivityManager + */ + public int type; + + /** + * the radio number from radio attributes config + */ + public int radio; + + /** + * higher number == higher priority when turning off connections + */ + public int priority; + + /** + * indicates the boot time dependencyMet setting + */ + public boolean dependencyMet; + + /** + * indicates the default restoral timer in seconds + * if the network is used as a special network feature + * -1 indicates no restoration of default + */ + public int restoreTime; + + /** + * input string from config.xml resource. Uses the form: + * [Connection name],[ConnectivityManager connection type], + * [associated radio-type],[priority],[dependencyMet] + */ + public NetworkConfig(String init) { + String fragments[] = init.split(","); + name = fragments[0].trim().toLowerCase(Locale.ROOT); + type = Integer.parseInt(fragments[1]); + radio = Integer.parseInt(fragments[2]); + priority = Integer.parseInt(fragments[3]); + restoreTime = Integer.parseInt(fragments[4]); + dependencyMet = Boolean.parseBoolean(fragments[5]); + } + + /** + * Indicates if this network is supposed to be default-routable + */ + public boolean isDefault() { + return (type == radio); + } +} diff --git a/framework/src/android/net/NetworkInfo.java b/framework/src/android/net/NetworkInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..bb234945933195dbed346810db0dbd88d08b4863 --- /dev/null +++ b/framework/src/android/net/NetworkInfo.java @@ -0,0 +1,625 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.EnumMap; + +/** + * Describes the status of a network interface. + * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents + * the current network connection. + * + * @deprecated Callers should instead use the {@link ConnectivityManager.NetworkCallback} API to + * learn about connectivity changes, or switch to use + * {@link ConnectivityManager#getNetworkCapabilities} or + * {@link ConnectivityManager#getLinkProperties} to get information synchronously. Keep + * in mind that while callbacks are guaranteed to be called for every event in order, + * synchronous calls have no such constraints, and as such it is unadvisable to use the + * synchronous methods inside the callbacks as they will often not offer a view of + * networking that is consistent (that is: they may return a past or a future state with + * respect to the event being processed by the callback). Instead, callers are advised + * to only use the arguments of the callbacks, possibly memorizing the specific bits of + * information they need to keep from one callback to another. + */ +@Deprecated +public class NetworkInfo implements Parcelable { + + /** + * Coarse-grained network state. This is probably what most applications should + * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}. + * The mapping between the two is as follows: + * <br/><br/> + * <table> + * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr> + * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>SCANNING</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>OBTAINING_IPADDR</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>VERIFYING_POOR_LINK</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>CAPTIVE_PORTAL_CHECK</code></td><td><code>CONNECTING</code></td></tr> + * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr> + * <tr><td><code>SUSPENDED</code></td><td><code>SUSPENDED</code></td></tr> + * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr> + * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr> + * <tr><td><code>BLOCKED</code></td><td><code>DISCONNECTED</code></td></tr> + * </table> + * + * @deprecated See {@link NetworkInfo}. + */ + @Deprecated + public enum State { + CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN + } + + /** + * The fine-grained state of a network connection. This level of detail + * is probably of interest to few applications. Most should use + * {@link android.net.NetworkInfo.State State} instead. + * + * @deprecated See {@link NetworkInfo}. + */ + @Deprecated + public enum DetailedState { + /** Ready to start data connection setup. */ + IDLE, + /** Searching for an available access point. */ + SCANNING, + /** Currently setting up data connection. */ + CONNECTING, + /** Network link established, performing authentication. */ + AUTHENTICATING, + /** Awaiting response from DHCP server in order to assign IP address information. */ + OBTAINING_IPADDR, + /** IP traffic should be available. */ + CONNECTED, + /** IP traffic is suspended */ + SUSPENDED, + /** Currently tearing down data connection. */ + DISCONNECTING, + /** IP traffic not available. */ + DISCONNECTED, + /** Attempt to connect failed. */ + FAILED, + /** Access to this network is blocked. */ + BLOCKED, + /** Link has poor connectivity. */ + VERIFYING_POOR_LINK, + /** Checking if network is a captive portal */ + CAPTIVE_PORTAL_CHECK + } + + /** + * This is the map described in the Javadoc comment above. The positions + * of the elements of the array must correspond to the ordinal values + * of <code>DetailedState</code>. + */ + private static final EnumMap<DetailedState, State> stateMap = + new EnumMap<DetailedState, State>(DetailedState.class); + + static { + stateMap.put(DetailedState.IDLE, State.DISCONNECTED); + stateMap.put(DetailedState.SCANNING, State.DISCONNECTED); + stateMap.put(DetailedState.CONNECTING, State.CONNECTING); + stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING); + stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING); + stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING); + stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING); + stateMap.put(DetailedState.CONNECTED, State.CONNECTED); + stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED); + stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING); + stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED); + stateMap.put(DetailedState.FAILED, State.DISCONNECTED); + stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED); + } + + private int mNetworkType; + private int mSubtype; + private String mTypeName; + private String mSubtypeName; + @NonNull + private State mState; + @NonNull + private DetailedState mDetailedState; + private String mReason; + private String mExtraInfo; + private boolean mIsFailover; + private boolean mIsAvailable; + private boolean mIsRoaming; + + /** + * Create a new instance of NetworkInfo. + * + * This may be useful for apps to write unit tests. + * + * @param type the legacy type of the network, as one of the ConnectivityManager.TYPE_* + * constants. + * @param subtype the subtype if applicable, as one of the TelephonyManager.NETWORK_TYPE_* + * constants. + * @param typeName a human-readable string for the network type, or an empty string or null. + * @param subtypeName a human-readable string for the subtype, or an empty string or null. + */ + public NetworkInfo(int type, int subtype, + @Nullable String typeName, @Nullable String subtypeName) { + if (!ConnectivityManager.isNetworkTypeValid(type) + && type != ConnectivityManager.TYPE_NONE) { + throw new IllegalArgumentException("Invalid network type: " + type); + } + mNetworkType = type; + mSubtype = subtype; + mTypeName = typeName; + mSubtypeName = subtypeName; + setDetailedState(DetailedState.IDLE, null, null); + mState = State.UNKNOWN; + } + + /** {@hide} */ + @UnsupportedAppUsage + public NetworkInfo(NetworkInfo source) { + if (source != null) { + synchronized (source) { + mNetworkType = source.mNetworkType; + mSubtype = source.mSubtype; + mTypeName = source.mTypeName; + mSubtypeName = source.mSubtypeName; + mState = source.mState; + mDetailedState = source.mDetailedState; + mReason = source.mReason; + mExtraInfo = source.mExtraInfo; + mIsFailover = source.mIsFailover; + mIsAvailable = source.mIsAvailable; + mIsRoaming = source.mIsRoaming; + } + } + } + + /** + * Reports the type of network to which the + * info in this {@code NetworkInfo} pertains. + * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link + * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link + * ConnectivityManager#TYPE_ETHERNET}, {@link ConnectivityManager#TYPE_BLUETOOTH}, or other + * types defined by {@link ConnectivityManager}. + * @deprecated Callers should switch to checking {@link NetworkCapabilities#hasTransport} + * instead with one of the NetworkCapabilities#TRANSPORT_* constants : + * {@link #getType} and {@link #getTypeName} cannot account for networks using + * multiple transports. Note that generally apps should not care about transport; + * {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED} and + * {@link NetworkCapabilities#getLinkDownstreamBandwidthKbps} are calls that + * apps concerned with meteredness or bandwidth should be looking at, as they + * offer this information with much better accuracy. + */ + @Deprecated + public int getType() { + synchronized (this) { + return mNetworkType; + } + } + + /** + * @deprecated Use {@link NetworkCapabilities} instead + * @hide + */ + @Deprecated + public void setType(int type) { + synchronized (this) { + mNetworkType = type; + } + } + + /** + * Return a network-type-specific integer describing the subtype + * of the network. + * @return the network subtype + * @deprecated Use {@link android.telephony.TelephonyManager#getDataNetworkType} instead. + */ + @Deprecated + public int getSubtype() { + synchronized (this) { + return mSubtype; + } + } + + /** + * @hide + */ + @UnsupportedAppUsage + public void setSubtype(int subtype, String subtypeName) { + synchronized (this) { + mSubtype = subtype; + mSubtypeName = subtypeName; + } + } + + /** + * Return a human-readable name describe the type of the network, + * for example "WIFI" or "MOBILE". + * @return the name of the network type + * @deprecated Callers should switch to checking {@link NetworkCapabilities#hasTransport} + * instead with one of the NetworkCapabilities#TRANSPORT_* constants : + * {@link #getType} and {@link #getTypeName} cannot account for networks using + * multiple transports. Note that generally apps should not care about transport; + * {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED} and + * {@link NetworkCapabilities#getLinkDownstreamBandwidthKbps} are calls that + * apps concerned with meteredness or bandwidth should be looking at, as they + * offer this information with much better accuracy. + */ + @Deprecated + public String getTypeName() { + synchronized (this) { + return mTypeName; + } + } + + /** + * Return a human-readable name describing the subtype of the network. + * @return the name of the network subtype + * @deprecated Use {@link android.telephony.TelephonyManager#getDataNetworkType} instead. + */ + @Deprecated + public String getSubtypeName() { + synchronized (this) { + return mSubtypeName; + } + } + + /** + * Indicates whether network connectivity exists or is in the process + * of being established. This is good for applications that need to + * do anything related to the network other than read or write data. + * For the latter, call {@link #isConnected()} instead, which guarantees + * that the network is fully usable. + * @return {@code true} if network connectivity exists or is in the process + * of being established, {@code false} otherwise. + * @deprecated Apps should instead use the + * {@link android.net.ConnectivityManager.NetworkCallback} API to + * learn about connectivity changes. + * {@link ConnectivityManager#registerDefaultNetworkCallback} and + * {@link ConnectivityManager#registerNetworkCallback}. These will + * give a more accurate picture of the connectivity state of + * the device and let apps react more easily and quickly to changes. + */ + @Deprecated + public boolean isConnectedOrConnecting() { + synchronized (this) { + return mState == State.CONNECTED || mState == State.CONNECTING; + } + } + + /** + * Indicates whether network connectivity exists and it is possible to establish + * connections and pass data. + * <p>Always call this before attempting to perform data transactions. + * @return {@code true} if network connectivity exists, {@code false} otherwise. + * @deprecated Apps should instead use the + * {@link android.net.ConnectivityManager.NetworkCallback} API to + * learn about connectivity changes. See + * {@link ConnectivityManager#registerDefaultNetworkCallback} and + * {@link ConnectivityManager#registerNetworkCallback}. These will + * give a more accurate picture of the connectivity state of + * the device and let apps react more easily and quickly to changes. + */ + @Deprecated + public boolean isConnected() { + synchronized (this) { + return mState == State.CONNECTED; + } + } + + /** + * Indicates whether network connectivity is possible. A network is unavailable + * when a persistent or semi-persistent condition prevents the possibility + * of connecting to that network. Examples include + * <ul> + * <li>The device is out of the coverage area for any network of this type.</li> + * <li>The device is on a network other than the home network (i.e., roaming), and + * data roaming has been disabled.</li> + * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li> + * </ul> + * Since Android L, this always returns {@code true}, because the system only + * returns info for available networks. + * @return {@code true} if the network is available, {@code false} otherwise + * @deprecated Apps should instead use the + * {@link android.net.ConnectivityManager.NetworkCallback} API to + * learn about connectivity changes. + * {@link ConnectivityManager#registerDefaultNetworkCallback} and + * {@link ConnectivityManager#registerNetworkCallback}. These will + * give a more accurate picture of the connectivity state of + * the device and let apps react more easily and quickly to changes. + */ + @Deprecated + public boolean isAvailable() { + synchronized (this) { + return mIsAvailable; + } + } + + /** + * Sets if the network is available, ie, if the connectivity is possible. + * @param isAvailable the new availability value. + * @deprecated Use {@link NetworkCapabilities} instead + * + * @hide + */ + @Deprecated + @UnsupportedAppUsage + public void setIsAvailable(boolean isAvailable) { + synchronized (this) { + mIsAvailable = isAvailable; + } + } + + /** + * Indicates whether the current attempt to connect to the network + * resulted from the ConnectivityManager trying to fail over to this + * network following a disconnect from another network. + * @return {@code true} if this is a failover attempt, {@code false} + * otherwise. + * @deprecated This field is not populated in recent Android releases, + * and does not make a lot of sense in a multi-network world. + */ + @Deprecated + public boolean isFailover() { + synchronized (this) { + return mIsFailover; + } + } + + /** + * Set the failover boolean. + * @param isFailover {@code true} to mark the current connection attempt + * as a failover. + * @deprecated This hasn't been set in any recent Android release. + * @hide + */ + @Deprecated + @UnsupportedAppUsage + public void setFailover(boolean isFailover) { + synchronized (this) { + mIsFailover = isFailover; + } + } + + /** + * Indicates whether the device is currently roaming on this network. When + * {@code true}, it suggests that use of data on this network may incur + * extra costs. + * + * @return {@code true} if roaming is in effect, {@code false} otherwise. + * @deprecated Callers should switch to checking + * {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING} + * instead, since that handles more complex situations, such as + * VPNs. + */ + @Deprecated + public boolean isRoaming() { + synchronized (this) { + return mIsRoaming; + } + } + + /** + * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING} instead. + * {@hide} + */ + @VisibleForTesting + @Deprecated + @UnsupportedAppUsage + public void setRoaming(boolean isRoaming) { + synchronized (this) { + mIsRoaming = isRoaming; + } + } + + /** + * Reports the current coarse-grained state of the network. + * @return the coarse-grained state + * @deprecated Apps should instead use the + * {@link android.net.ConnectivityManager.NetworkCallback} API to + * learn about connectivity changes. + * {@link ConnectivityManager#registerDefaultNetworkCallback} and + * {@link ConnectivityManager#registerNetworkCallback}. These will + * give a more accurate picture of the connectivity state of + * the device and let apps react more easily and quickly to changes. + */ + @Deprecated + public State getState() { + synchronized (this) { + return mState; + } + } + + /** + * Reports the current fine-grained state of the network. + * @return the fine-grained state + * @deprecated Apps should instead use the + * {@link android.net.ConnectivityManager.NetworkCallback} API to + * learn about connectivity changes. See + * {@link ConnectivityManager#registerDefaultNetworkCallback} and + * {@link ConnectivityManager#registerNetworkCallback}. These will + * give a more accurate picture of the connectivity state of + * the device and let apps react more easily and quickly to changes. + */ + @Deprecated + public @NonNull DetailedState getDetailedState() { + synchronized (this) { + return mDetailedState; + } + } + + /** + * Sets the fine-grained state of the network. + * + * This is only useful for testing. + * + * @param detailedState the {@link DetailedState}. + * @param reason a {@code String} indicating the reason for the state change, + * if one was supplied. May be {@code null}. + * @param extraInfo an optional {@code String} providing addditional network state + * information passed up from the lower networking layers. + * @deprecated Use {@link NetworkCapabilities} instead. + */ + @Deprecated + public void setDetailedState(@NonNull DetailedState detailedState, @Nullable String reason, + @Nullable String extraInfo) { + synchronized (this) { + this.mDetailedState = detailedState; + this.mState = stateMap.get(detailedState); + this.mReason = reason; + this.mExtraInfo = extraInfo; + } + } + + /** + * Set the extraInfo field. + * @param extraInfo an optional {@code String} providing addditional network state + * information passed up from the lower networking layers. + * @deprecated See {@link NetworkInfo#getExtraInfo}. + * @hide + */ + @Deprecated + public void setExtraInfo(String extraInfo) { + synchronized (this) { + this.mExtraInfo = extraInfo; + } + } + + /** + * Report the reason an attempt to establish connectivity failed, + * if one is available. + * @return the reason for failure, or null if not available + * @deprecated This method does not have a consistent contract that could make it useful + * to callers. + */ + public String getReason() { + synchronized (this) { + return mReason; + } + } + + /** + * Report the extra information about the network state, if any was + * provided by the lower networking layers. + * @return the extra information, or null if not available + * @deprecated Use other services e.g. WifiManager to get additional information passed up from + * the lower networking layers. + */ + @Deprecated + public String getExtraInfo() { + synchronized (this) { + return mExtraInfo; + } + } + + @Override + public String toString() { + synchronized (this) { + final StringBuilder builder = new StringBuilder("["); + builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()). + append("], state: ").append(mState).append("/").append(mDetailedState). + append(", reason: ").append(mReason == null ? "(unspecified)" : mReason). + append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo). + append(", failover: ").append(mIsFailover). + append(", available: ").append(mIsAvailable). + append(", roaming: ").append(mIsRoaming). + append("]"); + return builder.toString(); + } + } + + /** + * Returns a brief summary string suitable for debugging. + * @hide + */ + public String toShortString() { + synchronized (this) { + final StringBuilder builder = new StringBuilder(); + builder.append(getTypeName()); + + final String subtype = getSubtypeName(); + if (!TextUtils.isEmpty(subtype)) { + builder.append("[").append(subtype).append("]"); + } + + builder.append(" "); + builder.append(mDetailedState); + if (mIsRoaming) { + builder.append(" ROAMING"); + } + if (mExtraInfo != null) { + builder.append(" extra: ").append(mExtraInfo); + } + return builder.toString(); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + synchronized (this) { + dest.writeInt(mNetworkType); + dest.writeInt(mSubtype); + dest.writeString(mTypeName); + dest.writeString(mSubtypeName); + dest.writeString(mState.name()); + dest.writeString(mDetailedState.name()); + dest.writeInt(mIsFailover ? 1 : 0); + dest.writeInt(mIsAvailable ? 1 : 0); + dest.writeInt(mIsRoaming ? 1 : 0); + dest.writeString(mReason); + dest.writeString(mExtraInfo); + } + } + + public static final @android.annotation.NonNull Creator<NetworkInfo> CREATOR = new Creator<NetworkInfo>() { + @Override + public NetworkInfo createFromParcel(Parcel in) { + int netType = in.readInt(); + int subtype = in.readInt(); + String typeName = in.readString(); + String subtypeName = in.readString(); + NetworkInfo netInfo = new NetworkInfo(netType, subtype, typeName, subtypeName); + netInfo.mState = State.valueOf(in.readString()); + netInfo.mDetailedState = DetailedState.valueOf(in.readString()); + netInfo.mIsFailover = in.readInt() != 0; + netInfo.mIsAvailable = in.readInt() != 0; + netInfo.mIsRoaming = in.readInt() != 0; + netInfo.mReason = in.readString(); + netInfo.mExtraInfo = in.readString(); + return netInfo; + } + + @Override + public NetworkInfo[] newArray(int size) { + return new NetworkInfo[size]; + } + }; +} diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..0665af58498c0af2c6177ae6cd0f0e39916b3ab0 --- /dev/null +++ b/framework/src/android/net/NetworkProvider.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.concurrent.Executor; + +/** + * Base class for network providers such as telephony or Wi-Fi. NetworkProviders connect the device + * to networks and makes them available to the core network stack by creating + * {@link NetworkAgent}s. The networks can then provide connectivity to apps and can be interacted + * with via networking APIs such as {@link ConnectivityManager}. + * + * Subclasses should implement {@link #onNetworkRequested} and {@link #onNetworkRequestWithdrawn} + * to receive {@link NetworkRequest}s sent by the system and by apps. A network that is not the + * best (highest-scoring) network for any request is generally not used by the system, and torn + * down. + * + * @hide + */ +@SystemApi +public class NetworkProvider { + /** + * {@code providerId} value that indicates the absence of a provider. It is the providerId of + * any NetworkProvider that is not currently registered, and of any NetworkRequest that is not + * currently being satisfied by a network. + */ + public static final int ID_NONE = -1; + + /** + * The first providerId value that will be allocated. + * @hide only used by ConnectivityService. + */ + public static final int FIRST_PROVIDER_ID = 1; + + /** @hide only used by ConnectivityService */ + public static final int CMD_REQUEST_NETWORK = 1; + /** @hide only used by ConnectivityService */ + public static final int CMD_CANCEL_REQUEST = 2; + + private final Messenger mMessenger; + private final String mName; + private final Context mContext; + + private int mProviderId = ID_NONE; + + /** + * Constructs a new NetworkProvider. + * + * @param looper the Looper on which to run {@link #onNetworkRequested} and + * {@link #onNetworkRequestWithdrawn}. + * @param name the name of the listener, used only for debugging. + * + * @hide + */ + @SystemApi + public NetworkProvider(@NonNull Context context, @NonNull Looper looper, @NonNull String name) { + // TODO (b/174636568) : this class should be able to cache an instance of + // ConnectivityManager so it doesn't have to fetch it again every time. + final Handler handler = new Handler(looper) { + @Override + public void handleMessage(Message m) { + switch (m.what) { + case CMD_REQUEST_NETWORK: + onNetworkRequested((NetworkRequest) m.obj, m.arg1, m.arg2); + break; + case CMD_CANCEL_REQUEST: + onNetworkRequestWithdrawn((NetworkRequest) m.obj); + break; + default: + Log.e(mName, "Unhandled message: " + m.what); + } + } + }; + mContext = context; + mMessenger = new Messenger(handler); + mName = name; + } + + // TODO: consider adding a register() method so ConnectivityManager does not need to call this. + /** @hide */ + public @Nullable Messenger getMessenger() { + return mMessenger; + } + + /** @hide */ + public @NonNull String getName() { + return mName; + } + + /** + * Returns the ID of this provider. This is known only once the provider is registered via + * {@link ConnectivityManager#registerNetworkProvider()}, otherwise the ID is {@link #ID_NONE}. + * This ID must be used when registering any {@link NetworkAgent}s. + */ + public int getProviderId() { + return mProviderId; + } + + /** @hide */ + public void setProviderId(int providerId) { + mProviderId = providerId; + } + + /** + * Called when a NetworkRequest is received. The request may be a new request or an existing + * request with a different score. + * + * @param request the NetworkRequest being received + * @param score the score of the network currently satisfying the request, or 0 if none. + * @param providerId the ID of the provider that created the network currently satisfying this + * request, or {@link #ID_NONE} if none. + * + * @hide + */ + @SystemApi + public void onNetworkRequested(@NonNull NetworkRequest request, + @IntRange(from = 0, to = 99) int score, int providerId) {} + + /** + * Called when a NetworkRequest is withdrawn. + * @hide + */ + @SystemApi + public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {} + + /** + * Asserts that no provider will ever be able to satisfy the specified request. The provider + * must only call this method if it knows that it is the only provider on the system capable of + * satisfying this request, and that the request cannot be satisfied. The application filing the + * request will receive an {@link NetworkCallback#onUnavailable()} callback. + * + * @param request the request that permanently cannot be fulfilled + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public void declareNetworkRequestUnfulfillable(@NonNull NetworkRequest request) { + ConnectivityManager.from(mContext).declareNetworkRequestUnfulfillable(request); + } + + /** + * A callback for parties registering a NetworkOffer. + * + * This is used with {@link ConnectivityManager#offerNetwork}. When offering a network, + * the system will use this callback to inform the caller that a network corresponding to + * this offer is needed or unneeded. + * + * @hide + */ + @SystemApi + public interface NetworkOfferCallback { + /** + * Called by the system when a network for this offer is needed to satisfy some + * networking request. + */ + void onNetworkNeeded(@NonNull NetworkRequest request); + /** + * Called by the system when this offer is no longer valuable for this request. + */ + void onNetworkUnneeded(@NonNull NetworkRequest request); + } + + private class NetworkOfferCallbackProxy extends INetworkOfferCallback.Stub { + @NonNull public final NetworkOfferCallback callback; + @NonNull private final Executor mExecutor; + + NetworkOfferCallbackProxy(@NonNull final NetworkOfferCallback callback, + @NonNull final Executor executor) { + this.callback = callback; + this.mExecutor = executor; + } + + @Override + public void onNetworkNeeded(final @NonNull NetworkRequest request) { + mExecutor.execute(() -> callback.onNetworkNeeded(request)); + } + + @Override + public void onNetworkUnneeded(final @NonNull NetworkRequest request) { + mExecutor.execute(() -> callback.onNetworkUnneeded(request)); + } + } + + @GuardedBy("mProxies") + @NonNull private final ArrayList<NetworkOfferCallbackProxy> mProxies = new ArrayList<>(); + + // Returns the proxy associated with this callback, or null if none. + @Nullable + private NetworkOfferCallbackProxy findProxyForCallback(@NonNull final NetworkOfferCallback cb) { + synchronized (mProxies) { + for (final NetworkOfferCallbackProxy p : mProxies) { + if (p.callback == cb) return p; + } + } + return null; + } + + /** + * Register or update an offer for network with the passed capabilities and score. + * + * A NetworkProvider's role is to provide networks. This method is how a provider tells the + * connectivity stack what kind of network it may provide. The score and caps arguments act + * as filters that the connectivity stack uses to tell when the offer is valuable. When an + * offer might be preferred over existing networks, the provider will receive a call to + * the associated callback's {@link NetworkOfferCallback#onNetworkNeeded} method. The provider + * should then try to bring up this network. When an offer is no longer useful, the stack + * will inform the provider by calling {@link NetworkOfferCallback#onNetworkUnneeded}. The + * provider should stop trying to bring up such a network, or disconnect it if it already has + * one. + * + * The stack determines what offers are valuable according to what networks are currently + * available to the system, and what networking requests are made by applications. If an + * offer looks like it could connect a better network than any existing network for any + * particular request, that's when the stack decides the network is needed. If the current + * networking requests are all satisfied by networks that this offer couldn't possibly be a + * better match for, that's when the offer is no longer valuable. An offer starts out as + * unneeded ; the provider should not try to bring up the network until + * {@link NetworkOfferCallback#onNetworkNeeded} is called. + * + * Note that the offers are non-binding to the providers, in particular because providers + * often don't know if they will be able to bring up such a network at any given time. For + * example, no wireless network may be in range when the offer would be valuable. This is fine + * and expected ; the provider should simply continue to try to bring up the network and do so + * if/when it becomes possible. In the mean time, the stack will continue to satisfy requests + * with the best network currently available, or if none, keep the apps informed that no + * network can currently satisfy this request. When/if the provider can bring up the network, + * the connectivity stack will match it against requests, and inform interested apps of the + * availability of this network. This may, in turn, render the offer of some other provider + * low-value if all requests it used to satisfy are now better served by this network. + * + * A network can become unneeded for a reason like the above : whether the provider managed + * to bring up the offered network after it became needed or not, some other provider may + * bring up a better network than this one, making this network unneeded. A network may also + * become unneeded if the application making the request withdrew it (for example, after it + * is done transferring data, or if the user canceled an operation). + * + * The capabilities and score act as filters as to what requests the provider will see. + * They are not promises, but for best performance, the providers should strive to put + * as much known information as possible in the offer. For the score, it should put as + * strong a score as the networks will have, since this will filter what requests the + * provider sees – it's not a promise, it only serves to avoid sending requests that + * the provider can't ever hope to satisfy better than any current network. For capabilities, + * it should put all NetworkAgent-managed capabilities a network may have, even if it doesn't + * have them at first. This applies to INTERNET, for example ; if a provider thinks the + * network it can bring up for this offer may offer Internet access it should include the + * INTERNET bit. It's fine if the brought up network ends up not actually having INTERNET. + * + * TODO : in the future, to avoid possible infinite loops, there should be constraints on + * what can be put in capabilities of networks brought up for an offer. If a provider might + * bring up a network with or without INTERNET, then it should file two offers : this will + * let it know precisely what networks are needed, so it can avoid bringing up networks that + * won't actually satisfy requests and remove the risk for bring-up-bring-down loops. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public void registerNetworkOffer(@NonNull final NetworkScore score, + @NonNull final NetworkCapabilities caps, @NonNull final Executor executor, + @NonNull final NetworkOfferCallback callback) { + // Can't offer a network with a provider that is not yet registered or already unregistered. + final int providerId = mProviderId; + if (providerId == ID_NONE) return; + NetworkOfferCallbackProxy proxy = null; + synchronized (mProxies) { + for (final NetworkOfferCallbackProxy existingProxy : mProxies) { + if (existingProxy.callback == callback) { + proxy = existingProxy; + break; + } + } + if (null == proxy) { + proxy = new NetworkOfferCallbackProxy(callback, executor); + mProxies.add(proxy); + } + } + mContext.getSystemService(ConnectivityManager.class) + .offerNetwork(providerId, score, caps, proxy); + } + + /** + * Withdraw a network offer previously made to the networking stack. + * + * If a provider can no longer provide a network they offered, it should call this method. + * An example of usage could be if the hardware necessary to bring up the network was turned + * off in UI by the user. Note that because offers are never binding, the provider might + * alternatively decide not to withdraw this offer and simply refuse to bring up the network + * even when it's needed. However, withdrawing the request is slightly more resource-efficient + * because the networking stack won't have to compare this offer to exiting networks to see + * if it could beat any of them, and may be advantageous to the provider's implementation that + * can rely on no longer receiving callbacks for a network that they can't bring up anyways. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public void unregisterNetworkOffer(final @NonNull NetworkOfferCallback callback) { + final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback); + if (null == proxy) return; + mProxies.remove(proxy); + mContext.getSystemService(ConnectivityManager.class).unofferNetwork(proxy); + } +} diff --git a/framework/src/android/net/NetworkReleasedException.java b/framework/src/android/net/NetworkReleasedException.java new file mode 100644 index 0000000000000000000000000000000000000000..0629b7563aea8b29d8dcd6f39742e26f1bcd9257 --- /dev/null +++ b/framework/src/android/net/NetworkReleasedException.java @@ -0,0 +1,32 @@ +/* + * 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.SystemApi; + +/** + * Indicates that the {@link Network} was released and is no longer available. + * + * @hide + */ +@SystemApi +public class NetworkReleasedException extends Exception { + /** @hide */ + public NetworkReleasedException() { + super("The network was released and is no longer available"); + } +} diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..afc76d664af246a6cbe9709328965e485d2b0f89 --- /dev/null +++ b/framework/src/android/net/NetworkRequest.java @@ -0,0 +1,753 @@ +/* + * 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 android.net; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.net.NetworkCapabilities.NetCapability; +import android.net.NetworkCapabilities.Transport; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.text.TextUtils; +import android.util.Range; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Defines a request for a network, made through {@link NetworkRequest.Builder} and used + * to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes + * via {@link ConnectivityManager#registerNetworkCallback}. + */ +public class NetworkRequest implements Parcelable { + /** + * The first requestId value that will be allocated. + * @hide only used by ConnectivityService. + */ + public static final int FIRST_REQUEST_ID = 1; + + /** + * The requestId value that represents the absence of a request. + * @hide only used by ConnectivityService. + */ + public static final int REQUEST_ID_NONE = -1; + + /** + * The {@link NetworkCapabilities} that define this request. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public final @NonNull NetworkCapabilities networkCapabilities; + + /** + * Identifies the request. NetworkRequests should only be constructed by + * the Framework and given out to applications as tokens to be used to identify + * the request. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public final int requestId; + + /** + * Set for legacy requests and the default. Set to TYPE_NONE for none. + * Causes CONNECTIVITY_ACTION broadcasts to be sent. + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public final int legacyType; + + /** + * A NetworkRequest as used by the system can be one of the following types: + * + * - LISTEN, for which the framework will issue callbacks about any + * and all networks that match the specified NetworkCapabilities, + * + * - REQUEST, capable of causing a specific network to be created + * first (e.g. a telephony DUN request), the framework will issue + * callbacks about the single, highest scoring current network + * (if any) that matches the specified NetworkCapabilities, or + * + * - TRACK_DEFAULT, which causes the framework to issue callbacks for + * the single, highest scoring current network (if any) that will + * be chosen for an app, but which cannot cause the framework to + * either create or retain the existence of any specific network. + * + * - TRACK_SYSTEM_DEFAULT, which causes the framework to send callbacks + * for the network (if any) that satisfies the default Internet + * request. + * + * - TRACK_BEST, which causes the framework to send callbacks about + * the single, highest scoring current network (if any) that matches + * the specified NetworkCapabilities. + * + * - BACKGROUND_REQUEST, like REQUEST but does not cause any networks + * to retain the NET_CAPABILITY_FOREGROUND capability. A network with + * no foreground requests is in the background. A network that has + * one or more background requests and loses its last foreground + * request to a higher-scoring network will not go into the + * background immediately, but will linger and go into the background + * after the linger timeout. + * + * - The value NONE is used only by applications. When an application + * creates a NetworkRequest, it does not have a type; the type is set + * by the system depending on the method used to file the request + * (requestNetwork, registerNetworkCallback, etc.). + * + * @hide + */ + public static enum Type { + NONE, + LISTEN, + TRACK_DEFAULT, + REQUEST, + BACKGROUND_REQUEST, + TRACK_SYSTEM_DEFAULT, + LISTEN_FOR_BEST, + }; + + /** + * The type of the request. This is only used by the system and is always NONE elsewhere. + * + * @hide + */ + public final Type type; + + /** + * @hide + */ + public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId, Type type) { + if (nc == null) { + throw new NullPointerException(); + } + requestId = rId; + networkCapabilities = nc; + this.legacyType = legacyType; + this.type = type; + } + + /** + * @hide + */ + public NetworkRequest(NetworkRequest that) { + networkCapabilities = new NetworkCapabilities(that.networkCapabilities); + requestId = that.requestId; + this.legacyType = that.legacyType; + this.type = that.type; + } + + /** + * Builder used to create {@link NetworkRequest} objects. Specify the Network features + * needed in terms of {@link NetworkCapabilities} features + */ + public static class Builder { + /** + * Capabilities that are currently compatible with VCN networks. + */ + private static final List<Integer> VCN_SUPPORTED_CAPABILITIES = Arrays.asList( + NET_CAPABILITY_CAPTIVE_PORTAL, + NET_CAPABILITY_DUN, + NET_CAPABILITY_FOREGROUND, + NET_CAPABILITY_INTERNET, + NET_CAPABILITY_NOT_CONGESTED, + NET_CAPABILITY_NOT_METERED, + NET_CAPABILITY_NOT_RESTRICTED, + NET_CAPABILITY_NOT_ROAMING, + NET_CAPABILITY_NOT_SUSPENDED, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_PARTIAL_CONNECTIVITY, + NET_CAPABILITY_TEMPORARILY_NOT_METERED, + NET_CAPABILITY_TRUSTED, + NET_CAPABILITY_VALIDATED); + + private final NetworkCapabilities mNetworkCapabilities; + + // A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when + // the NetworkRequest object is built. + private boolean mShouldDeduceNotVcnManaged = true; + + /** + * Default constructor for Builder. + */ + public Builder() { + // By default, restrict this request to networks available to this app. + // Apps can rescind this restriction, but ConnectivityService will enforce + // it for apps that do not have the NETWORK_SETTINGS permission. + mNetworkCapabilities = new NetworkCapabilities(); + mNetworkCapabilities.setSingleUid(Process.myUid()); + } + + /** + * Creates a new Builder of NetworkRequest from an existing instance. + */ + public Builder(@NonNull final NetworkRequest request) { + Objects.requireNonNull(request); + mNetworkCapabilities = request.networkCapabilities; + // If the caller constructed the builder from a request, it means the user + // might explicitly want the capabilities from the request. Thus, the NOT_VCN_MANAGED + // capabilities should not be touched later. + mShouldDeduceNotVcnManaged = false; + } + + /** + * Build {@link NetworkRequest} give the current set of capabilities. + */ + public NetworkRequest build() { + // Make a copy of mNetworkCapabilities so we don't inadvertently remove NOT_RESTRICTED + // when later an unrestricted capability could be added to mNetworkCapabilities, in + // which case NOT_RESTRICTED should be returned to mNetworkCapabilities, which + // maybeMarkCapabilitiesRestricted() doesn't add back. + final NetworkCapabilities nc = new NetworkCapabilities(mNetworkCapabilities); + nc.maybeMarkCapabilitiesRestricted(); + deduceNotVcnManagedCapability(nc); + return new NetworkRequest(nc, ConnectivityManager.TYPE_NONE, + ConnectivityManager.REQUEST_ID_UNSET, Type.NONE); + } + + /** + * Add the given capability requirement to this builder. These represent + * the requested network's required capabilities. Note that when searching + * for a network to satisfy a request, all capabilities requested must be + * satisfied. + * + * @param capability The capability to add. + * @return The builder to facilitate chaining + * {@code builder.addCapability(...).addCapability();}. + */ + public Builder addCapability(@NetworkCapabilities.NetCapability int capability) { + mNetworkCapabilities.addCapability(capability); + if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) { + mShouldDeduceNotVcnManaged = false; + } + return this; + } + + /** + * Removes (if found) the given capability from this builder instance. + * + * @param capability The capability to remove. + * @return The builder to facilitate chaining. + */ + public Builder removeCapability(@NetworkCapabilities.NetCapability int capability) { + mNetworkCapabilities.removeCapability(capability); + if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) { + mShouldDeduceNotVcnManaged = false; + } + return this; + } + + /** + * Set the {@code NetworkCapabilities} for this builder instance, + * overriding any capabilities that had been previously set. + * + * @param nc The superseding {@code NetworkCapabilities} instance. + * @return The builder to facilitate chaining. + * @hide + */ + public Builder setCapabilities(NetworkCapabilities nc) { + mNetworkCapabilities.set(nc); + return this; + } + + /** + * Sets this request to match only networks that apply to the specified UIDs. + * + * By default, the set of UIDs is the UID of the calling app, and this request will match + * any network that applies to the app. Setting it to {@code null} will observe any + * network on the system, even if it does not apply to this app. In this case, any + * {@link NetworkSpecifier} set on this request will be redacted or removed to prevent the + * application deducing restricted information such as location. + * + * @param uids The UIDs as a set of {@code Range<Integer>}, or null for everything. + * @return The builder to facilitate chaining. + * @hide + */ + @NonNull + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setUids(@Nullable Set<Range<Integer>> uids) { + mNetworkCapabilities.setUids(uids); + return this; + } + + /** + * Add a capability that must not exist in the requested network. + * <p> + * If the capability was previously added to the list of required capabilities (for + * example, it was there by default or added using {@link #addCapability(int)} method), then + * it will be removed from the list of required capabilities as well. + * + * @see #addCapability(int) + * + * @param capability The capability to add to forbidden capability list. + * @return The builder to facilitate chaining. + * + * @hide + */ + @NonNull + @SuppressLint("MissingGetterMatchingBuilder") + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) { + mNetworkCapabilities.addForbiddenCapability(capability); + return this; + } + + /** + * Removes (if found) the given forbidden capability from this builder instance. + * + * @param capability The forbidden capability to remove. + * @return The builder to facilitate chaining. + * + * @hide + */ + @NonNull + @SuppressLint("BuilderSetStyle") + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public Builder removeForbiddenCapability( + @NetworkCapabilities.NetCapability int capability) { + mNetworkCapabilities.removeForbiddenCapability(capability); + return this; + } + + /** + * Completely clears all the {@code NetworkCapabilities} from this builder instance, + * removing even the capabilities that are set by default when the object is constructed. + * + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder clearCapabilities() { + mNetworkCapabilities.clearAll(); + // If the caller explicitly clear all capabilities, the NOT_VCN_MANAGED capabilities + // should not be add back later. + mShouldDeduceNotVcnManaged = false; + return this; + } + + /** + * Adds the given transport requirement to this builder. These represent + * the set of allowed transports for the request. Only networks using one + * of these transports will satisfy the request. If no particular transports + * are required, none should be specified here. + * + * @param transportType The transport type to add. + * @return The builder to facilitate chaining. + */ + public Builder addTransportType(@NetworkCapabilities.Transport int transportType) { + mNetworkCapabilities.addTransportType(transportType); + return this; + } + + /** + * Removes (if found) the given transport from this builder instance. + * + * @param transportType The transport type to remove. + * @return The builder to facilitate chaining. + */ + public Builder removeTransportType(@NetworkCapabilities.Transport int transportType) { + mNetworkCapabilities.removeTransportType(transportType); + return this; + } + + /** + * @hide + */ + public Builder setLinkUpstreamBandwidthKbps(int upKbps) { + mNetworkCapabilities.setLinkUpstreamBandwidthKbps(upKbps); + return this; + } + /** + * @hide + */ + public Builder setLinkDownstreamBandwidthKbps(int downKbps) { + mNetworkCapabilities.setLinkDownstreamBandwidthKbps(downKbps); + return this; + } + + /** + * Sets the optional bearer specific network specifier. + * This has no meaning if a single transport is also not specified, so calling + * this without a single transport set will generate an exception, as will + * subsequently adding or removing transports after this is set. + * </p> + * If the {@code networkSpecifier} is provided, it shall be interpreted as follows: + * <ul> + * <li>If the specifier can be parsed as an integer, it will be treated as a + * {@link android.net TelephonyNetworkSpecifier}, and the provided integer will be + * interpreted as a SubscriptionId. + * <li>If the value is an ethernet interface name, it will be treated as such. + * <li>For all other cases, the behavior is undefined. + * </ul> + * + * @param networkSpecifier A {@code String} of either a SubscriptionId in cellular + * network request or an ethernet interface name in ethernet + * network request. + * + * @deprecated Use {@link #setNetworkSpecifier(NetworkSpecifier)} instead. + */ + @Deprecated + public Builder setNetworkSpecifier(String networkSpecifier) { + try { + int subId = Integer.parseInt(networkSpecifier); + return setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(subId).build()); + } catch (NumberFormatException nfe) { + // An EthernetNetworkSpecifier or TestNetworkSpecifier does not accept null or empty + // ("") strings. When network specifiers were strings a null string and an empty + // string were considered equivalent. Hence no meaning is attached to a null or + // empty ("") string. + if (TextUtils.isEmpty(networkSpecifier)) { + return setNetworkSpecifier((NetworkSpecifier) null); + } else if (mNetworkCapabilities.hasTransport(TRANSPORT_TEST)) { + return setNetworkSpecifier(new TestNetworkSpecifier(networkSpecifier)); + } else { + return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier)); + } + } + } + + /** + * Sets the optional bearer specific network specifier. + * This has no meaning if a single transport is also not specified, so calling + * this without a single transport set will generate an exception, as will + * subsequently adding or removing transports after this is set. + * </p> + * + * @param networkSpecifier A concrete, parcelable framework class that extends + * NetworkSpecifier. + */ + public Builder setNetworkSpecifier(NetworkSpecifier networkSpecifier) { + if (networkSpecifier instanceof MatchAllNetworkSpecifier) { + throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted"); + } + mNetworkCapabilities.setNetworkSpecifier(networkSpecifier); + // Do not touch NOT_VCN_MANAGED if the caller needs to access to a very specific + // Network. + mShouldDeduceNotVcnManaged = false; + return this; + } + + /** + * Sets the signal strength. This is a signed integer, with higher values indicating a + * stronger signal. The exact units are bearer-dependent. For example, Wi-Fi uses the same + * RSSI units reported by WifiManager. + * <p> + * Note that when used to register a network callback, this specifies the minimum acceptable + * signal strength. When received as the state of an existing network it specifies the + * current value. A value of {@code SIGNAL_STRENGTH_UNSPECIFIED} means no value when + * received and has no effect when requesting a callback. + * + * <p>This method requires the caller to hold the + * {@link android.Manifest.permission#NETWORK_SIGNAL_STRENGTH_WAKEUP} permission + * + * @param signalStrength the bearer-specific signal strength. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP) + public @NonNull Builder setSignalStrength(int signalStrength) { + mNetworkCapabilities.setSignalStrength(signalStrength); + return this; + } + + /** + * Deduce the NET_CAPABILITY_NOT_VCN_MANAGED capability from other capabilities + * and user intention, which includes: + * 1. For the requests that don't have anything besides + * {@link #VCN_SUPPORTED_CAPABILITIES}, add the NET_CAPABILITY_NOT_VCN_MANAGED to + * allow the callers automatically utilize VCN networks if available. + * 2. For the requests that explicitly add or remove NET_CAPABILITY_NOT_VCN_MANAGED, + * or has clear intention of tracking specific network, + * do not alter them to allow user fire request that suits their need. + * + * @hide + */ + private void deduceNotVcnManagedCapability(final NetworkCapabilities nc) { + if (!mShouldDeduceNotVcnManaged) return; + for (final int cap : nc.getCapabilities()) { + if (!VCN_SUPPORTED_CAPABILITIES.contains(cap)) return; + } + nc.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + } + + /** + * Sets the optional subscription ID set. + * <p> + * This specify the subscription IDs requirement. + * A network will satisfy this request only if it matches one of the subIds in this set. + * An empty set matches all networks, including those without a subId. + * + * <p>Registering a NetworkRequest with a non-empty set of subIds requires the + * NETWORK_FACTORY permission. + * + * @param subIds A {@code Set} that represents subscription IDs. + * @hide + */ + @NonNull + @SystemApi + public Builder setSubscriptionIds(@NonNull Set<Integer> subIds) { + mNetworkCapabilities.setSubscriptionIds(subIds); + return this; + } + + /** + * Specifies whether the built request should also match networks that do not apply to the + * calling UID. + * + * By default, the built request will only match networks that apply to the calling UID. + * If this method is called with {@code true}, the built request will match any network on + * the system that matches the other parameters of the request. In this case, any + * information in the built request that is subject to redaction for security or privacy + * purposes, such as a {@link NetworkSpecifier}, will be redacted or removed to prevent the + * application deducing sensitive information. + * + * @param include Whether to match networks that do not apply to the calling UID. + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder setIncludeOtherUidNetworks(boolean include) { + if (include) { + mNetworkCapabilities.setUids(null); + } else { + mNetworkCapabilities.setSingleUid(Process.myUid()); + } + return this; + } + } + + // implement the Parcelable interface + public int describeContents() { + return 0; + } + public void writeToParcel(Parcel dest, int flags) { + networkCapabilities.writeToParcel(dest, flags); + dest.writeInt(legacyType); + dest.writeInt(requestId); + dest.writeString(type.name()); + } + + public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR = + new Creator<NetworkRequest>() { + public NetworkRequest createFromParcel(Parcel in) { + NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in); + int legacyType = in.readInt(); + int requestId = in.readInt(); + Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid. + NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type); + return result; + } + public NetworkRequest[] newArray(int size) { + return new NetworkRequest[size]; + } + }; + + /** + * Returns true iff. this NetworkRequest is of type LISTEN. + * + * @hide + */ + public boolean isListen() { + return type == Type.LISTEN; + } + + /** + * Returns true iff. this NetworkRequest is of type LISTEN_FOR_BEST. + * + * @hide + */ + public boolean isListenForBest() { + return type == Type.LISTEN_FOR_BEST; + } + + /** + * Returns true iff. the contained NetworkRequest is one that: + * + * - should be associated with at most one satisfying network + * at a time; + * + * - should cause a network to be kept up, but not necessarily in + * the foreground, if it is the best network which can satisfy the + * NetworkRequest. + * + * For full detail of how isRequest() is used for pairing Networks with + * NetworkRequests read rematchNetworkAndRequests(). + * + * @hide + */ + public boolean isRequest() { + return type == Type.REQUEST || type == Type.BACKGROUND_REQUEST; + } + + /** + * Returns true iff. this NetworkRequest is of type BACKGROUND_REQUEST. + * + * @hide + */ + public boolean isBackgroundRequest() { + return type == Type.BACKGROUND_REQUEST; + } + + /** + * @see Builder#addCapability(int) + */ + public boolean hasCapability(@NetCapability int capability) { + return networkCapabilities.hasCapability(capability); + } + + /** + * @see Builder#addForbiddenCapability(int) + * + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public boolean hasForbiddenCapability(@NetCapability int capability) { + return networkCapabilities.hasForbiddenCapability(capability); + } + + /** + * Returns true if and only if the capabilities requested in this NetworkRequest are satisfied + * by the provided {@link NetworkCapabilities}. + * + * @param nc Capabilities that should satisfy this NetworkRequest. null capabilities do not + * satisfy any request. + */ + public boolean canBeSatisfiedBy(@Nullable NetworkCapabilities nc) { + return networkCapabilities.satisfiedByNetworkCapabilities(nc); + } + + /** + * @see Builder#addTransportType(int) + */ + public boolean hasTransport(@Transport int transportType) { + return networkCapabilities.hasTransport(transportType); + } + + /** + * @see Builder#setNetworkSpecifier(NetworkSpecifier) + */ + @Nullable + public NetworkSpecifier getNetworkSpecifier() { + return networkCapabilities.getNetworkSpecifier(); + } + + /** + * @return the uid of the app making the request. + * + * Note: This could return {@link Process#INVALID_UID} if the {@link NetworkRequest} object was + * not obtained from {@link ConnectivityManager}. + * @hide + */ + @SystemApi + public int getRequestorUid() { + return networkCapabilities.getRequestorUid(); + } + + /** + * @return the package name of the app making the request. + * + * Note: This could return {@code null} if the {@link NetworkRequest} object was not obtained + * from {@link ConnectivityManager}. + * @hide + */ + @SystemApi + @Nullable + public String getRequestorPackageName() { + return networkCapabilities.getRequestorPackageName(); + } + + public String toString() { + return "NetworkRequest [ " + type + " id=" + requestId + + (legacyType != ConnectivityManager.TYPE_NONE ? ", legacyType=" + legacyType : "") + + ", " + networkCapabilities.toString() + " ]"; + } + + public boolean equals(@Nullable Object obj) { + if (obj instanceof NetworkRequest == false) return false; + NetworkRequest that = (NetworkRequest)obj; + return (that.legacyType == this.legacyType && + that.requestId == this.requestId && + that.type == this.type && + Objects.equals(that.networkCapabilities, this.networkCapabilities)); + } + + public int hashCode() { + return Objects.hash(requestId, legacyType, networkCapabilities, type); + } + + /** + * Gets all the capabilities set on this {@code NetworkRequest} instance. + * + * @return an array of capability values for this instance. + */ + @NonNull + public @NetCapability int[] getCapabilities() { + // No need to make a defensive copy here as NC#getCapabilities() already returns + // a new array. + return networkCapabilities.getCapabilities(); + } + + /** + * Gets all the forbidden capabilities set on this {@code NetworkRequest} instance. + * + * @return an array of forbidden capability values for this instance. + * + * @hide + */ + @NonNull + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public @NetCapability int[] getForbiddenCapabilities() { + // No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns + // a new array. + return networkCapabilities.getForbiddenCapabilities(); + } + + /** + * Gets all the transports set on this {@code NetworkRequest} instance. + * + * @return an array of transport type values for this instance. + */ + @NonNull + public @Transport int[] getTransportTypes() { + // No need to make a defensive copy here as NC#getTransportTypes() already returns + // a new array. + return networkCapabilities.getTransportTypes(); + } +} diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java new file mode 100644 index 0000000000000000000000000000000000000000..7be7deb999e2b3b0e17115e9baabd912d836ef3c --- /dev/null +++ b/framework/src/android/net/NetworkScore.java @@ -0,0 +1,328 @@ +/* + * 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.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Object representing the quality of a network as perceived by the user. + * + * A NetworkScore object represents the characteristics of a network that affects how good the + * network is considered for a particular use. + * @hide + */ +@SystemApi +public final class NetworkScore implements Parcelable { + // This will be removed soon. Do *NOT* depend on it for any new code that is not part of + // a migration. + private final int mLegacyInt; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + KEEP_CONNECTED_NONE, + KEEP_CONNECTED_FOR_HANDOVER + }) + public @interface KeepConnectedReason { } + + /** + * Do not keep this network connected if there is no outstanding request for it. + */ + public static final int KEEP_CONNECTED_NONE = 0; + /** + * Keep this network connected even if there is no outstanding request for it, because it + * is being considered for handover. + */ + public static final int KEEP_CONNECTED_FOR_HANDOVER = 1; + + // Agent-managed policies + // This network should lose to a wifi that has ever been validated + // NOTE : temporarily this policy is managed by ConnectivityService, because of legacy. The + // legacy design has this bit global to the system and tacked on WiFi which means it will affect + // networks from carriers who don't want it and non-carrier networks, which is bad for users. + // The S design has this on mobile networks only, so this can be fixed eventually ; as CS + // doesn't know what carriers need this bit, the initial S implementation will continue to + // affect other carriers but will at least leave non-mobile networks alone. Eventually Telephony + // should set this on networks from carriers that require it. + /** @hide */ + public static final int POLICY_YIELD_TO_BAD_WIFI = 1; + // This network is primary for this transport. + /** @hide */ + public static final int POLICY_TRANSPORT_PRIMARY = 2; + // This network is exiting : it will likely disconnect in a few seconds. + /** @hide */ + public static final int POLICY_EXITING = 3; + + /** @hide */ + public static final int MIN_AGENT_MANAGED_POLICY = POLICY_YIELD_TO_BAD_WIFI; + /** @hide */ + public static final int MAX_AGENT_MANAGED_POLICY = POLICY_EXITING; + + // Bitmask of all the policies applied to this score. + private final long mPolicies; + + private final int mKeepConnectedReason; + + /** @hide */ + NetworkScore(final int legacyInt, final long policies, + @KeepConnectedReason final int keepConnectedReason) { + mLegacyInt = legacyInt; + mPolicies = policies; + mKeepConnectedReason = keepConnectedReason; + } + + private NetworkScore(@NonNull final Parcel in) { + mLegacyInt = in.readInt(); + mPolicies = in.readLong(); + mKeepConnectedReason = in.readInt(); + } + + /** + * Get the legacy int score embedded in this NetworkScore. + * @see Builder#setLegacyInt(int) + */ + public int getLegacyInt() { + return mLegacyInt; + } + + /** + * Returns the keep-connected reason, or KEEP_CONNECTED_NONE. + */ + public int getKeepConnectedReason() { + return mKeepConnectedReason; + } + + /** + * @return whether this score has a particular policy. + * + * @hide + */ + @VisibleForTesting + public boolean hasPolicy(final int policy) { + return 0 != (mPolicies & (1L << policy)); + } + + /** + * To the exclusive usage of FullScore + * @hide + */ + public long getPolicies() { + return mPolicies; + } + + /** + * Whether this network should yield to a previously validated wifi gone bad. + * + * If this policy is set, other things being equal, the device will prefer a previously + * validated WiFi even if this network is validated and the WiFi is not. + * If this policy is not set, the device prefers the validated network. + * + * @hide + */ + // TODO : Unhide this for telephony and have telephony call it on the relevant carriers. + // In the mean time this is handled by Connectivity in a backward-compatible manner. + public boolean shouldYieldToBadWifi() { + return hasPolicy(POLICY_YIELD_TO_BAD_WIFI); + } + + /** + * Whether this network is primary for this transport. + * + * When multiple networks of the same transport are active, the device prefers the ones that + * are primary. This is meant in particular for DS-DA devices with a user setting to choose the + * default SIM card, or for WiFi STA+STA and make-before-break cases. + * + * @hide + */ + @SystemApi + public boolean isTransportPrimary() { + return hasPolicy(POLICY_TRANSPORT_PRIMARY); + } + + /** + * Whether this network is exiting. + * + * If this policy is set, the device will expect this network to disconnect within seconds. + * It will try to migrate to some other network if any is available, policy permitting, to + * avoid service disruption. + * This is useful in particular when a good cellular network is available and WiFi is getting + * weak and risks disconnecting soon. The WiFi network should be marked as exiting so that + * the device will prefer the reliable mobile network over this soon-to-be-lost WiFi. + * + * @hide + */ + @SystemApi + public boolean isExiting() { + return hasPolicy(POLICY_EXITING); + } + + @Override + public String toString() { + return "Score(" + mLegacyInt + " ; Policies : " + mPolicies + ")"; + } + + @Override + public void writeToParcel(@NonNull final Parcel dest, final int flags) { + dest.writeInt(mLegacyInt); + dest.writeLong(mPolicies); + dest.writeInt(mKeepConnectedReason); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull public static final Creator<NetworkScore> CREATOR = new Creator<>() { + @Override + @NonNull + public NetworkScore createFromParcel(@NonNull final Parcel in) { + return new NetworkScore(in); + } + + @Override + @NonNull + public NetworkScore[] newArray(int size) { + return new NetworkScore[size]; + } + }; + + /** + * A builder for NetworkScore. + */ + public static final class Builder { + private static final long POLICY_NONE = 0L; + private static final int INVALID_LEGACY_INT = Integer.MIN_VALUE; + private int mLegacyInt = INVALID_LEGACY_INT; + private int mKeepConnectedReason = KEEP_CONNECTED_NONE; + private int mPolicies = 0; + + /** + * Sets the legacy int for this score. + * + * This will be used for measurements and logs, but will no longer be used for ranking + * networks against each other. Callers that existed before Android S should send what + * they used to send as the int score. + * + * @param score the legacy int + * @return this + */ + @NonNull + public Builder setLegacyInt(final int score) { + mLegacyInt = score; + return this; + } + + + /** + * Set for a network that should never be preferred to a wifi that has ever been validated + * + * If this policy is set, other things being equal, the device will prefer a previously + * validated WiFi even if this network is validated and the WiFi is not. + * If this policy is not set, the device prefers the validated network. + * + * @return this builder + * @hide + */ + // TODO : Unhide this for telephony and have telephony call it on the relevant carriers. + // In the mean time this is handled by Connectivity in a backward-compatible manner. + @NonNull + public Builder setShouldYieldToBadWifi(final boolean val) { + if (val) { + mPolicies |= (1L << POLICY_YIELD_TO_BAD_WIFI); + } else { + mPolicies &= ~(1L << POLICY_YIELD_TO_BAD_WIFI); + } + return this; + } + + /** + * Set for a network that is primary for this transport. + * + * When multiple networks of the same transport are active, the device prefers the ones that + * are primary. This is meant in particular for DS-DA devices with a user setting to choose + * the default SIM card, or for WiFi STA+STA and make-before-break cases. + * + * @return this builder + * @hide + */ + @SystemApi + @NonNull + public Builder setTransportPrimary(final boolean val) { + if (val) { + mPolicies |= (1L << POLICY_TRANSPORT_PRIMARY); + } else { + mPolicies &= ~(1L << POLICY_TRANSPORT_PRIMARY); + } + return this; + } + + /** + * Set for a network that will likely disconnect in a few seconds. + * + * If this policy is set, the device will expect this network to disconnect within seconds. + * It will try to migrate to some other network if any is available, policy permitting, to + * avoid service disruption. + * This is useful in particular when a good cellular network is available and WiFi is + * getting weak and risks disconnecting soon. The WiFi network should be marked as exiting + * so that the device will prefer the reliable mobile network over this soon-to-be-lost + * WiFi. + * + * @return this builder + * @hide + */ + @SystemApi + @NonNull + public Builder setExiting(final boolean val) { + if (val) { + mPolicies |= (1L << POLICY_EXITING); + } else { + mPolicies &= ~(1L << POLICY_EXITING); + } + return this; + } + + /** + * Set the keep-connected reason. + * + * This can be reset by calling it again with {@link KEEP_CONNECTED_NONE}. + */ + @NonNull + public Builder setKeepConnectedReason(@KeepConnectedReason final int reason) { + mKeepConnectedReason = reason; + return this; + } + + /** + * Builds this NetworkScore. + * @return The built NetworkScore object. + */ + @NonNull + public NetworkScore build() { + return new NetworkScore(mLegacyInt, mPolicies, mKeepConnectedReason); + } + } +} diff --git a/framework/src/android/net/NetworkState.java b/framework/src/android/net/NetworkState.java new file mode 100644 index 0000000000000000000000000000000000000000..9b69674728a85281d4dd9d2f50f63fc4ca8b029f --- /dev/null +++ b/framework/src/android/net/NetworkState.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * Snapshot of network state. + * + * @hide + */ +public class NetworkState implements Parcelable { + private static final boolean VALIDATE_ROAMING_STATE = false; + + // TODO: remove and make members @NonNull. + public static final NetworkState EMPTY = new NetworkState(); + + public final NetworkInfo networkInfo; + public final LinkProperties linkProperties; + public final NetworkCapabilities networkCapabilities; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public final Network network; + public final String subscriberId; + public final int legacyNetworkType; + + private NetworkState() { + networkInfo = null; + linkProperties = null; + networkCapabilities = null; + network = null; + subscriberId = null; + legacyNetworkType = 0; + } + + public NetworkState(int legacyNetworkType, @NonNull LinkProperties linkProperties, + @NonNull NetworkCapabilities networkCapabilities, @NonNull Network network, + @Nullable String subscriberId) { + this(legacyNetworkType, new NetworkInfo(legacyNetworkType, 0, null, null), linkProperties, + networkCapabilities, network, subscriberId); + } + + // Constructor that used internally in ConnectivityService mainline module. + public NetworkState(@NonNull NetworkInfo networkInfo, @NonNull LinkProperties linkProperties, + @NonNull NetworkCapabilities networkCapabilities, @NonNull Network network, + @Nullable String subscriberId) { + this(networkInfo.getType(), networkInfo, linkProperties, + networkCapabilities, network, subscriberId); + } + + public NetworkState(int legacyNetworkType, @NonNull NetworkInfo networkInfo, + @NonNull LinkProperties linkProperties, + @NonNull NetworkCapabilities networkCapabilities, @NonNull Network network, + @Nullable String subscriberId) { + this.networkInfo = networkInfo; + this.linkProperties = linkProperties; + this.networkCapabilities = networkCapabilities; + this.network = network; + this.subscriberId = subscriberId; + this.legacyNetworkType = legacyNetworkType; + + // This object is an atomic view of a network, so the various components + // should always agree on roaming state. + if (VALIDATE_ROAMING_STATE && networkInfo != null && networkCapabilities != null) { + if (networkInfo.isRoaming() == networkCapabilities + .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) { + Log.wtf("NetworkState", "Roaming state disagreement between " + networkInfo + + " and " + networkCapabilities); + } + } + } + + @UnsupportedAppUsage + public NetworkState(Parcel in) { + networkInfo = in.readParcelable(null); + linkProperties = in.readParcelable(null); + networkCapabilities = in.readParcelable(null); + network = in.readParcelable(null); + subscriberId = in.readString(); + legacyNetworkType = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(networkInfo, flags); + out.writeParcelable(linkProperties, flags); + out.writeParcelable(networkCapabilities, flags); + out.writeParcelable(network, flags); + out.writeString(subscriberId); + out.writeInt(legacyNetworkType); + } + + @UnsupportedAppUsage + @NonNull + public static final Creator<NetworkState> CREATOR = new Creator<NetworkState>() { + @Override + public NetworkState createFromParcel(Parcel in) { + return new NetworkState(in); + } + + @Override + public NetworkState[] newArray(int size) { + return new NetworkState[size]; + } + }; +} diff --git a/framework/src/android/net/NetworkUtils.java b/framework/src/android/net/NetworkUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..2679b6218c1975dd7e6d375ebf2c6f2246824dbf --- /dev/null +++ b/framework/src/android/net/NetworkUtils.java @@ -0,0 +1,429 @@ +/* + * 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; + +import static android.net.ConnectivityManager.NETID_UNSET; + +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.system.ErrnoException; +import android.util.Log; +import android.util.Pair; + +import com.android.net.module.util.Inet4AddressUtils; + +import java.io.FileDescriptor; +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Locale; +import java.util.TreeSet; + +/** + * Native methods for managing network interfaces. + * + * {@hide} + */ +public class NetworkUtils { + static { + System.loadLibrary("framework-connectivity-jni"); + } + + private static final String TAG = "NetworkUtils"; + + /** + * Attaches a socket filter that drops all of incoming packets. + * @param fd the socket's {@link FileDescriptor}. + */ + public static native void attachDropAllBPFFilter(FileDescriptor fd) throws SocketException; + + /** + * Detaches a socket filter. + * @param fd the socket's {@link FileDescriptor}. + */ + public static native void detachBPFFilter(FileDescriptor fd) throws SocketException; + + private static native boolean bindProcessToNetworkHandle(long netHandle); + + /** + * Binds the current process to the network designated by {@code netId}. All sockets created + * in the future (and not explicitly bound via a bound {@link SocketFactory} (see + * {@link Network#getSocketFactory}) will be bound to this network. Note that if this + * {@code Network} ever disconnects all sockets created in this way will cease to work. This + * is by design so an application doesn't accidentally use sockets it thinks are still bound to + * a particular {@code Network}. Passing NETID_UNSET clears the binding. + */ + public static boolean bindProcessToNetwork(int netId) { + return bindProcessToNetworkHandle(new Network(netId).getNetworkHandle()); + } + + private static native long getBoundNetworkHandleForProcess(); + + /** + * Return the netId last passed to {@link #bindProcessToNetwork}, or NETID_UNSET if + * {@link #unbindProcessToNetwork} has been called since {@link #bindProcessToNetwork}. + */ + public static int getBoundNetworkForProcess() { + final long netHandle = getBoundNetworkHandleForProcess(); + return netHandle == 0L ? NETID_UNSET : Network.fromNetworkHandle(netHandle).getNetId(); + } + + /** + * Binds host resolutions performed by this process to the network designated by {@code netId}. + * {@link #bindProcessToNetwork} takes precedence over this setting. Passing NETID_UNSET clears + * the binding. + * + * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature(). + */ + @Deprecated + public native static boolean bindProcessToNetworkForHostResolution(int netId); + + private static native int bindSocketToNetworkHandle(FileDescriptor fd, long netHandle); + + /** + * Explicitly binds {@code fd} to the network designated by {@code netId}. This + * overrides any binding via {@link #bindProcessToNetwork}. + * @return 0 on success or negative errno on failure. + */ + public static int bindSocketToNetwork(FileDescriptor fd, int netId) { + return bindSocketToNetworkHandle(fd, new Network(netId).getNetworkHandle()); + } + + /** + * Determine if {@code uid} can access network designated by {@code netId}. + * @return {@code true} if {@code uid} can access network, {@code false} otherwise. + */ + public static boolean queryUserAccess(int uid, int netId) { + // TODO (b/183485986): remove this method + return false; + } + + private static native FileDescriptor resNetworkSend( + long netHandle, byte[] msg, int msglen, int flags) throws ErrnoException; + + /** + * DNS resolver series jni method. + * Issue the query {@code msg} on the network designated by {@code netId}. + * {@code flags} is an additional config to control actual querying behavior. + * @return a file descriptor to watch for read events + */ + public static FileDescriptor resNetworkSend( + int netId, byte[] msg, int msglen, int flags) throws ErrnoException { + return resNetworkSend(new Network(netId).getNetworkHandle(), msg, msglen, flags); + } + + private static native FileDescriptor resNetworkQuery( + long netHandle, String dname, int nsClass, int nsType, int flags) throws ErrnoException; + + /** + * DNS resolver series jni method. + * Look up the {@code nsClass} {@code nsType} Resource Record (RR) associated + * with Domain Name {@code dname} on the network designated by {@code netId}. + * {@code flags} is an additional config to control actual querying behavior. + * @return a file descriptor to watch for read events + */ + public static FileDescriptor resNetworkQuery( + int netId, String dname, int nsClass, int nsType, int flags) throws ErrnoException { + return resNetworkQuery(new Network(netId).getNetworkHandle(), dname, nsClass, nsType, + flags); + } + + /** + * DNS resolver series jni method. + * Read a result for the query associated with the {@code fd}. + * @return DnsResponse containing blob answer and rcode + */ + public static native DnsResolver.DnsResponse resNetworkResult(FileDescriptor fd) + throws ErrnoException; + + /** + * DNS resolver series jni method. + * Attempts to cancel the in-progress query associated with the {@code fd}. + */ + public static native void resNetworkCancel(FileDescriptor fd); + + /** + * DNS resolver series jni method. + * Attempts to get network which resolver will use if no network is explicitly selected. + */ + public static native Network getDnsNetwork() throws ErrnoException; + + /** + * Get the tcp repair window associated with the {@code fd}. + * + * @param fd the tcp socket's {@link FileDescriptor}. + * @return a {@link TcpRepairWindow} object indicates tcp window size. + */ + public static native TcpRepairWindow getTcpRepairWindow(FileDescriptor fd) + throws ErrnoException; + + /** + * @see Inet4AddressUtils#intToInet4AddressHTL(int) + * @deprecated Use either {@link Inet4AddressUtils#intToInet4AddressHTH(int)} + * or {@link Inet4AddressUtils#intToInet4AddressHTL(int)} + */ + @Deprecated + @UnsupportedAppUsage + public static InetAddress intToInetAddress(int hostAddress) { + return Inet4AddressUtils.intToInet4AddressHTL(hostAddress); + } + + /** + * @see Inet4AddressUtils#inet4AddressToIntHTL(Inet4Address) + * @deprecated Use either {@link Inet4AddressUtils#inet4AddressToIntHTH(Inet4Address)} + * or {@link Inet4AddressUtils#inet4AddressToIntHTL(Inet4Address)} + */ + @Deprecated + public static int inetAddressToInt(Inet4Address inetAddr) + throws IllegalArgumentException { + return Inet4AddressUtils.inet4AddressToIntHTL(inetAddr); + } + + /** + * @see Inet4AddressUtils#prefixLengthToV4NetmaskIntHTL(int) + * @deprecated Use either {@link Inet4AddressUtils#prefixLengthToV4NetmaskIntHTH(int)} + * or {@link Inet4AddressUtils#prefixLengthToV4NetmaskIntHTL(int)} + */ + @Deprecated + @UnsupportedAppUsage + public static int prefixLengthToNetmaskInt(int prefixLength) + throws IllegalArgumentException { + return Inet4AddressUtils.prefixLengthToV4NetmaskIntHTL(prefixLength); + } + + /** + * Convert a IPv4 netmask integer to a prefix length + * @param netmask as an integer (0xff000000 for a /8 subnet) + * @return the network prefix length + */ + public static int netmaskIntToPrefixLength(int netmask) { + return Integer.bitCount(netmask); + } + + /** + * Convert an IPv4 netmask to a prefix length, checking that the netmask is contiguous. + * @param netmask as a {@code Inet4Address}. + * @return the network prefix length + * @throws IllegalArgumentException the specified netmask was not contiguous. + * @hide + * @deprecated use {@link Inet4AddressUtils#netmaskToPrefixLength(Inet4Address)} + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Deprecated + public static int netmaskToPrefixLength(Inet4Address netmask) { + // This is only here because some apps seem to be using it (@UnsupportedAppUsage). + return Inet4AddressUtils.netmaskToPrefixLength(netmask); + } + + + /** + * Create an InetAddress from a string where the string must be a standard + * representation of a V4 or V6 address. Avoids doing a DNS lookup on failure + * but it will throw an IllegalArgumentException in that case. + * @param addrString + * @return the InetAddress + * @hide + * @deprecated Use {@link InetAddresses#parseNumericAddress(String)}, if possible. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + @Deprecated + public static InetAddress numericToInetAddress(String addrString) + throws IllegalArgumentException { + return InetAddresses.parseNumericAddress(addrString); + } + + /** + * Returns the implicit netmask of an IPv4 address, as was the custom before 1993. + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public static int getImplicitNetmask(Inet4Address address) { + // Only here because it seems to be used by apps + return Inet4AddressUtils.getImplicitNetmask(address); + } + + /** + * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64". + * @hide + */ + public static Pair<InetAddress, Integer> parseIpAndMask(String ipAndMaskString) { + InetAddress address = null; + int prefixLength = -1; + try { + String[] pieces = ipAndMaskString.split("/", 2); + prefixLength = Integer.parseInt(pieces[1]); + address = InetAddresses.parseNumericAddress(pieces[0]); + } catch (NullPointerException e) { // Null string. + } catch (ArrayIndexOutOfBoundsException e) { // No prefix length. + } catch (NumberFormatException e) { // Non-numeric prefix. + } catch (IllegalArgumentException e) { // Invalid IP address. + } + + if (address == null || prefixLength == -1) { + throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString); + } + + return new Pair<InetAddress, Integer>(address, prefixLength); + } + + /** + * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64". + * @hide + * + * @deprecated This method is used only for IpPrefix and LinkAddress. Since Android S, use + * {@link #parseIpAndMask(String)}, if possible. + */ + @Deprecated + public static Pair<InetAddress, Integer> legacyParseIpAndMask(String ipAndMaskString) { + InetAddress address = null; + int prefixLength = -1; + try { + String[] pieces = ipAndMaskString.split("/", 2); + prefixLength = Integer.parseInt(pieces[1]); + if (pieces[0] == null || pieces[0].isEmpty()) { + final byte[] bytes = new byte[16]; + bytes[15] = 1; + return new Pair<InetAddress, Integer>(Inet6Address.getByAddress( + "ip6-localhost"/* host */, bytes, 0 /* scope_id */), prefixLength); + } + + if (pieces[0].startsWith("[") + && pieces[0].endsWith("]") + && pieces[0].indexOf(':') != -1) { + pieces[0] = pieces[0].substring(1, pieces[0].length() - 1); + } + address = InetAddresses.parseNumericAddress(pieces[0]); + } catch (NullPointerException e) { // Null string. + } catch (ArrayIndexOutOfBoundsException e) { // No prefix length. + } catch (NumberFormatException e) { // Non-numeric prefix. + } catch (IllegalArgumentException e) { // Invalid IP address. + } catch (UnknownHostException e) { // IP address length is illegal + } + + if (address == null || prefixLength == -1) { + throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString); + } + + return new Pair<InetAddress, Integer>(address, prefixLength); + } + + /** + * Convert a 32 char hex string into a Inet6Address. + * throws a runtime exception if the string isn't 32 chars, isn't hex or can't be + * made into an Inet6Address + * @param addrHexString a 32 character hex string representing an IPv6 addr + * @return addr an InetAddress representation for the string + */ + public static InetAddress hexToInet6Address(String addrHexString) + throws IllegalArgumentException { + try { + return numericToInetAddress(String.format(Locale.US, "%s:%s:%s:%s:%s:%s:%s:%s", + addrHexString.substring(0,4), addrHexString.substring(4,8), + addrHexString.substring(8,12), addrHexString.substring(12,16), + addrHexString.substring(16,20), addrHexString.substring(20,24), + addrHexString.substring(24,28), addrHexString.substring(28,32))); + } catch (Exception e) { + Log.e("NetworkUtils", "error in hexToInet6Address(" + addrHexString + "): " + e); + throw new IllegalArgumentException(e); + } + } + + /** + * Trim leading zeros from IPv4 address strings + * Our base libraries will interpret that as octel.. + * Must leave non v4 addresses and host names alone. + * For example, 192.168.000.010 -> 192.168.0.10 + * TODO - fix base libraries and remove this function + * @param addr a string representing an ip addr + * @return a string propertly trimmed + */ + @UnsupportedAppUsage + public static String trimV4AddrZeros(String addr) { + return Inet4AddressUtils.trimAddressZeros(addr); + } + + /** + * Returns a prefix set without overlaps. + * + * This expects the src set to be sorted from shorter to longer. Results are undefined + * failing this condition. The returned prefix set is sorted in the same order as the + * passed set, with the same comparator. + */ + private static TreeSet<IpPrefix> deduplicatePrefixSet(final TreeSet<IpPrefix> src) { + final TreeSet<IpPrefix> dst = new TreeSet<>(src.comparator()); + // Prefixes match addresses that share their upper part up to their length, therefore + // the only kind of possible overlap in two prefixes is strict inclusion of the longer + // (more restrictive) in the shorter (including equivalence if they have the same + // length). + // Because prefixes in the src set are sorted from shorter to longer, deduplicating + // is done by simply iterating in order, and not adding any longer prefix that is + // already covered by a shorter one. + newPrefixes: + for (IpPrefix newPrefix : src) { + for (IpPrefix existingPrefix : dst) { + if (existingPrefix.containsPrefix(newPrefix)) { + continue newPrefixes; + } + } + dst.add(newPrefix); + } + return dst; + } + + /** + * Returns how many IPv4 addresses match any of the prefixes in the passed ordered set. + * + * Obviously this returns an integral value between 0 and 2**32. + * The behavior is undefined if any of the prefixes is not an IPv4 prefix or if the + * set is not ordered smallest prefix to longer prefix. + * + * @param prefixes the set of prefixes, ordered by length + */ + public static long routedIPv4AddressCount(final TreeSet<IpPrefix> prefixes) { + long routedIPCount = 0; + for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) { + if (!prefix.isIPv4()) { + Log.wtf(TAG, "Non-IPv4 prefix in routedIPv4AddressCount"); + } + int rank = 32 - prefix.getPrefixLength(); + routedIPCount += 1L << rank; + } + return routedIPCount; + } + + /** + * Returns how many IPv6 addresses match any of the prefixes in the passed ordered set. + * + * This returns a BigInteger between 0 and 2**128. + * The behavior is undefined if any of the prefixes is not an IPv6 prefix or if the + * set is not ordered smallest prefix to longer prefix. + */ + public static BigInteger routedIPv6AddressCount(final TreeSet<IpPrefix> prefixes) { + BigInteger routedIPCount = BigInteger.ZERO; + for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) { + if (!prefix.isIPv6()) { + Log.wtf(TAG, "Non-IPv6 prefix in routedIPv6AddressCount"); + } + int rank = 128 - prefix.getPrefixLength(); + routedIPCount = routedIPCount.add(BigInteger.ONE.shiftLeft(rank)); + } + return routedIPCount; + } + +} diff --git a/framework/src/android/net/OemNetworkPreferences.java b/framework/src/android/net/OemNetworkPreferences.java new file mode 100644 index 0000000000000000000000000000000000000000..2bb006df82e0610db53322fa2912d7580f4ac79e --- /dev/null +++ b/framework/src/android/net/OemNetworkPreferences.java @@ -0,0 +1,276 @@ +/* + * 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.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Network preferences to set the default active network on a per-application basis as per a given + * {@link OemNetworkPreference}. An example of this would be to set an application's network + * preference to {@link #OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK} which would have the default + * network for that application set to an unmetered network first if available and if not, it then + * set that application's default network to an OEM managed network if available. + * + * @hide + */ +@SystemApi +public final class OemNetworkPreferences implements Parcelable { + // Valid production preferences must be > 0, negative values reserved for testing + /** + * This preference is only to be used for testing and nothing else. + * Use only TRANSPORT_TEST transport networks. + * @hide + */ + public static final int OEM_NETWORK_PREFERENCE_TEST_ONLY = -2; + + /** + * This preference is only to be used for testing and nothing else. + * If an unmetered network is available, use it. + * Otherwise, if a network with the TRANSPORT_TEST transport is available, use it. + * Otherwise, use the general default network. + * @hide + */ + public static final int OEM_NETWORK_PREFERENCE_TEST = -1; + + /** + * Default in case this value is not set. Using it will result in an error. + */ + public static final int OEM_NETWORK_PREFERENCE_UNINITIALIZED = 0; + + /** + * If an unmetered network is available, use it. + * Otherwise, if a network with the OEM_PAID capability is available, use it. + * Otherwise, use the general default network. + */ + public static final int OEM_NETWORK_PREFERENCE_OEM_PAID = 1; + + /** + * If an unmetered network is available, use it. + * Otherwise, if a network with the OEM_PAID capability is available, use it. + * Otherwise, the app doesn't get a default network. + */ + public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK = 2; + + /** + * Use only NET_CAPABILITY_OEM_PAID networks. + */ + public static final int OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY = 3; + + /** + * Use only NET_CAPABILITY_OEM_PRIVATE networks. + */ + public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4; + + /** + * The max allowed value for an OEM network preference. + * @hide + */ + public static final int OEM_NETWORK_PREFERENCE_MAX = OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + @NonNull + private final Bundle mNetworkMappings; + + /** + * Return whether this object is empty. + * @hide + */ + public boolean isEmpty() { + return mNetworkMappings.keySet().size() == 0; + } + + /** + * Return the currently built application package name to {@link OemNetworkPreference} mappings. + * @return the current network preferences map. + */ + @NonNull + public Map<String, Integer> getNetworkPreferences() { + return convertToUnmodifiableMap(mNetworkMappings); + } + + private OemNetworkPreferences(@NonNull final Bundle networkMappings) { + Objects.requireNonNull(networkMappings); + mNetworkMappings = (Bundle) networkMappings.clone(); + } + + @Override + public String toString() { + return "OemNetworkPreferences{" + "mNetworkMappings=" + getNetworkPreferences() + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OemNetworkPreferences that = (OemNetworkPreferences) o; + + return mNetworkMappings.size() == that.mNetworkMappings.size() + && mNetworkMappings.toString().equals(that.mNetworkMappings.toString()); + } + + @Override + public int hashCode() { + return Objects.hash(mNetworkMappings); + } + + /** + * Builder used to create {@link OemNetworkPreferences} objects. Specify the preferred Network + * to package name mappings. + */ + public static final class Builder { + private final Bundle mNetworkMappings; + + public Builder() { + mNetworkMappings = new Bundle(); + } + + /** + * Constructor to populate the builder's values with an already built + * {@link OemNetworkPreferences}. + * @param preferences the {@link OemNetworkPreferences} to populate with. + */ + public Builder(@NonNull final OemNetworkPreferences preferences) { + Objects.requireNonNull(preferences); + mNetworkMappings = (Bundle) preferences.mNetworkMappings.clone(); + } + + /** + * Add a network preference for a given package. Previously stored values for the given + * package will be overwritten. + * + * @param packageName full package name (e.g.: "com.google.apps.contacts") of the app + * to use the given preference + * @param preference the desired network preference to use + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder addNetworkPreference(@NonNull final String packageName, + @OemNetworkPreference final int preference) { + Objects.requireNonNull(packageName); + mNetworkMappings.putInt(packageName, preference); + return this; + } + + /** + * Remove a network preference for a given package. + * + * @param packageName full package name (e.g.: "com.google.apps.contacts") of the app to + * remove a preference for. + * @return The builder to facilitate chaining. + */ + @NonNull + public Builder clearNetworkPreference(@NonNull final String packageName) { + Objects.requireNonNull(packageName); + mNetworkMappings.remove(packageName); + return this; + } + + /** + * Build {@link OemNetworkPreferences} return the current OEM network preferences. + */ + @NonNull + public OemNetworkPreferences build() { + return new OemNetworkPreferences(mNetworkMappings); + } + } + + private static Map<String, Integer> convertToUnmodifiableMap(@NonNull final Bundle bundle) { + final Map<String, Integer> networkPreferences = new HashMap<>(); + for (final String key : bundle.keySet()) { + networkPreferences.put(key, bundle.getInt(key)); + } + return Collections.unmodifiableMap(networkPreferences); + } + + /** @hide */ + @IntDef(prefix = "OEM_NETWORK_PREFERENCE_", value = { + OEM_NETWORK_PREFERENCE_TEST_ONLY, + OEM_NETWORK_PREFERENCE_TEST, + OEM_NETWORK_PREFERENCE_UNINITIALIZED, + OEM_NETWORK_PREFERENCE_OEM_PAID, + OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK, + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY, + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface OemNetworkPreference {} + + /** + * Return the string value for OemNetworkPreference + * + * @param value int value of OemNetworkPreference + * @return string version of OemNetworkPreference + * + * @hide + */ + @NonNull + public static String oemNetworkPreferenceToString(@OemNetworkPreference int value) { + switch (value) { + case OEM_NETWORK_PREFERENCE_TEST_ONLY: + return "OEM_NETWORK_PREFERENCE_TEST_ONLY"; + case OEM_NETWORK_PREFERENCE_TEST: + return "OEM_NETWORK_PREFERENCE_TEST"; + case OEM_NETWORK_PREFERENCE_UNINITIALIZED: + return "OEM_NETWORK_PREFERENCE_UNINITIALIZED"; + case OEM_NETWORK_PREFERENCE_OEM_PAID: + return "OEM_NETWORK_PREFERENCE_OEM_PAID"; + case OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK: + return "OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK"; + case OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY: + return "OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY"; + case OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY: + return "OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY"; + default: + return Integer.toHexString(value); + } + } + + @Override + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + dest.writeBundle(mNetworkMappings); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Parcelable.Creator<OemNetworkPreferences> CREATOR = + new Parcelable.Creator<OemNetworkPreferences>() { + @Override + public OemNetworkPreferences[] newArray(int size) { + return new OemNetworkPreferences[size]; + } + + @Override + public OemNetworkPreferences createFromParcel(@NonNull android.os.Parcel in) { + return new OemNetworkPreferences( + in.readBundle(getClass().getClassLoader())); + } + }; +} diff --git a/framework/src/android/net/ParseException.java b/framework/src/android/net/ParseException.java new file mode 100644 index 0000000000000000000000000000000000000000..9d4727a84bc0f360979e506b616d77db2f3a8564 --- /dev/null +++ b/framework/src/android/net/ParseException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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; + +/** + * Thrown when parsing failed. + */ +// See non-public class {@link WebAddress}. +public class ParseException extends RuntimeException { + public String response; + + public ParseException(@NonNull String response) { + super(response); + this.response = response; + } + + public ParseException(@NonNull String response, @NonNull Throwable cause) { + super(response, cause); + this.response = response; + } +} diff --git a/framework/src/android/net/ProxyInfo.java b/framework/src/android/net/ProxyInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..0deda371f6b9e4d795abb378852e19290f4e7b54 --- /dev/null +++ b/framework/src/android/net/ProxyInfo.java @@ -0,0 +1,370 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import com.android.net.module.util.ProxyUtils; + +import java.net.InetSocketAddress; +import java.net.URLConnection; +import java.util.List; +import java.util.Locale; + +/** + * Describes a proxy configuration. + * + * Proxy configurations are already integrated within the {@code java.net} and + * Apache HTTP stack. So {@link URLConnection} and Apache's {@code HttpClient} will use + * them automatically. + * + * Other HTTP stacks will need to obtain the proxy info by watching for the + * {@link Proxy#PROXY_CHANGE_ACTION} broadcast and calling methods such as + * {@link android.net.ConnectivityManager#getDefaultProxy}. + */ +public class ProxyInfo implements Parcelable { + + private final String mHost; + private final int mPort; + private final String mExclusionList; + private final String[] mParsedExclusionList; + private final Uri mPacFileUrl; + + /** + *@hide + */ + public static final String LOCAL_EXCL_LIST = ""; + /** + *@hide + */ + public static final int LOCAL_PORT = -1; + /** + *@hide + */ + public static final String LOCAL_HOST = "localhost"; + + /** + * Constructs a {@link ProxyInfo} object that points at a Direct proxy + * on the specified host and port. + */ + public static ProxyInfo buildDirectProxy(String host, int port) { + return new ProxyInfo(host, port, null); + } + + /** + * Constructs a {@link ProxyInfo} object that points at a Direct proxy + * on the specified host and port. + * + * The proxy will not be used to access any host in exclusion list, exclList. + * + * @param exclList Hosts to exclude using the proxy on connections for. These + * hosts can use wildcards such as *.example.com. + */ + public static ProxyInfo buildDirectProxy(String host, int port, List<String> exclList) { + String[] array = exclList.toArray(new String[exclList.size()]); + return new ProxyInfo(host, port, TextUtils.join(",", array), array); + } + + /** + * Construct a {@link ProxyInfo} that will download and run the PAC script + * at the specified URL. + */ + public static ProxyInfo buildPacProxy(Uri pacUri) { + return new ProxyInfo(pacUri); + } + + /** + * Construct a {@link ProxyInfo} object that will download and run the PAC script at the + * specified URL and port. + */ + @NonNull + public static ProxyInfo buildPacProxy(@NonNull Uri pacUrl, int port) { + return new ProxyInfo(pacUrl, port); + } + + /** + * Create a ProxyProperties that points at a HTTP Proxy. + * @hide + */ + @UnsupportedAppUsage + public ProxyInfo(String host, int port, String exclList) { + mHost = host; + mPort = port; + mExclusionList = exclList; + mParsedExclusionList = parseExclusionList(mExclusionList); + mPacFileUrl = Uri.EMPTY; + } + + /** + * Create a ProxyProperties that points at a PAC URL. + * @hide + */ + public ProxyInfo(@NonNull Uri pacFileUrl) { + mHost = LOCAL_HOST; + mPort = LOCAL_PORT; + mExclusionList = LOCAL_EXCL_LIST; + mParsedExclusionList = parseExclusionList(mExclusionList); + if (pacFileUrl == null) { + throw new NullPointerException(); + } + mPacFileUrl = pacFileUrl; + } + + /** + * Only used in PacProxyService after Local Proxy is bound. + * @hide + */ + public ProxyInfo(@NonNull Uri pacFileUrl, int localProxyPort) { + mHost = LOCAL_HOST; + mPort = localProxyPort; + mExclusionList = LOCAL_EXCL_LIST; + mParsedExclusionList = parseExclusionList(mExclusionList); + if (pacFileUrl == null) { + throw new NullPointerException(); + } + mPacFileUrl = pacFileUrl; + } + + private static String[] parseExclusionList(String exclusionList) { + if (exclusionList == null) { + return new String[0]; + } else { + return exclusionList.toLowerCase(Locale.ROOT).split(","); + } + } + + private ProxyInfo(String host, int port, String exclList, String[] parsedExclList) { + mHost = host; + mPort = port; + mExclusionList = exclList; + mParsedExclusionList = parsedExclList; + mPacFileUrl = Uri.EMPTY; + } + + /** + * A copy constructor to hold proxy properties. + */ + public ProxyInfo(@Nullable ProxyInfo source) { + if (source != null) { + mHost = source.getHost(); + mPort = source.getPort(); + mPacFileUrl = source.mPacFileUrl; + mExclusionList = source.getExclusionListAsString(); + mParsedExclusionList = source.mParsedExclusionList; + } else { + mHost = null; + mPort = 0; + mExclusionList = null; + mParsedExclusionList = null; + mPacFileUrl = Uri.EMPTY; + } + } + + /** + * @hide + */ + public InetSocketAddress getSocketAddress() { + InetSocketAddress inetSocketAddress = null; + try { + inetSocketAddress = new InetSocketAddress(mHost, mPort); + } catch (IllegalArgumentException e) { } + return inetSocketAddress; + } + + /** + * Returns the URL of the current PAC script or null if there is + * no PAC script. + */ + public Uri getPacFileUrl() { + return mPacFileUrl; + } + + /** + * When configured to use a Direct Proxy this returns the host + * of the proxy. + */ + public String getHost() { + return mHost; + } + + /** + * When configured to use a Direct Proxy this returns the port + * of the proxy + */ + public int getPort() { + return mPort; + } + + /** + * When configured to use a Direct Proxy this returns the list + * of hosts for which the proxy is ignored. + */ + public String[] getExclusionList() { + return mParsedExclusionList; + } + + /** + * comma separated + * @hide + */ + @Nullable + public String getExclusionListAsString() { + return mExclusionList; + } + + /** + * Return true if the pattern of proxy is valid, otherwise return false. + */ + public boolean isValid() { + if (!Uri.EMPTY.equals(mPacFileUrl)) return true; + return ProxyUtils.PROXY_VALID == ProxyUtils.validate(mHost == null ? "" : mHost, + mPort == 0 ? "" : Integer.toString(mPort), + mExclusionList == null ? "" : mExclusionList); + } + + /** + * @hide + */ + public java.net.Proxy makeProxy() { + java.net.Proxy proxy = java.net.Proxy.NO_PROXY; + if (mHost != null) { + try { + InetSocketAddress inetSocketAddress = new InetSocketAddress(mHost, mPort); + proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, inetSocketAddress); + } catch (IllegalArgumentException e) { + } + } + return proxy; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (!Uri.EMPTY.equals(mPacFileUrl)) { + sb.append("PAC Script: "); + sb.append(mPacFileUrl); + } + if (mHost != null) { + sb.append("["); + sb.append(mHost); + sb.append("] "); + sb.append(Integer.toString(mPort)); + if (mExclusionList != null) { + sb.append(" xl=").append(mExclusionList); + } + } else { + sb.append("[ProxyProperties.mHost == null]"); + } + return sb.toString(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof ProxyInfo)) return false; + ProxyInfo p = (ProxyInfo)o; + // If PAC URL is present in either then they must be equal. + // Other parameters will only be for fall back. + if (!Uri.EMPTY.equals(mPacFileUrl)) { + return mPacFileUrl.equals(p.getPacFileUrl()) && mPort == p.mPort; + } + if (!Uri.EMPTY.equals(p.mPacFileUrl)) { + return false; + } + if (mExclusionList != null && !mExclusionList.equals(p.getExclusionListAsString())) { + return false; + } + if (mHost != null && p.getHost() != null && mHost.equals(p.getHost()) == false) { + return false; + } + if (mHost != null && p.mHost == null) return false; + if (mHost == null && p.mHost != null) return false; + if (mPort != p.mPort) return false; + return true; + } + + /** + * Implement the Parcelable interface + * @hide + */ + public int describeContents() { + return 0; + } + + @Override + /* + * generate hashcode based on significant fields + */ + public int hashCode() { + return ((null == mHost) ? 0 : mHost.hashCode()) + + ((null == mExclusionList) ? 0 : mExclusionList.hashCode()) + + mPort; + } + + /** + * Implement the Parcelable interface. + * @hide + */ + public void writeToParcel(Parcel dest, int flags) { + if (!Uri.EMPTY.equals(mPacFileUrl)) { + dest.writeByte((byte)1); + mPacFileUrl.writeToParcel(dest, 0); + dest.writeInt(mPort); + return; + } else { + dest.writeByte((byte)0); + } + if (mHost != null) { + dest.writeByte((byte)1); + dest.writeString(mHost); + dest.writeInt(mPort); + } else { + dest.writeByte((byte)0); + } + dest.writeString(mExclusionList); + dest.writeStringArray(mParsedExclusionList); + } + + public static final @android.annotation.NonNull Creator<ProxyInfo> CREATOR = + new Creator<ProxyInfo>() { + public ProxyInfo createFromParcel(Parcel in) { + String host = null; + int port = 0; + if (in.readByte() != 0) { + Uri url = Uri.CREATOR.createFromParcel(in); + int localPort = in.readInt(); + return new ProxyInfo(url, localPort); + } + if (in.readByte() != 0) { + host = in.readString(); + port = in.readInt(); + } + String exclList = in.readString(); + String[] parsedExclList = in.createStringArray(); + ProxyInfo proxyProperties = new ProxyInfo(host, port, exclList, parsedExclList); + return proxyProperties; + } + + public ProxyInfo[] newArray(int size) { + return new ProxyInfo[size]; + } + }; +} diff --git a/framework/src/android/net/QosCallback.java b/framework/src/android/net/QosCallback.java new file mode 100644 index 0000000000000000000000000000000000000000..22f06bc0e690c0f03bc22523a29af292f62112eb --- /dev/null +++ b/framework/src/android/net/QosCallback.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.SystemApi; + +import java.util.concurrent.Executor; + +/** + * Receives Qos information given a {@link Network}. The callback is registered with + * {@link ConnectivityManager#registerQosCallback}. + * + * <p> + * <br/> + * The callback will no longer receive calls if any of the following takes place: + * <ol> + * <li>{@link ConnectivityManager#unregisterQosCallback(QosCallback)} is called with the same + * callback instance.</li> + * <li>{@link QosCallback#onError(QosCallbackException)} is called.</li> + * <li>A network specific issue occurs. eg. Congestion on a carrier network.</li> + * <li>The network registered with the callback has no associated QoS providers</li> + * </ul> + * {@hide} + */ +@SystemApi +public abstract class QosCallback { + /** + * Invoked after an error occurs on a registered callback. Once called, the callback is + * automatically unregistered and the callback will no longer receive calls. + * + * <p>The underlying exception can either be a runtime exception or a custom exception made for + * {@link QosCallback}. see: {@link QosCallbackException}. + * + * @param exception wraps the underlying cause + */ + public void onError(@NonNull final QosCallbackException exception) { + } + + /** + * Called when a Qos Session first becomes available to the callback or if its attributes have + * changed. + * <p> + * Note: The callback may be called multiple times with the same attributes. + * + * @param session the available session + * @param sessionAttributes the attributes of the session + */ + public void onQosSessionAvailable(@NonNull final QosSession session, + @NonNull final QosSessionAttributes sessionAttributes) { + } + + /** + * Called after a Qos Session is lost. + * <p> + * At least one call to + * {@link QosCallback#onQosSessionAvailable(QosSession, QosSessionAttributes)} + * with the same {@link QosSession} will precede a call to lost. + * + * @param session the lost session + */ + public void onQosSessionLost(@NonNull final QosSession session) { + } + + /** + * Thrown when there is a problem registering {@link QosCallback} with + * {@link ConnectivityManager#registerQosCallback(QosSocketInfo, QosCallback, Executor)}. + */ + public static class QosCallbackRegistrationException extends RuntimeException { + /** + * @hide + */ + public QosCallbackRegistrationException() { + super(); + } + } +} diff --git a/framework/src/android/net/QosCallbackConnection.java b/framework/src/android/net/QosCallbackConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..de0fc243b43bee2aeea0815fa27fdd2341cf6d44 --- /dev/null +++ b/framework/src/android/net/QosCallbackConnection.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Sends messages from {@link com.android.server.ConnectivityService} to the registered + * {@link QosCallback}. + * <p/> + * This is a satellite class of {@link ConnectivityManager} and not meant + * to be used in other contexts. + * + * @hide + */ +class QosCallbackConnection extends android.net.IQosCallback.Stub { + + @NonNull private final ConnectivityManager mConnectivityManager; + @Nullable private volatile QosCallback mCallback; + @NonNull private final Executor mExecutor; + + @VisibleForTesting + @Nullable + public QosCallback getCallback() { + return mCallback; + } + + /** + * The constructor for the connection + * + * @param connectivityManager the mgr that created this connection + * @param callback the callback to send messages back to + * @param executor The executor on which the callback will be invoked. The provided + * {@link Executor} must run callback sequentially, otherwise the order of + * callbacks cannot be guaranteed. + */ + QosCallbackConnection(@NonNull final ConnectivityManager connectivityManager, + @NonNull final QosCallback callback, + @NonNull final Executor executor) { + mConnectivityManager = Objects.requireNonNull(connectivityManager, + "connectivityManager must be non-null"); + mCallback = Objects.requireNonNull(callback, "callback must be non-null"); + mExecutor = Objects.requireNonNull(executor, "executor must be non-null"); + } + + /** + * Called when either the {@link EpsBearerQosSessionAttributes} has changed or on the first time + * the attributes have become available. + * + * @param session the session that is now available + * @param attributes the corresponding attributes of session + */ + @Override + public void onQosEpsBearerSessionAvailable(@NonNull final QosSession session, + @NonNull final EpsBearerQosSessionAttributes attributes) { + + mExecutor.execute(() -> { + final QosCallback callback = mCallback; + if (callback != null) { + callback.onQosSessionAvailable(session, attributes); + } + }); + } + + /** + * Called when either the {@link NrQosSessionAttributes} has changed or on the first time + * the attributes have become available. + * + * @param session the session that is now available + * @param attributes the corresponding attributes of session + */ + @Override + public void onNrQosSessionAvailable(@NonNull final QosSession session, + @NonNull final NrQosSessionAttributes attributes) { + + mExecutor.execute(() -> { + final QosCallback callback = mCallback; + if (callback != null) { + callback.onQosSessionAvailable(session, attributes); + } + }); + } + + /** + * Called when the session is lost. + * + * @param session the session that was lost + */ + @Override + public void onQosSessionLost(@NonNull final QosSession session) { + mExecutor.execute(() -> { + final QosCallback callback = mCallback; + if (callback != null) { + callback.onQosSessionLost(session); + } + }); + } + + /** + * Called when there is an error on the registered callback. + * + * @param errorType the type of error + */ + @Override + public void onError(@QosCallbackException.ExceptionType final int errorType) { + mExecutor.execute(() -> { + final QosCallback callback = mCallback; + if (callback != null) { + // Messages no longer need to be received since there was an error. + stopReceivingMessages(); + mConnectivityManager.unregisterQosCallback(callback); + callback.onError(QosCallbackException.createException(errorType)); + } + }); + } + + /** + * The callback will stop receiving messages. + * <p/> + * There are no synchronization guarantees on exactly when the callback will stop receiving + * messages. + */ + void stopReceivingMessages() { + mCallback = null; + } +} diff --git a/framework/src/android/net/QosCallbackException.java b/framework/src/android/net/QosCallbackException.java new file mode 100644 index 0000000000000000000000000000000000000000..7fd9a527e2ac2599c447943fbfb374d0a7b56ac5 --- /dev/null +++ b/framework/src/android/net/QosCallbackException.java @@ -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 android.net; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This is the exception type passed back through the onError method on {@link QosCallback}. + * {@link QosCallbackException#getCause()} contains the actual error that caused this exception. + * + * The possible exception types as causes are: + * 1. {@link NetworkReleasedException} + * 2. {@link SocketNotBoundException} + * 3. {@link UnsupportedOperationException} + * 4. {@link SocketLocalAddressChangedException} + * + * @hide + */ +@SystemApi +public final class QosCallbackException extends Exception { + + /** @hide */ + @IntDef(prefix = {"EX_TYPE_"}, value = { + EX_TYPE_FILTER_NONE, + EX_TYPE_FILTER_NETWORK_RELEASED, + EX_TYPE_FILTER_SOCKET_NOT_BOUND, + EX_TYPE_FILTER_NOT_SUPPORTED, + EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ExceptionType {} + + private static final String TAG = "QosCallbackException"; + + // Types of exceptions supported // + /** {@hide} */ + public static final int EX_TYPE_FILTER_NONE = 0; + + /** {@hide} */ + public static final int EX_TYPE_FILTER_NETWORK_RELEASED = 1; + + /** {@hide} */ + public static final int EX_TYPE_FILTER_SOCKET_NOT_BOUND = 2; + + /** {@hide} */ + public static final int EX_TYPE_FILTER_NOT_SUPPORTED = 3; + + /** {@hide} */ + public static final int EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED = 4; + + /** + * Creates exception based off of a type and message. Not all types of exceptions accept a + * custom message. + * + * {@hide} + */ + @NonNull + static QosCallbackException createException(@ExceptionType final int type) { + switch (type) { + case EX_TYPE_FILTER_NETWORK_RELEASED: + return new QosCallbackException(new NetworkReleasedException()); + case EX_TYPE_FILTER_SOCKET_NOT_BOUND: + return new QosCallbackException(new SocketNotBoundException()); + case EX_TYPE_FILTER_NOT_SUPPORTED: + return new QosCallbackException(new UnsupportedOperationException( + "This device does not support the specified filter")); + case EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED: + return new QosCallbackException( + new SocketLocalAddressChangedException()); + default: + Log.wtf(TAG, "create: No case setup for exception type: '" + type + "'"); + return new QosCallbackException( + new RuntimeException("Unknown exception code: " + type)); + } + } + + /** + * @hide + */ + public QosCallbackException(@NonNull final String message) { + super(message); + } + + /** + * @hide + */ + public QosCallbackException(@NonNull final Throwable cause) { + super(cause); + } +} diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..957c867f206da3aba003485957629f763f64287d --- /dev/null +++ b/framework/src/android/net/QosFilter.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.SystemApi; + +import java.net.InetAddress; + +/** + * Provides the related filtering logic to the {@link NetworkAgent} to match {@link QosSession}s + * to their related {@link QosCallback}. + * + * Used by the {@link com.android.server.ConnectivityService} to validate a {@link QosCallback} + * is still able to receive a {@link QosSession}. + * + * @hide + */ +@SystemApi +public abstract class QosFilter { + + /** + * The constructor is kept hidden from outside this package to ensure that all derived types + * are known and properly handled when being passed to and from {@link NetworkAgent}. + * + * @hide + */ + QosFilter() { + } + + /** + * The network used with this filter. + * + * @return the registered {@link Network} + */ + @NonNull + public abstract Network getNetwork(); + + /** + * Validates that conditions have not changed such that no further {@link QosSession}s should + * be passed back to the {@link QosCallback} associated to this filter. + * + * @return the error code when present, otherwise the filter is valid + * + * @hide + */ + @QosCallbackException.ExceptionType + public abstract int validate(); + + /** + * Determines whether or not the parameters is a match for the filter. + * + * @param address the local address + * @param startPort the start of the port range + * @param endPort the end of the port range + * @return whether the parameters match the local address of the filter + */ + public abstract boolean matchesLocalAddress(@NonNull InetAddress address, + int startPort, int endPort); + + /** + * Determines whether or not the parameters is a match for the filter. + * + * @param address the remote address + * @param startPort the start of the port range + * @param endPort the end of the port range + * @return whether the parameters match the remote address of the filter + */ + public abstract boolean matchesRemoteAddress(@NonNull InetAddress address, + int startPort, int endPort); +} + diff --git a/framework/src/android/net/QosFilterParcelable.java b/framework/src/android/net/QosFilterParcelable.java new file mode 100644 index 0000000000000000000000000000000000000000..da3b2cf8ff7aea64a452c49f7b0031585833065f --- /dev/null +++ b/framework/src/android/net/QosFilterParcelable.java @@ -0,0 +1,113 @@ +/* + * 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.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.util.Objects; + +/** + * Aware of how to parcel different types of {@link QosFilter}s. Any new type of qos filter must + * have a specialized case written here. + * <p/> + * Specifically leveraged when transferring {@link QosFilter} from + * {@link com.android.server.ConnectivityService} to {@link NetworkAgent} when the filter is first + * registered. + * <p/> + * This is not meant to be used in other contexts. + * + * @hide + */ +public final class QosFilterParcelable implements Parcelable { + + private static final String LOG_TAG = QosFilterParcelable.class.getSimpleName(); + + // Indicates that the filter was not successfully written to the parcel. + private static final int NO_FILTER_PRESENT = 0; + + // The parcel is of type qos socket filter. + private static final int QOS_SOCKET_FILTER = 1; + + private final QosFilter mQosFilter; + + /** + * The underlying qos filter. + * <p/> + * Null only in the case parceling failed. + */ + @Nullable + public QosFilter getQosFilter() { + return mQosFilter; + } + + public QosFilterParcelable(@NonNull final QosFilter qosFilter) { + Objects.requireNonNull(qosFilter, "qosFilter must be non-null"); + + // NOTE: Normally a type check would belong here, but doing so breaks unit tests that rely + // on mocking qos filter. + mQosFilter = qosFilter; + } + + private QosFilterParcelable(final Parcel in) { + final int filterParcelType = in.readInt(); + + switch (filterParcelType) { + case QOS_SOCKET_FILTER: { + mQosFilter = new QosSocketFilter(QosSocketInfo.CREATOR.createFromParcel(in)); + break; + } + + case NO_FILTER_PRESENT: + default: { + mQosFilter = null; + } + } + } + + public static final Creator<QosFilterParcelable> CREATOR = new Creator<QosFilterParcelable>() { + @Override + public QosFilterParcelable createFromParcel(final Parcel in) { + return new QosFilterParcelable(in); + } + + @Override + public QosFilterParcelable[] newArray(final int size) { + return new QosFilterParcelable[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) { + if (mQosFilter instanceof QosSocketFilter) { + dest.writeInt(QOS_SOCKET_FILTER); + final QosSocketFilter qosSocketFilter = (QosSocketFilter) mQosFilter; + qosSocketFilter.getQosSocketInfo().writeToParcel(dest, 0); + return; + } + dest.writeInt(NO_FILTER_PRESENT); + Log.e(LOG_TAG, "Parceling failed, unknown type of filter present: " + mQosFilter); + } +} diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java new file mode 100644 index 0000000000000000000000000000000000000000..93f2ff2bf724cb1281aaaed09e1839ecf7c4f41a --- /dev/null +++ b/framework/src/android/net/QosSession.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Provides identifying information of a QoS session. Sent to an application through + * {@link QosCallback}. + * + * @hide + */ +@SystemApi +public final class QosSession implements Parcelable { + + /** + * The {@link QosSession} is a LTE EPS Session. + */ + public static final int TYPE_EPS_BEARER = 1; + + /** + * The {@link QosSession} is a NR Session. + */ + public static final int TYPE_NR_BEARER = 2; + + private final int mSessionId; + + private final int mSessionType; + + /** + * Gets the unique id of the session that is used to differentiate sessions across different + * types. + * <p/> + * Note: Different qos sessions can be provided by different actors. + * + * @return the unique id + */ + public long getUniqueId() { + return (long) mSessionType << 32 | mSessionId; + } + + /** + * Gets the session id that is unique within that type. + * <p/> + * Note: The session id is set by the actor providing the qos. It can be either manufactured by + * the actor, but also may have a particular meaning within that type. For example, using the + * bearer id as the session id for {@link android.telephony.data.EpsBearerQosSessionAttributes} + * is a straight forward way to keep the sessions unique from one another within that type. + * + * @return the id of the session + */ + public int getSessionId() { + return mSessionId; + } + + /** + * Gets the type of session. + */ + @QosSessionType + public int getSessionType() { + return mSessionType; + } + + /** + * Creates a {@link QosSession}. + * + * @param sessionId uniquely identifies the session across all sessions of the same type + * @param sessionType the type of session + */ + public QosSession(final int sessionId, @QosSessionType final int sessionType) { + //Ensures the session id is unique across types of sessions + mSessionId = sessionId; + mSessionType = sessionType; + } + + + @Override + public String toString() { + return "QosSession{" + + "mSessionId=" + mSessionId + + ", mSessionType=" + mSessionType + + '}'; + } + + /** + * Annotations for types of qos sessions. + */ + @IntDef(value = { + TYPE_EPS_BEARER, + TYPE_NR_BEARER, + }) + @interface QosSessionType {} + + private QosSession(final Parcel in) { + mSessionId = in.readInt(); + mSessionType = in.readInt(); + } + + @NonNull + public static final Creator<QosSession> CREATOR = new Creator<QosSession>() { + @NonNull + @Override + public QosSession createFromParcel(@NonNull final Parcel in) { + return new QosSession(in); + } + + @NonNull + @Override + public QosSession[] newArray(final int size) { + return new QosSession[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull final Parcel dest, final int flags) { + dest.writeInt(mSessionId); + dest.writeInt(mSessionType); + } +} diff --git a/framework/src/android/net/QosSessionAttributes.java b/framework/src/android/net/QosSessionAttributes.java new file mode 100644 index 0000000000000000000000000000000000000000..7a885942d1b5ce5d54242ce79a10c8d64cf015a7 --- /dev/null +++ b/framework/src/android/net/QosSessionAttributes.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.SystemApi; + +/** + * Implemented by classes that encapsulate Qos related attributes that describe a Qos Session. + * + * Use the instanceof keyword to determine the underlying type. + * + * @hide + */ +@SystemApi +public interface QosSessionAttributes { +} diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..69da7f4401853e8aa11e7b3053576354b38af68c --- /dev/null +++ b/framework/src/android/net/QosSocketFilter.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.net.QosCallbackException.EX_TYPE_FILTER_NONE; +import static android.net.QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.Objects; + +/** + * Filters a {@link QosSession} according to the binding on the provided {@link Socket}. + * + * @hide + */ +public class QosSocketFilter extends QosFilter { + + private static final String TAG = QosSocketFilter.class.getSimpleName(); + + @NonNull + private final QosSocketInfo mQosSocketInfo; + + /** + * Creates a {@link QosSocketFilter} based off of {@link QosSocketInfo}. + * + * @param qosSocketInfo the information required to filter and validate + */ + public QosSocketFilter(@NonNull final QosSocketInfo qosSocketInfo) { + Objects.requireNonNull(qosSocketInfo, "qosSocketInfo must be non-null"); + mQosSocketInfo = qosSocketInfo; + } + + /** + * Gets the parcelable qos socket info that was used to create the filter. + */ + @NonNull + public QosSocketInfo getQosSocketInfo() { + return mQosSocketInfo; + } + + /** + * Performs two validations: + * 1. If the socket is not bound, then return + * {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND}. This is detected + * by checking the local address on the filter which becomes null when the socket is no + * longer bound. + * 2. In the scenario that the socket is now bound to a different local address, which can + * happen in the case of UDP, then + * {@link QosCallbackException.EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED} is returned. + * @return validation error code + */ + @Override + public int validate() { + final InetSocketAddress sa = getAddressFromFileDescriptor(); + if (sa == null) { + return QosCallbackException.EX_TYPE_FILTER_SOCKET_NOT_BOUND; + } + + if (!sa.equals(mQosSocketInfo.getLocalSocketAddress())) { + return EX_TYPE_FILTER_SOCKET_LOCAL_ADDRESS_CHANGED; + } + + return EX_TYPE_FILTER_NONE; + } + + /** + * The local address of the socket's binding. + * + * Note: If the socket is no longer bound, null is returned. + * + * @return the local address + */ + @Nullable + private InetSocketAddress getAddressFromFileDescriptor() { + final ParcelFileDescriptor parcelFileDescriptor = mQosSocketInfo.getParcelFileDescriptor(); + if (parcelFileDescriptor == null) return null; + + final FileDescriptor fd = parcelFileDescriptor.getFileDescriptor(); + if (fd == null) return null; + + final SocketAddress address; + try { + address = Os.getsockname(fd); + } catch (final ErrnoException e) { + Log.e(TAG, "getAddressFromFileDescriptor: getLocalAddress exception", e); + return null; + } + if (address instanceof InetSocketAddress) { + return (InetSocketAddress) address; + } + return null; + } + + /** + * The network used with this filter. + * + * @return the registered {@link Network} + */ + @NonNull + @Override + public Network getNetwork() { + return mQosSocketInfo.getNetwork(); + } + + /** + * @inheritDoc + */ + @Override + public boolean matchesLocalAddress(@NonNull final InetAddress address, final int startPort, + final int endPort) { + if (mQosSocketInfo.getLocalSocketAddress() == null) { + return false; + } + return matchesAddress(mQosSocketInfo.getLocalSocketAddress(), address, startPort, + endPort); + } + + /** + * @inheritDoc + */ + @Override + public boolean matchesRemoteAddress(@NonNull final InetAddress address, final int startPort, + final int endPort) { + if (mQosSocketInfo.getRemoteSocketAddress() == null) { + return false; + } + return matchesAddress(mQosSocketInfo.getRemoteSocketAddress(), address, startPort, + endPort); + } + + /** + * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)} + * and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the + * filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}. + * <p> + * This method exists for testing purposes since {@link QosSocketInfo} couldn't be mocked + * due to being final. + * + * @param filterSocketAddress the socket address of the filter + * @param address the address to compare the filterSocketAddressWith + * @param startPort the start of the port range to check + * @param endPort the end of the port range to check + */ + @VisibleForTesting + public static boolean matchesAddress(@NonNull final InetSocketAddress filterSocketAddress, + @NonNull final InetAddress address, + final int startPort, final int endPort) { + return startPort <= filterSocketAddress.getPort() + && endPort >= filterSocketAddress.getPort() + && filterSocketAddress.getAddress().equals(address); + } +} diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..a45d5075d6c77bf6af984af80a2422bd5a9f1278 --- /dev/null +++ b/framework/src/android/net/QosSocketInfo.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.ParcelFileDescriptor; +import android.os.Parcelable; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Objects; + +/** + * Used in conjunction with + * {@link ConnectivityManager#registerQosCallback} + * in order to receive Qos Sessions related to the local address and port of a bound {@link Socket} + * and/or remote address and port of a connected {@link Socket}. + * + * @hide + */ +@SystemApi +public final class QosSocketInfo implements Parcelable { + + @NonNull + private final Network mNetwork; + + @NonNull + private final ParcelFileDescriptor mParcelFileDescriptor; + + @NonNull + private final InetSocketAddress mLocalSocketAddress; + + @Nullable + private final InetSocketAddress mRemoteSocketAddress; + + /** + * The {@link Network} the socket is on. + * + * @return the registered {@link Network} + */ + @NonNull + public Network getNetwork() { + return mNetwork; + } + + /** + * The parcel file descriptor wrapped around the socket's file descriptor. + * + * @return the parcel file descriptor of the socket + */ + @NonNull + ParcelFileDescriptor getParcelFileDescriptor() { + return mParcelFileDescriptor; + } + + /** + * The local address of the socket passed into {@link QosSocketInfo(Network, Socket)}. + * The value does not reflect any changes that occur to the socket after it is first set + * in the constructor. + * + * @return the local address of the socket + */ + @NonNull + public InetSocketAddress getLocalSocketAddress() { + return mLocalSocketAddress; + } + + /** + * The remote address of the socket passed into {@link QosSocketInfo(Network, Socket)}. + * The value does not reflect any changes that occur to the socket after it is first set + * in the constructor. + * + * @return the remote address of the socket if socket is connected, null otherwise + */ + @Nullable + public InetSocketAddress getRemoteSocketAddress() { + return mRemoteSocketAddress; + } + + /** + * Creates a {@link QosSocketInfo} given a {@link Network} and bound {@link Socket}. The + * {@link Socket} must remain bound in order to receive {@link QosSession}s. + * + * @param network the network + * @param socket the bound {@link Socket} + */ + public QosSocketInfo(@NonNull final Network network, @NonNull final Socket socket) + throws IOException { + Objects.requireNonNull(socket, "socket cannot be null"); + + mNetwork = Objects.requireNonNull(network, "network cannot be null"); + mParcelFileDescriptor = ParcelFileDescriptor.fromSocket(socket); + mLocalSocketAddress = + new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort()); + + if (socket.isConnected()) { + mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); + } else { + mRemoteSocketAddress = null; + } + } + + /* Parcelable methods */ + private QosSocketInfo(final Parcel in) { + mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in)); + mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in); + + final int localAddressLength = in.readInt(); + mLocalSocketAddress = readSocketAddress(in, localAddressLength); + + final int remoteAddressLength = in.readInt(); + mRemoteSocketAddress = remoteAddressLength == 0 ? null + : readSocketAddress(in, remoteAddressLength); + } + + private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) { + final byte[] address = new byte[addressLength]; + in.readByteArray(address); + final int port = in.readInt(); + + try { + return new InetSocketAddress(InetAddress.getByAddress(address), port); + } catch (final UnknownHostException e) { + /* This can never happen. UnknownHostException will never be thrown + since the address provided is numeric and non-null. */ + throw new RuntimeException("UnknownHostException on numeric address", e); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull final Parcel dest, final int flags) { + mNetwork.writeToParcel(dest, 0); + mParcelFileDescriptor.writeToParcel(dest, 0); + + final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress(); + dest.writeInt(localAddress.length); + dest.writeByteArray(localAddress); + dest.writeInt(mLocalSocketAddress.getPort()); + + if (mRemoteSocketAddress == null) { + dest.writeInt(0); + } else { + final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress(); + dest.writeInt(remoteAddress.length); + dest.writeByteArray(remoteAddress); + dest.writeInt(mRemoteSocketAddress.getPort()); + } + } + + @NonNull + public static final Parcelable.Creator<QosSocketInfo> CREATOR = + new Parcelable.Creator<QosSocketInfo>() { + @NonNull + @Override + public QosSocketInfo createFromParcel(final Parcel in) { + return new QosSocketInfo(in); + } + + @NonNull + @Override + public QosSocketInfo[] newArray(final int size) { + return new QosSocketInfo[size]; + } + }; +} diff --git a/framework/src/android/net/RouteInfo.java b/framework/src/android/net/RouteInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..fad3144a4b80a87076550f2e6631d432e07a8524 --- /dev/null +++ b/framework/src/android/net/RouteInfo.java @@ -0,0 +1,659 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.net.module.util.NetUtils; +import com.android.net.module.util.NetworkStackConstants; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.Objects; + +/** + * Represents a network route. + * <p> + * This is used both to describe static network configuration and live network + * configuration information. + * + * A route contains three pieces of information: + * <ul> + * <li>a destination {@link IpPrefix} specifying the network destinations covered by this route. + * If this is {@code null} it indicates a default route of the address family (IPv4 or IPv6) + * implied by the gateway IP address. + * <li>a gateway {@link InetAddress} indicating the next hop to use. If this is {@code null} it + * indicates a directly-connected route. + * <li>an interface (which may be unspecified). + * </ul> + * Either the destination or the gateway may be {@code null}, but not both. If the + * destination and gateway are both specified, they must be of the same address family + * (IPv4 or IPv6). + */ +public final class RouteInfo implements Parcelable { + /** @hide */ + @IntDef(value = { + RTN_UNICAST, + RTN_UNREACHABLE, + RTN_THROW, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RouteType {} + + /** + * The IP destination address for this route. + */ + @NonNull + private final IpPrefix mDestination; + + /** + * The gateway address for this route. + */ + @UnsupportedAppUsage + @Nullable + private final InetAddress mGateway; + + /** + * The interface for this route. + */ + @Nullable + private final String mInterface; + + + /** Unicast route. @hide */ + @SystemApi + public static final int RTN_UNICAST = 1; + + /** Unreachable route. @hide */ + @SystemApi + public static final int RTN_UNREACHABLE = 7; + + /** Throw route. @hide */ + @SystemApi + public static final int RTN_THROW = 9; + + /** + * The type of this route; one of the RTN_xxx constants above. + */ + private final int mType; + + /** + * The maximum transmission unit size for this route. + */ + private final int mMtu; + + // Derived data members. + // TODO: remove these. + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private final boolean mIsHost; + private final boolean mHasGateway; + + /** + * Constructs a RouteInfo object. + * + * If destination is null, then gateway must be specified and the + * constructed route is either the IPv4 default route <code>0.0.0.0</code> + * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default + * route <code>::/0</code> if gateway is an instance of + * {@link Inet6Address}. + * <p> + * destination and gateway may not both be null. + * + * @param destination the destination prefix + * @param gateway the IP address to route packets through + * @param iface the interface name to send packets on + * @param type the type of this route + * + * @hide + */ + @SystemApi + public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway, + @Nullable String iface, @RouteType int type) { + this(destination, gateway, iface, type, 0); + } + + /** + * Constructs a RouteInfo object. + * + * If destination is null, then gateway must be specified and the + * constructed route is either the IPv4 default route <code>0.0.0.0</code> + * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default + * route <code>::/0</code> if gateway is an instance of + * {@link Inet6Address}. + * <p> + * destination and gateway may not both be null. + * + * @param destination the destination prefix + * @param gateway the IP address to route packets through + * @param iface the interface name to send packets on + * @param type the type of this route + * @param mtu the maximum transmission unit size for this route + * + * @hide + */ + @SystemApi + public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway, + @Nullable String iface, @RouteType int type, int mtu) { + switch (type) { + case RTN_UNICAST: + case RTN_UNREACHABLE: + case RTN_THROW: + // TODO: It would be nice to ensure that route types that don't have nexthops or + // interfaces, such as unreachable or throw, can't be created if an interface or + // a gateway is specified. This is a bit too complicated to do at the moment + // because: + // + // - LinkProperties sets the interface on routes added to it, and modifies the + // interfaces of all the routes when its interface name changes. + // - Even when the gateway is null, we store a non-null gateway here. + // + // For now, we just rely on the code that sets routes to do things properly. + break; + default: + throw new IllegalArgumentException("Unknown route type " + type); + } + + if (destination == null) { + if (gateway != null) { + if (gateway instanceof Inet4Address) { + destination = new IpPrefix(NetworkStackConstants.IPV4_ADDR_ANY, 0); + } else { + destination = new IpPrefix(NetworkStackConstants.IPV6_ADDR_ANY, 0); + } + } else { + // no destination, no gateway. invalid. + throw new IllegalArgumentException("Invalid arguments passed in: " + gateway + "," + + destination); + } + } + // TODO: set mGateway to null if there is no gateway. This is more correct, saves space, and + // matches the documented behaviour. Before we can do this we need to fix all callers (e.g., + // ConnectivityService) to stop doing things like r.getGateway().equals(), ... . + if (gateway == null) { + if (destination.getAddress() instanceof Inet4Address) { + gateway = NetworkStackConstants.IPV4_ADDR_ANY; + } else { + gateway = NetworkStackConstants.IPV6_ADDR_ANY; + } + } + mHasGateway = (!gateway.isAnyLocalAddress()); + + if ((destination.getAddress() instanceof Inet4Address + && !(gateway instanceof Inet4Address)) + || (destination.getAddress() instanceof Inet6Address + && !(gateway instanceof Inet6Address))) { + throw new IllegalArgumentException("address family mismatch in RouteInfo constructor"); + } + mDestination = destination; // IpPrefix objects are immutable. + mGateway = gateway; // InetAddress objects are immutable. + mInterface = iface; // Strings are immutable. + mType = type; + mIsHost = isHost(); + mMtu = mtu; + } + + /** + * Constructs a {@code RouteInfo} object. + * + * If destination is null, then gateway must be specified and the + * constructed route is either the IPv4 default route <code>0.0.0.0</code> + * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default + * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}. + * <p> + * Destination and gateway may not both be null. + * + * @param destination the destination address and prefix in an {@link IpPrefix} + * @param gateway the {@link InetAddress} to route packets through + * @param iface the interface name to send packets on + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway, + @Nullable String iface) { + this(destination, gateway, iface, RTN_UNICAST); + } + + /** + * @hide + */ + @UnsupportedAppUsage + public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway, + @Nullable String iface) { + this(destination == null ? null : + new IpPrefix(destination.getAddress(), destination.getPrefixLength()), + gateway, iface); + } + + /** + * Constructs a {@code RouteInfo} object. + * + * If destination is null, then gateway must be specified and the + * constructed route is either the IPv4 default route <code>0.0.0.0</code> + * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default + * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}. + * <p> + * Destination and gateway may not both be null. + * + * @param destination the destination address and prefix in an {@link IpPrefix} + * @param gateway the {@link InetAddress} to route packets through + * + * @hide + */ + public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway) { + this(destination, gateway, null); + } + + /** + * @hide + * + * TODO: Remove this. + */ + @UnsupportedAppUsage + public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway) { + this(destination, gateway, null); + } + + /** + * Constructs a default {@code RouteInfo} object. + * + * @param gateway the {@link InetAddress} to route packets through + * + * @hide + */ + @UnsupportedAppUsage + public RouteInfo(@NonNull InetAddress gateway) { + this((IpPrefix) null, gateway, null); + } + + /** + * Constructs a {@code RouteInfo} object representing a direct connected subnet. + * + * @param destination the {@link IpPrefix} describing the address and prefix + * length of the subnet. + * + * @hide + */ + public RouteInfo(@NonNull IpPrefix destination) { + this(destination, null, null); + } + + /** + * @hide + */ + public RouteInfo(@NonNull LinkAddress destination) { + this(destination, null, null); + } + + /** + * @hide + */ + public RouteInfo(@NonNull IpPrefix destination, @RouteType int type) { + this(destination, null, null, type); + } + + /** + * @hide + */ + public static RouteInfo makeHostRoute(@NonNull InetAddress host, @Nullable String iface) { + return makeHostRoute(host, null, iface); + } + + /** + * @hide + */ + public static RouteInfo makeHostRoute(@Nullable InetAddress host, @Nullable InetAddress gateway, + @Nullable String iface) { + if (host == null) return null; + + if (host instanceof Inet4Address) { + return new RouteInfo(new IpPrefix(host, 32), gateway, iface); + } else { + return new RouteInfo(new IpPrefix(host, 128), gateway, iface); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private boolean isHost() { + return (mDestination.getAddress() instanceof Inet4Address && + mDestination.getPrefixLength() == 32) || + (mDestination.getAddress() instanceof Inet6Address && + mDestination.getPrefixLength() == 128); + } + + /** + * Retrieves the destination address and prefix length in the form of an {@link IpPrefix}. + * + * @return {@link IpPrefix} specifying the destination. This is never {@code null}. + */ + @NonNull + public IpPrefix getDestination() { + return mDestination; + } + + /** + * TODO: Convert callers to use IpPrefix and then remove. + * @hide + */ + @NonNull + public LinkAddress getDestinationLinkAddress() { + return new LinkAddress(mDestination.getAddress(), mDestination.getPrefixLength()); + } + + /** + * Retrieves the gateway or next hop {@link InetAddress} for this route. + * + * @return {@link InetAddress} specifying the gateway or next hop. This may be + * {@code null} for a directly-connected route." + */ + @Nullable + public InetAddress getGateway() { + return mGateway; + } + + /** + * Retrieves the interface used for this route if specified, else {@code null}. + * + * @return The name of the interface used for this route. + */ + @Nullable + public String getInterface() { + return mInterface; + } + + /** + * Retrieves the type of this route. + * + * @return The type of this route; one of the {@code RTN_xxx} constants defined in this class. + * + * @hide + */ + @SystemApi + @RouteType + public int getType() { + return mType; + } + + /** + * Retrieves the MTU size for this route. + * + * @return The MTU size, or 0 if it has not been set. + * @hide + */ + @SystemApi + public int getMtu() { + return mMtu; + } + + /** + * Indicates if this route is a default route (ie, has no destination specified). + * + * @return {@code true} if the destination has a prefix length of 0. + */ + public boolean isDefaultRoute() { + return mType == RTN_UNICAST && mDestination.getPrefixLength() == 0; + } + + /** + * Indicates if this route is an unreachable default route. + * + * @return {@code true} if it's an unreachable route with prefix length of 0. + * @hide + */ + private boolean isUnreachableDefaultRoute() { + return mType == RTN_UNREACHABLE && mDestination.getPrefixLength() == 0; + } + + /** + * Indicates if this route is an IPv4 default route. + * @hide + */ + public boolean isIPv4Default() { + return isDefaultRoute() && mDestination.getAddress() instanceof Inet4Address; + } + + /** + * Indicates if this route is an IPv4 unreachable default route. + * @hide + */ + public boolean isIPv4UnreachableDefault() { + return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet4Address; + } + + /** + * Indicates if this route is an IPv6 default route. + * @hide + */ + public boolean isIPv6Default() { + return isDefaultRoute() && mDestination.getAddress() instanceof Inet6Address; + } + + /** + * Indicates if this route is an IPv6 unreachable default route. + * @hide + */ + public boolean isIPv6UnreachableDefault() { + return isUnreachableDefaultRoute() && mDestination.getAddress() instanceof Inet6Address; + } + + /** + * Indicates if this route is a host route (ie, matches only a single host address). + * + * @return {@code true} if the destination has a prefix length of 32 or 128 for IPv4 or IPv6, + * respectively. + * @hide + */ + public boolean isHostRoute() { + return mIsHost; + } + + /** + * Indicates if this route has a next hop ({@code true}) or is directly-connected + * ({@code false}). + * + * @return {@code true} if a gateway is specified + */ + public boolean hasGateway() { + return mHasGateway; + } + + /** + * Determines whether the destination and prefix of this route includes the specified + * address. + * + * @param destination A {@link InetAddress} to test to see if it would match this route. + * @return {@code true} if the destination and prefix length cover the given address. + */ + public boolean matches(InetAddress destination) { + return mDestination.contains(destination); + } + + /** + * Find the route from a Collection of routes that best matches a given address. + * May return null if no routes are applicable. + * @param routes a Collection of RouteInfos to chose from + * @param dest the InetAddress your trying to get to + * @return the RouteInfo from the Collection that best fits the given address + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Nullable + public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) { + return NetUtils.selectBestRoute(routes, dest); + } + + /** + * Returns a human-readable description of this object. + */ + public String toString() { + String val = ""; + if (mDestination != null) val = mDestination.toString(); + if (mType == RTN_UNREACHABLE) { + val += " unreachable"; + } else if (mType == RTN_THROW) { + val += " throw"; + } else { + val += " ->"; + if (mGateway != null) val += " " + mGateway.getHostAddress(); + if (mInterface != null) val += " " + mInterface; + if (mType != RTN_UNICAST) { + val += " unknown type " + mType; + } + } + val += " mtu " + mMtu; + return val; + } + + /** + * Compares this RouteInfo object against the specified object and indicates if they are equal. + * @return {@code true} if the objects are equal, {@code false} otherwise. + */ + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + + if (!(obj instanceof RouteInfo)) return false; + + RouteInfo target = (RouteInfo) obj; + + return Objects.equals(mDestination, target.getDestination()) && + Objects.equals(mGateway, target.getGateway()) && + Objects.equals(mInterface, target.getInterface()) && + mType == target.getType() && mMtu == target.getMtu(); + } + + /** + * A helper class that contains the destination, the gateway and the interface in a + * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or + * {@link LinkProperties#addRoute} to calculate the list to be updated. + * {@code RouteInfo} objects with different interfaces are treated as different routes because + * *usually* on Android different interfaces use different routing tables, and moving a route + * to a new routing table never constitutes an update, but is always a remove and an add. + * + * @hide + */ + public static class RouteKey { + @NonNull private final IpPrefix mDestination; + @Nullable private final InetAddress mGateway; + @Nullable private final String mInterface; + + RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway, + @Nullable String iface) { + mDestination = destination; + mGateway = gateway; + mInterface = iface; + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof RouteKey)) { + return false; + } + RouteKey p = (RouteKey) o; + // No need to do anything special for scoped addresses. Inet6Address#equals does not + // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel) + // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only + // look at RTA_OIF. + return Objects.equals(p.mDestination, mDestination) + && Objects.equals(p.mGateway, mGateway) + && Objects.equals(p.mInterface, mInterface); + } + + @Override + public int hashCode() { + return Objects.hash(mDestination, mGateway, mInterface); + } + } + + /** + * Get {@code RouteKey} of this {@code RouteInfo}. + * @return a {@code RouteKey} object. + * + * @hide + */ + @NonNull + public RouteKey getRouteKey() { + return new RouteKey(mDestination, mGateway, mInterface); + } + + /** + * Returns a hashcode for this <code>RouteInfo</code> object. + */ + public int hashCode() { + return (mDestination.hashCode() * 41) + + (mGateway == null ? 0 :mGateway.hashCode() * 47) + + (mInterface == null ? 0 :mInterface.hashCode() * 67) + + (mType * 71) + (mMtu * 89); + } + + /** + * Implement the Parcelable interface + */ + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mDestination, flags); + byte[] gatewayBytes = (mGateway == null) ? null : mGateway.getAddress(); + dest.writeByteArray(gatewayBytes); + dest.writeString(mInterface); + dest.writeInt(mType); + dest.writeInt(mMtu); + } + + /** + * Implement the Parcelable interface. + */ + public static final @android.annotation.NonNull Creator<RouteInfo> CREATOR = + new Creator<RouteInfo>() { + public RouteInfo createFromParcel(Parcel in) { + IpPrefix dest = in.readParcelable(null); + + InetAddress gateway = null; + byte[] addr = in.createByteArray(); + try { + gateway = InetAddress.getByAddress(addr); + } catch (UnknownHostException e) {} + + String iface = in.readString(); + int type = in.readInt(); + int mtu = in.readInt(); + + return new RouteInfo(dest, gateway, iface, type, mtu); + } + + public RouteInfo[] newArray(int size) { + return new RouteInfo[size]; + } + }; +} diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java new file mode 100644 index 0000000000000000000000000000000000000000..f6cae725160917e9c0f8f5c22c475ce17b63e700 --- /dev/null +++ b/framework/src/android/net/SocketKeepalive.java @@ -0,0 +1,347 @@ +/* + * 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.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; + +/** + * Allows applications to request that the system periodically send specific packets on their + * behalf, using hardware offload to save battery power. + * + * To request that the system send keepalives, call one of the methods that return a + * {@link SocketKeepalive} object, such as {@link ConnectivityManager#createSocketKeepalive}, + * passing in a non-null callback. If the {@link SocketKeepalive} is successfully + * started, the callback's {@code onStarted} method will be called. If an error occurs, + * {@code onError} will be called, specifying one of the {@code ERROR_*} constants in this + * class. + * + * To stop an existing keepalive, call {@link SocketKeepalive#stop}. The system will call + * {@link SocketKeepalive.Callback#onStopped} if the operation was successful or + * {@link SocketKeepalive.Callback#onError} if an error occurred. + * + * For cellular, the device MUST support at least 1 keepalive slot. + * + * For WiFi, the device SHOULD support keepalive offload. If it does not, it MUST reply with + * {@link SocketKeepalive.Callback#onError} with {@code ERROR_UNSUPPORTED} to any keepalive offload + * request. If it does, it MUST support at least 3 concurrent keepalive slots. + */ +public abstract class SocketKeepalive implements AutoCloseable { + static final String TAG = "SocketKeepalive"; + + /** + * Success. It indicates there is no error. + * @hide + */ + @SystemApi + public static final int SUCCESS = 0; + + /** + * No keepalive. This should only be internally as it indicates There is no keepalive. + * It should not propagate to applications. + * @hide + */ + public static final int NO_KEEPALIVE = -1; + + /** + * Data received. + * @hide + */ + public static final int DATA_RECEIVED = -2; + + /** + * The binder died. + * @hide + */ + public static final int BINDER_DIED = -10; + + /** + * The invalid network. It indicates the specified {@code Network} is not connected. + */ + public static final int ERROR_INVALID_NETWORK = -20; + + /** + * The invalid IP addresses. Indicates the specified IP addresses are invalid. + * For example, the specified source IP address is not configured on the + * specified {@code Network}. + */ + public static final int ERROR_INVALID_IP_ADDRESS = -21; + + /** + * The port is invalid. + */ + public static final int ERROR_INVALID_PORT = -22; + + /** + * The length is invalid (e.g. too long). + */ + public static final int ERROR_INVALID_LENGTH = -23; + + /** + * The interval is invalid (e.g. too short). + */ + public static final int ERROR_INVALID_INTERVAL = -24; + + /** + * The socket is invalid. + */ + public static final int ERROR_INVALID_SOCKET = -25; + + /** + * The socket is not idle. + */ + public static final int ERROR_SOCKET_NOT_IDLE = -26; + + /** + * The stop reason is uninitialized. This should only be internally used as initial state + * of stop reason, instead of propagating to application. + * @hide + */ + public static final int ERROR_STOP_REASON_UNINITIALIZED = -27; + + /** + * The request is unsupported. + */ + public static final int ERROR_UNSUPPORTED = -30; + + /** + * There was a hardware error. + */ + public static final int ERROR_HARDWARE_ERROR = -31; + + /** + * Resources are insufficient (e.g. all hardware slots are in use). + */ + public static final int ERROR_INSUFFICIENT_RESOURCES = -32; + + /** + * There was no such slot. This should only be internally as it indicates + * a programming error in the system server. It should not propagate to + * applications. + * @hide + */ + @SystemApi + public static final int ERROR_NO_SUCH_SLOT = -33; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ERROR_" }, value = { + ERROR_INVALID_NETWORK, + ERROR_INVALID_IP_ADDRESS, + ERROR_INVALID_PORT, + ERROR_INVALID_LENGTH, + ERROR_INVALID_INTERVAL, + ERROR_INVALID_SOCKET, + ERROR_SOCKET_NOT_IDLE, + ERROR_NO_SUCH_SLOT + }) + public @interface ErrorCode {} + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + SUCCESS, + ERROR_INVALID_LENGTH, + ERROR_UNSUPPORTED, + ERROR_INSUFFICIENT_RESOURCES, + }) + public @interface KeepaliveEvent {} + + /** + * The minimum interval in seconds between keepalive packet transmissions. + * + * @hide + **/ + public static final int MIN_INTERVAL_SEC = 10; + + /** + * The maximum interval in seconds between keepalive packet transmissions. + * + * @hide + **/ + public static final int MAX_INTERVAL_SEC = 3600; + + /** + * An exception that embarks an error code. + * @hide + */ + public static class ErrorCodeException extends Exception { + public final int error; + public ErrorCodeException(final int error, final Throwable e) { + super(e); + this.error = error; + } + public ErrorCodeException(final int error) { + this.error = error; + } + } + + /** + * This socket is invalid. + * See the error code for details, and the optional cause. + * @hide + */ + public static class InvalidSocketException extends ErrorCodeException { + public InvalidSocketException(final int error, final Throwable e) { + super(error, e); + } + public InvalidSocketException(final int error) { + super(error); + } + } + + @NonNull final IConnectivityManager mService; + @NonNull final Network mNetwork; + @NonNull final ParcelFileDescriptor mPfd; + @NonNull final Executor mExecutor; + @NonNull final ISocketKeepaliveCallback mCallback; + // TODO: remove slot since mCallback could be used to identify which keepalive to stop. + @Nullable Integer mSlot; + + SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network, + @NonNull ParcelFileDescriptor pfd, + @NonNull Executor executor, @NonNull Callback callback) { + mService = service; + mNetwork = network; + mPfd = pfd; + mExecutor = executor; + mCallback = new ISocketKeepaliveCallback.Stub() { + @Override + public void onStarted(int slot) { + final long token = Binder.clearCallingIdentity(); + try { + mExecutor.execute(() -> { + mSlot = slot; + callback.onStarted(); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onStopped() { + final long token = Binder.clearCallingIdentity(); + try { + executor.execute(() -> { + mSlot = null; + callback.onStopped(); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onError(int error) { + final long token = Binder.clearCallingIdentity(); + try { + executor.execute(() -> { + mSlot = null; + callback.onError(error); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onDataReceived() { + final long token = Binder.clearCallingIdentity(); + try { + executor.execute(() -> { + mSlot = null; + callback.onDataReceived(); + }); + } finally { + Binder.restoreCallingIdentity(token); + } + } + }; + } + + /** + * Request that keepalive be started with the given {@code intervalSec}. See + * {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an exception + * when invoking start or stop of the {@link SocketKeepalive}, a {@link RemoteException} will be + * thrown into the {@code executor}. This is typically not important to catch because the remote + * party is the system, so if it is not in shape to communicate through binder the system is + * probably going down anyway. If the caller cares regardless, it can use a custom + * {@link Executor} to catch the {@link RemoteException}. + * + * @param intervalSec The target interval in seconds between keepalive packet transmissions. + * The interval should be between 10 seconds and 3600 seconds, otherwise + * {@link #ERROR_INVALID_INTERVAL} will be returned. + */ + public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC) + int intervalSec) { + startImpl(intervalSec); + } + + abstract void startImpl(int intervalSec); + + /** + * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped} + * before using the object. See {@link SocketKeepalive}. + */ + public final void stop() { + stopImpl(); + } + + abstract void stopImpl(); + + /** + * Deactivate this {@link SocketKeepalive} and free allocated resources. The instance won't be + * usable again if {@code close()} is called. + */ + @Override + public final void close() { + stop(); + try { + mPfd.close(); + } catch (IOException e) { + // Nothing much can be done. + } + } + + /** + * The callback which app can use to learn the status changes of {@link SocketKeepalive}. See + * {@link SocketKeepalive}. + */ + public static class Callback { + /** The requested keepalive was successfully started. */ + public void onStarted() {} + /** The keepalive was successfully stopped. */ + public void onStopped() {} + /** An error occurred. */ + public void onError(@ErrorCode int error) {} + /** The keepalive on a TCP socket was stopped because the socket received data. This is + * never called for UDP sockets. */ + public void onDataReceived() {} + } +} diff --git a/framework/src/android/net/SocketLocalAddressChangedException.java b/framework/src/android/net/SocketLocalAddressChangedException.java new file mode 100644 index 0000000000000000000000000000000000000000..9daad83fd13e8f6270b6d8fde591c92f7bc5d042 --- /dev/null +++ b/framework/src/android/net/SocketLocalAddressChangedException.java @@ -0,0 +1,32 @@ +/* + * 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.SystemApi; + +/** + * Thrown when the local address of the socket has changed. + * + * @hide + */ +@SystemApi +public class SocketLocalAddressChangedException extends Exception { + /** @hide */ + public SocketLocalAddressChangedException() { + super("The local address of the socket changed"); + } +} diff --git a/framework/src/android/net/SocketNotBoundException.java b/framework/src/android/net/SocketNotBoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..b1d7026ac981775a382601affcf8c441589a18d0 --- /dev/null +++ b/framework/src/android/net/SocketNotBoundException.java @@ -0,0 +1,32 @@ +/* + * 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.SystemApi; + +/** + * Thrown when a previously bound socket becomes unbound. + * + * @hide + */ +@SystemApi +public class SocketNotBoundException extends Exception { + /** @hide */ + public SocketNotBoundException() { + super("The socket is unbound"); + } +} diff --git a/framework/src/android/net/StaticIpConfiguration.java b/framework/src/android/net/StaticIpConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..7904f7a4ec17dc83d11e17359110447271aa7f1d --- /dev/null +++ b/framework/src/android/net/StaticIpConfiguration.java @@ -0,0 +1,331 @@ +/* + * 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 android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.net.module.util.InetAddressUtils; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Class that describes static IP configuration. + * + * <p>This class is different from {@link LinkProperties} because it represents + * configuration intent. The general contract is that if we can represent + * a configuration here, then we should be able to configure it on a network. + * The intent is that it closely match the UI we have for configuring networks. + * + * <p>In contrast, {@link LinkProperties} represents current state. It is much more + * expressive. For example, it supports multiple IP addresses, multiple routes, + * stacked interfaces, and so on. Because LinkProperties is so expressive, + * using it to represent configuration intent as well as current state causes + * problems. For example, we could unknowingly save a configuration that we are + * not in fact capable of applying, or we could save a configuration that the + * UI cannot display, which has the potential for malicious code to hide + * hostile or unexpected configuration from the user. + * + * @hide + */ +@SystemApi +public final class StaticIpConfiguration implements Parcelable { + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Nullable + public LinkAddress ipAddress; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Nullable + public InetAddress gateway; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @NonNull + public final ArrayList<InetAddress> dnsServers; + /** @hide */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @Nullable + public String domains; + + public StaticIpConfiguration() { + dnsServers = new ArrayList<>(); + } + + public StaticIpConfiguration(@Nullable StaticIpConfiguration source) { + this(); + if (source != null) { + // All of these except dnsServers are immutable, so no need to make copies. + ipAddress = source.ipAddress; + gateway = source.gateway; + dnsServers.addAll(source.dnsServers); + domains = source.domains; + } + } + + public void clear() { + ipAddress = null; + gateway = null; + dnsServers.clear(); + domains = null; + } + + /** + * Get the static IP address included in the configuration. + */ + public @Nullable LinkAddress getIpAddress() { + return ipAddress; + } + + /** + * Get the gateway included in the configuration. + */ + public @Nullable InetAddress getGateway() { + return gateway; + } + + /** + * Get the DNS servers included in the configuration. + */ + public @NonNull List<InetAddress> getDnsServers() { + return dnsServers; + } + + /** + * Get a {@link String} containing the comma separated domains to search when resolving host + * names on this link, in priority order. + */ + public @Nullable String getDomains() { + return domains; + } + + /** + * Helper class to build a new instance of {@link StaticIpConfiguration}. + */ + public static final class Builder { + private LinkAddress mIpAddress; + private InetAddress mGateway; + private Iterable<InetAddress> mDnsServers; + private String mDomains; + + /** + * Set the IP address to be included in the configuration; null by default. + * @return The {@link Builder} for chaining. + */ + public @NonNull Builder setIpAddress(@Nullable LinkAddress ipAddress) { + mIpAddress = ipAddress; + return this; + } + + /** + * Set the address of the gateway to be included in the configuration; null by default. + * @return The {@link Builder} for chaining. + */ + public @NonNull Builder setGateway(@Nullable InetAddress gateway) { + mGateway = gateway; + return this; + } + + /** + * Set the addresses of the DNS servers included in the configuration; empty by default. + * @return The {@link Builder} for chaining. + */ + public @NonNull Builder setDnsServers(@NonNull Iterable<InetAddress> dnsServers) { + Objects.requireNonNull(dnsServers); + mDnsServers = dnsServers; + return this; + } + + /** + * Sets the DNS domain search path to be used on the link; null by default. + * @param newDomains A {@link String} containing the comma separated domains to search when + * resolving host names on this link, in priority order. + * @return The {@link Builder} for chaining. + */ + public @NonNull Builder setDomains(@Nullable String newDomains) { + mDomains = newDomains; + return this; + } + + /** + * Create a {@link StaticIpConfiguration} from the parameters in this {@link Builder}. + * @return The newly created StaticIpConfiguration. + */ + public @NonNull StaticIpConfiguration build() { + final StaticIpConfiguration config = new StaticIpConfiguration(); + config.ipAddress = mIpAddress; + config.gateway = mGateway; + if (mDnsServers != null) { + for (InetAddress server : mDnsServers) { + config.dnsServers.add(server); + } + } + config.domains = mDomains; + return config; + } + } + + /** + * Add a DNS server to this configuration. + */ + public void addDnsServer(@NonNull InetAddress server) { + dnsServers.add(server); + } + + /** + * Returns the network routes specified by this object. Will typically include a + * directly-connected route for the IP address's local subnet and a default route. + * @param iface Interface to include in the routes. + */ + public @NonNull List<RouteInfo> getRoutes(@Nullable String iface) { + List<RouteInfo> routes = new ArrayList<RouteInfo>(3); + if (ipAddress != null) { + RouteInfo connectedRoute = new RouteInfo(ipAddress, null, iface); + routes.add(connectedRoute); + // If the default gateway is not covered by the directly-connected route, also add a + // host route to the gateway as well. This configuration is arguably invalid, but it + // used to work in K and earlier, and other OSes appear to accept it. + if (gateway != null && !connectedRoute.matches(gateway)) { + routes.add(RouteInfo.makeHostRoute(gateway, iface)); + } + } + if (gateway != null) { + routes.add(new RouteInfo((IpPrefix) null, gateway, iface)); + } + return routes; + } + + /** + * Returns a LinkProperties object expressing the data in this object. Note that the information + * contained in the LinkProperties will not be a complete picture of the link's configuration, + * because any configuration information that is obtained dynamically by the network (e.g., + * IPv6 configuration) will not be included. + * @hide + */ + public @NonNull LinkProperties toLinkProperties(String iface) { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(iface); + if (ipAddress != null) { + lp.addLinkAddress(ipAddress); + } + for (RouteInfo route : getRoutes(iface)) { + lp.addRoute(route); + } + for (InetAddress dns : dnsServers) { + lp.addDnsServer(dns); + } + lp.setDomains(domains); + return lp; + } + + @NonNull + @Override + public String toString() { + StringBuffer str = new StringBuffer(); + + str.append("IP address "); + if (ipAddress != null ) str.append(ipAddress).append(" "); + + str.append("Gateway "); + if (gateway != null) str.append(gateway.getHostAddress()).append(" "); + + str.append(" DNS servers: ["); + for (InetAddress dnsServer : dnsServers) { + str.append(" ").append(dnsServer.getHostAddress()); + } + + str.append(" ] Domains "); + if (domains != null) str.append(domains); + return str.toString(); + } + + @Override + public int hashCode() { + int result = 13; + result = 47 * result + (ipAddress == null ? 0 : ipAddress.hashCode()); + result = 47 * result + (gateway == null ? 0 : gateway.hashCode()); + result = 47 * result + (domains == null ? 0 : domains.hashCode()); + result = 47 * result + dnsServers.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) return true; + + if (!(obj instanceof StaticIpConfiguration)) return false; + + StaticIpConfiguration other = (StaticIpConfiguration) obj; + + return other != null && + Objects.equals(ipAddress, other.ipAddress) && + Objects.equals(gateway, other.gateway) && + dnsServers.equals(other.dnsServers) && + Objects.equals(domains, other.domains); + } + + /** Implement the Parcelable interface */ + public static final @android.annotation.NonNull Creator<StaticIpConfiguration> CREATOR = + new Creator<StaticIpConfiguration>() { + public StaticIpConfiguration createFromParcel(Parcel in) { + return readFromParcel(in); + } + + public StaticIpConfiguration[] newArray(int size) { + return new StaticIpConfiguration[size]; + } + }; + + /** Implement the Parcelable interface */ + @Override + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(ipAddress, flags); + InetAddressUtils.parcelInetAddress(dest, gateway, flags); + dest.writeInt(dnsServers.size()); + for (InetAddress dnsServer : dnsServers) { + InetAddressUtils.parcelInetAddress(dest, dnsServer, flags); + } + dest.writeString(domains); + } + + /** @hide */ + public static StaticIpConfiguration readFromParcel(Parcel in) { + final StaticIpConfiguration s = new StaticIpConfiguration(); + s.ipAddress = in.readParcelable(null); + s.gateway = InetAddressUtils.unparcelInetAddress(in); + s.dnsServers.clear(); + int size = in.readInt(); + for (int i = 0; i < size; i++) { + s.dnsServers.add(InetAddressUtils.unparcelInetAddress(in)); + } + s.domains = in.readString(); + return s; + } +} diff --git a/framework/src/android/net/TcpKeepalivePacketData.java b/framework/src/android/net/TcpKeepalivePacketData.java new file mode 100644 index 0000000000000000000000000000000000000000..c2c4f32ca6738894122ba102e06d308c37a05fc9 --- /dev/null +++ b/framework/src/android/net/TcpKeepalivePacketData.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.net.InetAddress; +import java.util.Objects; + +/** + * Represents the actual tcp keep alive packets which will be used for hardware offload. + * @hide + */ +@SystemApi +public final class TcpKeepalivePacketData extends KeepalivePacketData implements Parcelable { + private static final String TAG = "TcpKeepalivePacketData"; + + /** + * TCP sequence number. + * @hide + */ + public final int tcpSeq; + + /** + * TCP ACK number. + * @hide + */ + public final int tcpAck; + + /** + * TCP RCV window. + * @hide + */ + public final int tcpWindow; + + /** TCP RCV window scale. + * @hide + */ + public final int tcpWindowScale; + + /** + * IP TOS. + * @hide + */ + public final int ipTos; + + /** + * IP TTL. + * @hide + */ + public final int ipTtl; + + public TcpKeepalivePacketData(@NonNull final InetAddress srcAddress, int srcPort, + @NonNull final InetAddress dstAddress, int dstPort, @NonNull final byte[] data, + int tcpSeq, int tcpAck, int tcpWindow, int tcpWindowScale, int ipTos, int ipTtl) + throws InvalidPacketException { + super(srcAddress, srcPort, dstAddress, dstPort, data); + this.tcpSeq = tcpSeq; + this.tcpAck = tcpAck; + this.tcpWindow = tcpWindow; + this.tcpWindowScale = tcpWindowScale; + this.ipTos = ipTos; + this.ipTtl = ipTtl; + } + + /** + * Get the TCP sequence number. + * + * See https://tools.ietf.org/html/rfc793#page-15. + */ + public int getTcpSeq() { + return tcpSeq; + } + + /** + * Get the TCP ACK number. + * + * See https://tools.ietf.org/html/rfc793#page-15. + */ + public int getTcpAck() { + return tcpAck; + } + + /** + * Get the TCP RCV window. + * + * See https://tools.ietf.org/html/rfc793#page-15. + */ + public int getTcpWindow() { + return tcpWindow; + } + + /** + * Get the TCP RCV window scale. + * + * See https://tools.ietf.org/html/rfc793#page-15. + */ + public int getTcpWindowScale() { + return tcpWindowScale; + } + + /** + * Get the IP type of service. + */ + public int getIpTos() { + return ipTos; + } + + /** + * Get the IP TTL. + */ + public int getIpTtl() { + return ipTtl; + } + + @Override + public boolean equals(@Nullable final Object o) { + if (!(o instanceof TcpKeepalivePacketData)) return false; + final TcpKeepalivePacketData other = (TcpKeepalivePacketData) o; + final InetAddress srcAddress = getSrcAddress(); + final InetAddress dstAddress = getDstAddress(); + return srcAddress.equals(other.getSrcAddress()) + && dstAddress.equals(other.getDstAddress()) + && getSrcPort() == other.getSrcPort() + && getDstPort() == other.getDstPort() + && this.tcpAck == other.tcpAck + && this.tcpSeq == other.tcpSeq + && this.tcpWindow == other.tcpWindow + && this.tcpWindowScale == other.tcpWindowScale + && this.ipTos == other.ipTos + && this.ipTtl == other.ipTtl; + } + + @Override + public int hashCode() { + return Objects.hash(getSrcAddress(), getDstAddress(), getSrcPort(), getDstPort(), + tcpAck, tcpSeq, tcpWindow, tcpWindowScale, ipTos, ipTtl); + } + + /** + * Parcelable Implementation. + * Note that this object implements parcelable (and needs to keep doing this as it inherits + * from a class that does), but should usually be parceled as a stable parcelable using + * the toStableParcelable() and fromStableParcelable() methods. + */ + @Override + public int describeContents() { + return 0; + } + + /** Write to parcel. */ + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeString(getSrcAddress().getHostAddress()); + out.writeString(getDstAddress().getHostAddress()); + out.writeInt(getSrcPort()); + out.writeInt(getDstPort()); + out.writeByteArray(getPacket()); + out.writeInt(tcpSeq); + out.writeInt(tcpAck); + out.writeInt(tcpWindow); + out.writeInt(tcpWindowScale); + out.writeInt(ipTos); + out.writeInt(ipTtl); + } + + private static TcpKeepalivePacketData readFromParcel(Parcel in) throws InvalidPacketException { + InetAddress srcAddress = InetAddresses.parseNumericAddress(in.readString()); + InetAddress dstAddress = InetAddresses.parseNumericAddress(in.readString()); + int srcPort = in.readInt(); + int dstPort = in.readInt(); + byte[] packet = in.createByteArray(); + int tcpSeq = in.readInt(); + int tcpAck = in.readInt(); + int tcpWnd = in.readInt(); + int tcpWndScale = in.readInt(); + int ipTos = in.readInt(); + int ipTtl = in.readInt(); + return new TcpKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, packet, tcpSeq, + tcpAck, tcpWnd, tcpWndScale, ipTos, ipTtl); + } + + /** Parcelable Creator. */ + public static final @NonNull Parcelable.Creator<TcpKeepalivePacketData> CREATOR = + new Parcelable.Creator<TcpKeepalivePacketData>() { + public TcpKeepalivePacketData createFromParcel(Parcel in) { + try { + return readFromParcel(in); + } catch (InvalidPacketException e) { + throw new IllegalArgumentException( + "Invalid TCP keepalive data: " + e.getError()); + } + } + + public TcpKeepalivePacketData[] newArray(int size) { + return new TcpKeepalivePacketData[size]; + } + }; + + @Override + public String toString() { + return "saddr: " + getSrcAddress() + + " daddr: " + getDstAddress() + + " sport: " + getSrcPort() + + " dport: " + getDstPort() + + " seq: " + tcpSeq + + " ack: " + tcpAck + + " window: " + tcpWindow + + " windowScale: " + tcpWindowScale + + " tos: " + ipTos + + " ttl: " + ipTtl; + } +} diff --git a/framework/src/android/net/TcpRepairWindow.java b/framework/src/android/net/TcpRepairWindow.java new file mode 100644 index 0000000000000000000000000000000000000000..86034f0a76ed232b497733763f600164dff117ab --- /dev/null +++ b/framework/src/android/net/TcpRepairWindow.java @@ -0,0 +1,45 @@ +/* + * 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; + +/** + * Corresponds to C's {@code struct tcp_repair_window} from + * include/uapi/linux/tcp.h + * + * @hide + */ +public final class TcpRepairWindow { + public final int sndWl1; + public final int sndWnd; + public final int maxWindow; + public final int rcvWnd; + public final int rcvWup; + public final int rcvWndScale; + + /** + * Constructs an instance with the given field values. + */ + public TcpRepairWindow(final int sndWl1, final int sndWnd, final int maxWindow, + final int rcvWnd, final int rcvWup, final int rcvWndScale) { + this.sndWl1 = sndWl1; + this.sndWnd = sndWnd; + this.maxWindow = maxWindow; + this.rcvWnd = rcvWnd; + this.rcvWup = rcvWup; + this.rcvWndScale = rcvWndScale; + } +} diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java new file mode 100644 index 0000000000000000000000000000000000000000..d89814d49bd0c27aaef86bfe207155435b2b987e --- /dev/null +++ b/framework/src/android/net/TcpSocketKeepalive.java @@ -0,0 +1,77 @@ +/* + * 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.annotation.NonNull; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import java.util.concurrent.Executor; + +/** @hide */ +final class TcpSocketKeepalive extends SocketKeepalive { + + TcpSocketKeepalive(@NonNull IConnectivityManager service, + @NonNull Network network, + @NonNull ParcelFileDescriptor pfd, + @NonNull Executor executor, + @NonNull Callback callback) { + super(service, network, pfd, executor, callback); + } + + /** + * Starts keepalives. {@code mSocket} must be a connected TCP socket. + * + * - The application must not write to or read from the socket after calling this method, until + * onDataReceived, onStopped, or onError are called. If it does, the keepalive will fail + * with {@link #ERROR_SOCKET_NOT_IDLE}, or {@code #ERROR_INVALID_SOCKET} if the socket + * experienced an error (as in poll(2) returned POLLERR or POLLHUP); if this happens, the data + * received from the socket may be invalid, and the socket can't be recovered. + * - If the socket has data in the send or receive buffer, then this call will fail with + * {@link #ERROR_SOCKET_NOT_IDLE} and can be retried after the data has been processed. + * An app could ensure this by using an application-layer protocol to receive acknowledgement + * that indicates all data has been delivered to server, e.g. HTTP 200 OK. + * Then the app could go into keepalive mode after reading all remaining data within the + * acknowledgement. + */ + @Override + void startImpl(int intervalSec) { + mExecutor.execute(() -> { + try { + mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error starting packet keepalive: ", e); + throw e.rethrowFromSystemServer(); + } + }); + } + + @Override + void stopImpl() { + mExecutor.execute(() -> { + try { + if (mSlot != null) { + mService.stopKeepalive(mNetwork, mSlot); + } + } catch (RemoteException e) { + Log.e(TAG, "Error stopping packet keepalive: ", e); + throw e.rethrowFromSystemServer(); + } + }); + } +} diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..4449ff80180b6b7f6eb70d0918c534d63175af67 --- /dev/null +++ b/framework/src/android/net/TestNetworkInterface.java @@ -0,0 +1,78 @@ +/* + * 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.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +/** + * This class is used to return the interface name and fd of the test interface + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class TestNetworkInterface implements Parcelable { + @NonNull + private final ParcelFileDescriptor mFileDescriptor; + @NonNull + private final String mInterfaceName; + + @Override + public int describeContents() { + return (mFileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE); + out.writeString(mInterfaceName); + } + + public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) { + mFileDescriptor = pfd; + mInterfaceName = intf; + } + + private TestNetworkInterface(@NonNull Parcel in) { + mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); + mInterfaceName = in.readString(); + } + + @NonNull + public ParcelFileDescriptor getFileDescriptor() { + return mFileDescriptor; + } + + @NonNull + public String getInterfaceName() { + return mInterfaceName; + } + + @NonNull + public static final Parcelable.Creator<TestNetworkInterface> CREATOR = + new Parcelable.Creator<TestNetworkInterface>() { + public TestNetworkInterface createFromParcel(Parcel in) { + return new TestNetworkInterface(in); + } + + public TestNetworkInterface[] newArray(int size) { + return new TestNetworkInterface[size]; + } + }; +} diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java new file mode 100644 index 0000000000000000000000000000000000000000..9ddd2f57679b94ce103e524e9d4b5fadcd22e196 --- /dev/null +++ b/framework/src/android/net/TestNetworkManager.java @@ -0,0 +1,182 @@ +/* + * 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; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.IBinder; +import android.os.RemoteException; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; + +/** + * Class that allows creation and management of per-app, test-only networks + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class TestNetworkManager { + /** + * Prefix for tun interfaces created by this class. + * @hide + */ + public static final String TEST_TUN_PREFIX = "testtun"; + + /** + * Prefix for tap interfaces created by this class. + */ + public static final String TEST_TAP_PREFIX = "testtap"; + + @NonNull private static final String TAG = TestNetworkManager.class.getSimpleName(); + + @NonNull private final ITestNetworkManager mService; + + /** @hide */ + public TestNetworkManager(@NonNull ITestNetworkManager service) { + mService = Objects.requireNonNull(service, "missing ITestNetworkManager"); + } + + /** + * Teardown the capability-limited, testing-only network for a given interface + * + * @param network The test network that should be torn down + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public void teardownTestNetwork(@NonNull Network network) { + try { + mService.teardownTestNetwork(network.netId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private void setupTestNetwork( + @NonNull String iface, + @Nullable LinkProperties lp, + boolean isMetered, + @NonNull int[] administratorUids, + @NonNull IBinder binder) { + try { + mService.setupTestNetwork(iface, lp, isMetered, administratorUids, binder); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sets up a capability-limited, testing-only network for a given interface + * + * @param lp The LinkProperties for the TestNetworkService to use for this test network. Note + * that the interface name and link addresses will be overwritten, and the passed-in values + * discarded. + * @param isMetered Whether or not the network should be considered metered. + * @param binder A binder object guarding the lifecycle of this test network. + * @hide + */ + public void setupTestNetwork( + @NonNull LinkProperties lp, boolean isMetered, @NonNull IBinder binder) { + Objects.requireNonNull(lp, "Invalid LinkProperties"); + setupTestNetwork(lp.getInterfaceName(), lp, isMetered, new int[0], binder); + } + + /** + * Sets up a capability-limited, testing-only network for a given interface + * + * @param iface the name of the interface to be used for the Network LinkProperties. + * @param binder A binder object guarding the lifecycle of this test network. + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) { + setupTestNetwork(iface, null, true, new int[0], binder); + } + + /** + * Sets up a capability-limited, testing-only network for a given interface with the given + * administrator UIDs. + * + * @param iface the name of the interface to be used for the Network LinkProperties. + * @param administratorUids The administrator UIDs to be used for the test-only network + * @param binder A binder object guarding the lifecycle of this test network. + * @hide + */ + public void setupTestNetwork( + @NonNull String iface, @NonNull int[] administratorUids, @NonNull IBinder binder) { + setupTestNetwork(iface, null, true, administratorUids, binder); + } + + /** + * Create a tun interface for testing purposes + * + * @param linkAddrs an array of LinkAddresses to assign to the TUN interface + * @return A ParcelFileDescriptor of the underlying TUN interface. Close this to tear down the + * TUN interface. + * @deprecated Use {@link #createTunInterface(Collection)} instead. + * @hide + */ + @Deprecated + @NonNull + public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) { + return createTunInterface(Arrays.asList(linkAddrs)); + } + + /** + * Create a tun interface for testing purposes + * + * @param linkAddrs an array of LinkAddresses to assign to the TUN interface + * @return A ParcelFileDescriptor of the underlying TUN interface. Close this to tear down the + * TUN interface. + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) { + try { + final LinkAddress[] arr = new LinkAddress[linkAddrs.size()]; + return mService.createTunInterface(linkAddrs.toArray(arr)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Create a tap interface for testing purposes + * + * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the + * TAP interface. + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + public TestNetworkInterface createTapInterface() { + try { + return mService.createTapInterface(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + +} diff --git a/framework/src/android/net/TestNetworkSpecifier.java b/framework/src/android/net/TestNetworkSpecifier.java new file mode 100644 index 0000000000000000000000000000000000000000..117457dffc9f23c7bde8fade6703f2961778b2fc --- /dev/null +++ b/framework/src/android/net/TestNetworkSpecifier.java @@ -0,0 +1,97 @@ +/* + * 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.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * A {@link NetworkSpecifier} used to identify test interfaces. + * + * @see TestNetworkManager + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class TestNetworkSpecifier extends NetworkSpecifier implements Parcelable { + + /** + * Name of the network interface. + */ + @NonNull + private final String mInterfaceName; + + public TestNetworkSpecifier(@NonNull String interfaceName) { + if (TextUtils.isEmpty(interfaceName)) { + throw new IllegalArgumentException("Empty interfaceName"); + } + mInterfaceName = interfaceName; + } + + // This may be null in the future to support specifiers based on data other than the interface + // name. + @Nullable + public String getInterfaceName() { + return mInterfaceName; + } + + @Override + public boolean canBeSatisfiedBy(@Nullable NetworkSpecifier other) { + return equals(other); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TestNetworkSpecifier)) return false; + return TextUtils.equals(mInterfaceName, ((TestNetworkSpecifier) o).mInterfaceName); + } + + @Override + public int hashCode() { + return Objects.hashCode(mInterfaceName); + } + + @Override + public String toString() { + return "TestNetworkSpecifier (" + mInterfaceName + ")"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mInterfaceName); + } + + public static final @NonNull Creator<TestNetworkSpecifier> CREATOR = + new Creator<TestNetworkSpecifier>() { + public TestNetworkSpecifier createFromParcel(Parcel in) { + return new TestNetworkSpecifier(in.readString()); + } + public TestNetworkSpecifier[] newArray(int size) { + return new TestNetworkSpecifier[size]; + } + }; +} diff --git a/framework/src/android/net/TransportInfo.java b/framework/src/android/net/TransportInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..fa889eabb842bb50e8d7ba78ece482efe76c7b1b --- /dev/null +++ b/framework/src/android/net/TransportInfo.java @@ -0,0 +1,75 @@ +/* + * 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; + +import android.annotation.NonNull; +import android.annotation.SystemApi; + +/** + * A container for transport-specific capabilities which is returned by + * {@link NetworkCapabilities#getTransportInfo()}. Specific networks + * may provide concrete implementations of this interface. + * @see android.net.wifi.aware.WifiAwareNetworkInfo + * @see android.net.wifi.WifiInfo + */ +public interface TransportInfo { + + /** + * Create a copy of a {@link TransportInfo} with some fields redacted based on the permissions + * held by the receiving app. + * + * <p> + * Usage by connectivity stack: + * <ul> + * <li> Connectivity stack will invoke {@link #getApplicableRedactions()} to find the list + * of redactions that are required by this {@link TransportInfo} instance.</li> + * <li> Connectivity stack then loops through each bit in the bitmask returned and checks if the + * receiving app holds the corresponding permission. + * <ul> + * <li> If the app holds the corresponding permission, the bit is cleared from the + * |redactions| bitmask. </li> + * <li> If the app does not hold the corresponding permission, the bit is retained in the + * |redactions| bitmask. </li> + * </ul> + * <li> Connectivity stack then invokes {@link #makeCopy(long)} with the necessary |redactions| + * to create a copy to send to the corresponding app. </li> + * </ul> + * </p> + * + * @param redactions bitmask of redactions that needs to be performed on this instance. + * @return Copy of this instance with the necessary redactions. + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + default TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) { + return this; + } + + /** + * Returns a bitmask of all the applicable redactions (based on the permissions held by the + * receiving app) to be performed on this TransportInfo. + * + * @return bitmask of redactions applicable on this instance. + * @see #makeCopy(long) + * @hide + */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + default @NetworkCapabilities.RedactionType long getApplicableRedactions() { + return NetworkCapabilities.REDACT_NONE; + } +} diff --git a/framework/src/android/net/UidRange.aidl b/framework/src/android/net/UidRange.aidl new file mode 100644 index 0000000000000000000000000000000000000000..f70fc8e2fefd9166d724458a3a2a977b266fbd1f --- /dev/null +++ b/framework/src/android/net/UidRange.aidl @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * An inclusive range of UIDs. + * + * {@hide} + */ +parcelable UidRange; \ No newline at end of file diff --git a/framework/src/android/net/UidRange.java b/framework/src/android/net/UidRange.java new file mode 100644 index 0000000000000000000000000000000000000000..bd332928f4632fd57461e1d7352a545fa07d883f --- /dev/null +++ b/framework/src/android/net/UidRange.java @@ -0,0 +1,183 @@ +/* + * 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 android.net; + +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Range; + +import java.util.Collection; +import java.util.Set; + +/** + * An inclusive range of UIDs. + * + * @hide + */ +public final class UidRange implements Parcelable { + public final int start; + public final int stop; + + public UidRange(int startUid, int stopUid) { + if (startUid < 0) throw new IllegalArgumentException("Invalid start UID."); + if (stopUid < 0) throw new IllegalArgumentException("Invalid stop UID."); + if (startUid > stopUid) throw new IllegalArgumentException("Invalid UID range."); + start = startUid; + stop = stopUid; + } + + /** Creates a UidRange for the specified user. */ + public static UidRange createForUser(UserHandle user) { + final UserHandle nextUser = UserHandle.of(user.getIdentifier() + 1); + final int start = user.getUid(0 /* appId */); + final int end = nextUser.getUid(0 /* appId */) - 1; + return new UidRange(start, end); + } + + /** Returns the smallest user Id which is contained in this UidRange */ + public int getStartUser() { + return UserHandle.getUserHandleForUid(start).getIdentifier(); + } + + /** Returns the largest user Id which is contained in this UidRange */ + public int getEndUser() { + return UserHandle.getUserHandleForUid(stop).getIdentifier(); + } + + /** Returns whether the UidRange contains the specified UID. */ + public boolean contains(int uid) { + return start <= uid && uid <= stop; + } + + /** + * Returns the count of UIDs in this range. + */ + public int count() { + return 1 + stop - start; + } + + /** + * @return {@code true} if this range contains every UID contained by the {@code other} range. + */ + public boolean containsRange(UidRange other) { + return start <= other.start && other.stop <= stop; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + start; + result = 31 * result + stop; + return result; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof UidRange) { + UidRange other = (UidRange) o; + return start == other.start && stop == other.stop; + } + return false; + } + + @Override + public String toString() { + return start + "-" + stop; + } + + // Implement the Parcelable interface + // TODO: Consider making this class no longer parcelable, since all users are likely in the + // system server. + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(start); + dest.writeInt(stop); + } + + public static final @android.annotation.NonNull Creator<UidRange> CREATOR = + new Creator<UidRange>() { + @Override + public UidRange createFromParcel(Parcel in) { + int start = in.readInt(); + int stop = in.readInt(); + + return new UidRange(start, stop); + } + @Override + public UidRange[] newArray(int size) { + return new UidRange[size]; + } + }; + + /** + * Returns whether any of the UidRange in the collection contains the specified uid + * + * @param ranges The collection of UidRange to check + * @param uid the uid in question + * @return {@code true} if the uid is contained within the ranges, {@code false} otherwise + * + * @see UidRange#contains(int) + */ + public static boolean containsUid(Collection<UidRange> ranges, int uid) { + if (ranges == null) return false; + for (UidRange range : ranges) { + if (range.contains(uid)) { + return true; + } + } + return false; + } + + /** + * Convert a set of {@code Range<Integer>} to a set of {@link UidRange}. + */ + @Nullable + public static ArraySet<UidRange> fromIntRanges(@Nullable Set<Range<Integer>> ranges) { + if (null == ranges) return null; + + final ArraySet<UidRange> uids = new ArraySet<>(); + for (Range<Integer> range : ranges) { + uids.add(new UidRange(range.getLower(), range.getUpper())); + } + return uids; + } + + /** + * Convert a set of {@link UidRange} to a set of {@code Range<Integer>}. + */ + @Nullable + public static ArraySet<Range<Integer>> toIntRanges(@Nullable Set<UidRange> ranges) { + if (null == ranges) return null; + + final ArraySet<Range<Integer>> uids = new ArraySet<>(); + for (UidRange range : ranges) { + uids.add(new Range<Integer>(range.start, range.stop)); + } + return uids; + } +} diff --git a/framework/src/android/net/VpnTransportInfo.java b/framework/src/android/net/VpnTransportInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..4071c9ab71b4ff671688be0f25cb7fa45c4ac692 --- /dev/null +++ b/framework/src/android/net/VpnTransportInfo.java @@ -0,0 +1,120 @@ +/* + * 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 static android.annotation.SystemApi.Client.MODULE_LIBRARIES; +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.net.NetworkCapabilities.RedactionType; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * Container for VPN-specific transport information. + * + * @see android.net.TransportInfo + * @see NetworkCapabilities#getTransportInfo() + * + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class VpnTransportInfo implements TransportInfo, Parcelable { + /** Type of this VPN. */ + private final int mType; + + @Nullable + private final String mSessionId; + + @Override + public @RedactionType long getApplicableRedactions() { + return REDACT_FOR_NETWORK_SETTINGS; + } + + /** + * Create a copy of a {@link VpnTransportInfo} with the sessionId redacted if necessary. + */ + @NonNull + public VpnTransportInfo makeCopy(@RedactionType long redactions) { + return new VpnTransportInfo(mType, + ((redactions & REDACT_FOR_NETWORK_SETTINGS) != 0) ? null : mSessionId); + } + + public VpnTransportInfo(int type, @Nullable String sessionId) { + this.mType = type; + this.mSessionId = sessionId; + } + + /** + * Returns the session Id of this VpnTransportInfo. + */ + @Nullable + public String getSessionId() { + return mSessionId; + } + + /** + * Returns the type of this VPN. + */ + public int getType() { + return mType; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VpnTransportInfo)) return false; + + VpnTransportInfo that = (VpnTransportInfo) o; + return (this.mType == that.mType) && TextUtils.equals(this.mSessionId, that.mSessionId); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mSessionId); + } + + @Override + public String toString() { + return String.format("VpnTransportInfo{type=%d, sessionId=%s}", mType, mSessionId); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeString(mSessionId); + } + + public static final @NonNull Creator<VpnTransportInfo> CREATOR = + new Creator<VpnTransportInfo>() { + public VpnTransportInfo createFromParcel(Parcel in) { + return new VpnTransportInfo(in.readInt(), in.readString()); + } + public VpnTransportInfo[] newArray(int size) { + return new VpnTransportInfo[size]; + } + }; +} diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..663c1b3d2dc9f201e8cb821dbf79f4ed0d38c96c --- /dev/null +++ b/framework/src/android/net/apf/ApfCapabilities.java @@ -0,0 +1,151 @@ +/* + * 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.apf; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityResources; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * APF program support capabilities. APF stands for Android Packet Filtering and it is a flexible + * way to drop unwanted network packets to save power. + * + * See documentation at hardware/google/apf/apf.h + * + * This class is immutable. + * @hide + */ +@SystemApi +public final class ApfCapabilities implements Parcelable { + private static ConnectivityResources sResources; + + /** + * Version of APF instruction set supported for packet filtering. 0 indicates no support for + * packet filtering using APF programs. + */ + public final int apfVersionSupported; + + /** + * Maximum size of APF program allowed. + */ + public final int maximumApfProgramSize; + + /** + * Format of packets passed to APF filter. Should be one of ARPHRD_* + */ + public final int apfPacketFormat; + + public ApfCapabilities( + int apfVersionSupported, int maximumApfProgramSize, int apfPacketFormat) { + this.apfVersionSupported = apfVersionSupported; + this.maximumApfProgramSize = maximumApfProgramSize; + this.apfPacketFormat = apfPacketFormat; + } + + private ApfCapabilities(Parcel in) { + apfVersionSupported = in.readInt(); + maximumApfProgramSize = in.readInt(); + apfPacketFormat = in.readInt(); + } + + @NonNull + private static synchronized ConnectivityResources getResources(@NonNull Context ctx) { + if (sResources == null) { + sResources = new ConnectivityResources(ctx); + } + return sResources; + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(apfVersionSupported); + dest.writeInt(maximumApfProgramSize); + dest.writeInt(apfPacketFormat); + } + + public static final Creator<ApfCapabilities> CREATOR = new Creator<ApfCapabilities>() { + @Override + public ApfCapabilities createFromParcel(Parcel in) { + return new ApfCapabilities(in); + } + + @Override + public ApfCapabilities[] newArray(int size) { + return new ApfCapabilities[size]; + } + }; + + @NonNull + @Override + public String toString() { + return String.format("%s{version: %d, maxSize: %d, format: %d}", getClass().getSimpleName(), + apfVersionSupported, maximumApfProgramSize, apfPacketFormat); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ApfCapabilities)) return false; + final ApfCapabilities other = (ApfCapabilities) obj; + return apfVersionSupported == other.apfVersionSupported + && maximumApfProgramSize == other.maximumApfProgramSize + && apfPacketFormat == other.apfPacketFormat; + } + + /** + * Determines whether the APF interpreter advertises support for the data buffer access opcodes + * LDDW (LoaD Data Word) and STDW (STore Data Word). Full LDDW (LoaD Data Word) and + * STDW (STore Data Word) support is present from APFv4 on. + * + * @return {@code true} if the IWifiStaIface#readApfPacketFilterData is supported. + */ + public boolean hasDataAccess() { + return apfVersionSupported >= 4; + } + + /** + * @return Whether the APF Filter in the device should filter out IEEE 802.3 Frames. + */ + public static boolean getApfDrop8023Frames() { + // TODO: deprecate/remove this method (now unused in the platform), as the resource was + // moved to NetworkStack. + final Resources systemRes = Resources.getSystem(); + final int id = systemRes.getIdentifier("config_apfDrop802_3Frames", "bool", "android"); + return systemRes.getBoolean(id); + } + + /** + * @return An array of denylisted EtherType, packets with EtherTypes within it will be dropped. + */ + public static @NonNull int[] getApfEtherTypeBlackList() { + // TODO: deprecate/remove this method (now unused in the platform), as the resource was + // moved to NetworkStack. + final Resources systemRes = Resources.getSystem(); + final int id = systemRes.getIdentifier("config_apfEthTypeBlackList", "array", "android"); + return systemRes.getIntArray(id); + } +} diff --git a/framework/src/android/net/util/DnsUtils.java b/framework/src/android/net/util/DnsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3fe245edb9e2fcb9f526dcef8553eecaa6f901a8 --- /dev/null +++ b/framework/src/android/net/util/DnsUtils.java @@ -0,0 +1,377 @@ +/* + * 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.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.InetAddresses; +import android.net.Network; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import libcore.io.IoUtils; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * @hide + */ +public class DnsUtils { + private static final String TAG = "DnsUtils"; + private static final int CHAR_BIT = 8; + public static final int IPV6_ADDR_SCOPE_NODELOCAL = 0x01; + public static final int IPV6_ADDR_SCOPE_LINKLOCAL = 0x02; + public static final int IPV6_ADDR_SCOPE_SITELOCAL = 0x05; + public static final int IPV6_ADDR_SCOPE_GLOBAL = 0x0e; + private static final Comparator<SortableAddress> sRfc6724Comparator = new Rfc6724Comparator(); + + /** + * Comparator to sort SortableAddress in Rfc6724 style. + */ + public static class Rfc6724Comparator implements Comparator<SortableAddress> { + // This function matches the behaviour of _rfc6724_compare in the native resolver. + @Override + public int compare(SortableAddress span1, SortableAddress span2) { + // Rule 1: Avoid unusable destinations. + if (span1.hasSrcAddr != span2.hasSrcAddr) { + return span2.hasSrcAddr - span1.hasSrcAddr; + } + + // Rule 2: Prefer matching scope. + if (span1.scopeMatch != span2.scopeMatch) { + return span2.scopeMatch - span1.scopeMatch; + } + + // TODO: Implement rule 3: Avoid deprecated addresses. + // TODO: Implement rule 4: Prefer home addresses. + + // Rule 5: Prefer matching label. + if (span1.labelMatch != span2.labelMatch) { + return span2.labelMatch - span1.labelMatch; + } + + // Rule 6: Prefer higher precedence. + if (span1.precedence != span2.precedence) { + return span2.precedence - span1.precedence; + } + + // TODO: Implement rule 7: Prefer native transport. + + // Rule 8: Prefer smaller scope. + if (span1.scope != span2.scope) { + return span1.scope - span2.scope; + } + + // Rule 9: Use longest matching prefix. IPv6 only. + if (span1.prefixMatchLen != span2.prefixMatchLen) { + return span2.prefixMatchLen - span1.prefixMatchLen; + } + + // Rule 10: Leave the order unchanged. Collections.sort is a stable sort. + return 0; + } + } + + /** + * Class used to sort with RFC 6724 + */ + public static class SortableAddress { + public final int label; + public final int labelMatch; + public final int scope; + public final int scopeMatch; + public final int precedence; + public final int prefixMatchLen; + public final int hasSrcAddr; + public final InetAddress address; + + public SortableAddress(@NonNull InetAddress addr, @Nullable InetAddress srcAddr) { + address = addr; + hasSrcAddr = (srcAddr != null) ? 1 : 0; + label = findLabel(addr); + scope = findScope(addr); + precedence = findPrecedence(addr); + labelMatch = ((srcAddr != null) && (label == findLabel(srcAddr))) ? 1 : 0; + scopeMatch = ((srcAddr != null) && (scope == findScope(srcAddr))) ? 1 : 0; + if (isIpv6Address(addr) && isIpv6Address(srcAddr)) { + prefixMatchLen = compareIpv6PrefixMatchLen(srcAddr, addr); + } else { + prefixMatchLen = 0; + } + } + } + + /** + * Sort the given address list in RFC6724 order. + * Will leave the list unchanged if an error occurs. + * + * This function matches the behaviour of _rfc6724_sort in the native resolver. + */ + public static @NonNull List<InetAddress> rfc6724Sort(@Nullable Network network, + @NonNull List<InetAddress> answers) { + final ArrayList<SortableAddress> sortableAnswerList = new ArrayList<>(); + for (InetAddress addr : answers) { + sortableAnswerList.add(new SortableAddress(addr, findSrcAddress(network, addr))); + } + + Collections.sort(sortableAnswerList, sRfc6724Comparator); + + final List<InetAddress> sortedAnswers = new ArrayList<>(); + for (SortableAddress ans : sortableAnswerList) { + sortedAnswers.add(ans.address); + } + + return sortedAnswers; + } + + private static @Nullable InetAddress findSrcAddress(@Nullable Network network, + @NonNull InetAddress addr) { + final int domain; + if (isIpv4Address(addr)) { + domain = AF_INET; + } else if (isIpv6Address(addr)) { + domain = AF_INET6; + } else { + return null; + } + final FileDescriptor socket; + try { + socket = Os.socket(domain, SOCK_DGRAM, IPPROTO_UDP); + } catch (ErrnoException e) { + Log.e(TAG, "findSrcAddress:" + e.toString()); + return null; + } + try { + if (network != null) network.bindSocket(socket); + Os.connect(socket, new InetSocketAddress(addr, 0)); + return ((InetSocketAddress) Os.getsockname(socket)).getAddress(); + } catch (IOException | ErrnoException e) { + return null; + } finally { + IoUtils.closeQuietly(socket); + } + } + + /** + * Get the label for a given IPv4/IPv6 address. + * RFC 6724, section 2.1. + * + * Note that Java will return an IPv4-mapped address as an IPv4 address. + */ + private static int findLabel(@NonNull InetAddress addr) { + if (isIpv4Address(addr)) { + return 4; + } else if (isIpv6Address(addr)) { + if (addr.isLoopbackAddress()) { + return 0; + } else if (isIpv6Address6To4(addr)) { + return 2; + } else if (isIpv6AddressTeredo(addr)) { + return 5; + } else if (isIpv6AddressULA(addr)) { + return 13; + } else if (((Inet6Address) addr).isIPv4CompatibleAddress()) { + return 3; + } else if (addr.isSiteLocalAddress()) { + return 11; + } else if (isIpv6Address6Bone(addr)) { + return 12; + } else { + // All other IPv6 addresses, including global unicast addresses. + return 1; + } + } else { + // This should never happen. + return 1; + } + } + + private static boolean isIpv6Address(@Nullable InetAddress addr) { + return addr instanceof Inet6Address; + } + + private static boolean isIpv4Address(@Nullable InetAddress addr) { + return addr instanceof Inet4Address; + } + + private static boolean isIpv6Address6To4(@NonNull InetAddress addr) { + if (!isIpv6Address(addr)) return false; + final byte[] byteAddr = addr.getAddress(); + return byteAddr[0] == 0x20 && byteAddr[1] == 0x02; + } + + private static boolean isIpv6AddressTeredo(@NonNull InetAddress addr) { + if (!isIpv6Address(addr)) return false; + final byte[] byteAddr = addr.getAddress(); + return byteAddr[0] == 0x20 && byteAddr[1] == 0x01 && byteAddr[2] == 0x00 + && byteAddr[3] == 0x00; + } + + private static boolean isIpv6AddressULA(@NonNull InetAddress addr) { + return isIpv6Address(addr) && (addr.getAddress()[0] & 0xfe) == 0xfc; + } + + private static boolean isIpv6Address6Bone(@NonNull InetAddress addr) { + if (!isIpv6Address(addr)) return false; + final byte[] byteAddr = addr.getAddress(); + return byteAddr[0] == 0x3f && byteAddr[1] == (byte) 0xfe; + } + + private static int getIpv6MulticastScope(@NonNull InetAddress addr) { + return !isIpv6Address(addr) ? 0 : (addr.getAddress()[1] & 0x0f); + } + + private static int findScope(@NonNull InetAddress addr) { + if (isIpv6Address(addr)) { + if (addr.isMulticastAddress()) { + return getIpv6MulticastScope(addr); + } else if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) { + /** + * RFC 4291 section 2.5.3 says loopback is to be treated as having + * link-local scope. + */ + return IPV6_ADDR_SCOPE_LINKLOCAL; + } else if (addr.isSiteLocalAddress()) { + return IPV6_ADDR_SCOPE_SITELOCAL; + } else { + return IPV6_ADDR_SCOPE_GLOBAL; + } + } else if (isIpv4Address(addr)) { + if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) { + return IPV6_ADDR_SCOPE_LINKLOCAL; + } else { + /** + * RFC 6724 section 3.2. Other IPv4 addresses, including private addresses + * and shared addresses (100.64.0.0/10), are assigned global scope. + */ + return IPV6_ADDR_SCOPE_GLOBAL; + } + } else { + /** + * This should never happen. + * Return a scope with low priority as a last resort. + */ + return IPV6_ADDR_SCOPE_NODELOCAL; + } + } + + /** + * Get the precedence for a given IPv4/IPv6 address. + * RFC 6724, section 2.1. + * + * Note that Java will return an IPv4-mapped address as an IPv4 address. + */ + private static int findPrecedence(@NonNull InetAddress addr) { + if (isIpv4Address(addr)) { + return 35; + } else if (isIpv6Address(addr)) { + if (addr.isLoopbackAddress()) { + return 50; + } else if (isIpv6Address6To4(addr)) { + return 30; + } else if (isIpv6AddressTeredo(addr)) { + return 5; + } else if (isIpv6AddressULA(addr)) { + return 3; + } else if (((Inet6Address) addr).isIPv4CompatibleAddress() || addr.isSiteLocalAddress() + || isIpv6Address6Bone(addr)) { + return 1; + } else { + // All other IPv6 addresses, including global unicast addresses. + return 40; + } + } else { + return 1; + } + } + + /** + * Find number of matching initial bits between the two addresses. + */ + private static int compareIpv6PrefixMatchLen(@NonNull InetAddress srcAddr, + @NonNull InetAddress dstAddr) { + final byte[] srcByte = srcAddr.getAddress(); + final byte[] dstByte = dstAddr.getAddress(); + + // This should never happen. + if (srcByte.length != dstByte.length) return 0; + + for (int i = 0; i < dstByte.length; ++i) { + if (srcByte[i] == dstByte[i]) { + continue; + } + int x = (srcByte[i] & 0xff) ^ (dstByte[i] & 0xff); + return i * CHAR_BIT + (Integer.numberOfLeadingZeros(x) - 24); // Java ints are 32 bits + } + return dstByte.length * CHAR_BIT; + } + + /** + * Check if given network has Ipv4 capability + * This function matches the behaviour of have_ipv4 in the native resolver. + */ + public static boolean haveIpv4(@Nullable Network network) { + final SocketAddress addrIpv4 = + new InetSocketAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 0); + return checkConnectivity(network, AF_INET, addrIpv4); + } + + /** + * Check if given network has Ipv6 capability + * This function matches the behaviour of have_ipv6 in the native resolver. + */ + public static boolean haveIpv6(@Nullable Network network) { + final SocketAddress addrIpv6 = + new InetSocketAddress(InetAddresses.parseNumericAddress("2000::"), 0); + return checkConnectivity(network, AF_INET6, addrIpv6); + } + + private static boolean checkConnectivity(@Nullable Network network, + int domain, @NonNull SocketAddress addr) { + final FileDescriptor socket; + try { + socket = Os.socket(domain, SOCK_DGRAM, IPPROTO_UDP); + } catch (ErrnoException e) { + return false; + } + try { + if (network != null) network.bindSocket(socket); + Os.connect(socket, addr); + } catch (IOException | ErrnoException e) { + return false; + } finally { + IoUtils.closeQuietly(socket); + } + return true; + } +} diff --git a/framework/src/android/net/util/KeepaliveUtils.java b/framework/src/android/net/util/KeepaliveUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8d7a0b3d02edfb80a31ef30eb85d6027fd73f744 --- /dev/null +++ b/framework/src/android/net/util/KeepaliveUtils.java @@ -0,0 +1,117 @@ +/* + * 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.annotation.NonNull; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityResources; +import android.net.NetworkCapabilities; +import android.text.TextUtils; +import android.util.AndroidRuntimeException; + +/** + * Collection of utilities for socket keepalive offload. + * + * @hide + */ +public final class KeepaliveUtils { + + public static final String TAG = "KeepaliveUtils"; + + public static class KeepaliveDeviceConfigurationException extends AndroidRuntimeException { + public KeepaliveDeviceConfigurationException(final String msg) { + super(msg); + } + } + + /** + * Read supported keepalive count for each transport type from overlay resource. This should be + * used to create a local variable store of resource customization, and use it as the input for + * {@link getSupportedKeepalivesForNetworkCapabilities}. + * + * @param context The context to read resource from. + * @return An array of supported keepalive count for each transport type. + */ + @NonNull + public static int[] getSupportedKeepalives(@NonNull Context context) { + String[] res = null; + try { + final ConnectivityResources connRes = new ConnectivityResources(context); + // TODO: use R.id.config_networkSupportedKeepaliveCount directly + final int id = connRes.get().getIdentifier("config_networkSupportedKeepaliveCount", + "array", connRes.getResourcesContext().getPackageName()); + res = new ConnectivityResources(context).get().getStringArray(id); + } catch (Resources.NotFoundException unused) { + } + if (res == null) throw new KeepaliveDeviceConfigurationException("invalid resource"); + + final int[] ret = new int[NetworkCapabilities.MAX_TRANSPORT + 1]; + for (final String row : res) { + if (TextUtils.isEmpty(row)) { + throw new KeepaliveDeviceConfigurationException("Empty string"); + } + final String[] arr = row.split(","); + if (arr.length != 2) { + throw new KeepaliveDeviceConfigurationException("Invalid parameter length"); + } + + int transport; + int supported; + try { + transport = Integer.parseInt(arr[0]); + supported = Integer.parseInt(arr[1]); + } catch (NumberFormatException e) { + throw new KeepaliveDeviceConfigurationException("Invalid number format"); + } + + if (!NetworkCapabilities.isValidTransport(transport)) { + throw new KeepaliveDeviceConfigurationException("Invalid transport " + transport); + } + + if (supported < 0) { + throw new KeepaliveDeviceConfigurationException( + "Invalid supported count " + supported + " for " + + NetworkCapabilities.transportNameOf(transport)); + } + ret[transport] = supported; + } + return ret; + } + + /** + * Get supported keepalive count for the given {@link NetworkCapabilities}. + * + * @param supportedKeepalives An array of supported keepalive count for each transport type. + * @param nc The {@link NetworkCapabilities} of the network the socket keepalive is on. + * + * @return Supported keepalive count for the given {@link NetworkCapabilities}. + */ + public static int getSupportedKeepalivesForNetworkCapabilities( + @NonNull int[] supportedKeepalives, @NonNull NetworkCapabilities nc) { + final int[] transports = nc.getTransportTypes(); + if (transports.length == 0) return 0; + int supportedCount = supportedKeepalives[transports[0]]; + // Iterate through transports and return minimum supported value. + for (final int transport : transports) { + if (supportedCount > supportedKeepalives[transport]) { + supportedCount = supportedKeepalives[transport]; + } + } + return supportedCount; + } +} diff --git a/framework/src/android/net/util/MultinetworkPolicyTracker.java b/framework/src/android/net/util/MultinetworkPolicyTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..0b42a0036925f8c63b1eee4fb67ba2db0e954e8a --- /dev/null +++ b/framework/src/android/net/util/MultinetworkPolicyTracker.java @@ -0,0 +1,247 @@ +/* + * 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.util; + +import static android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI; +import static android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.net.ConnectivityResources; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +/** + * A class to encapsulate management of the "Smart Networking" capability of + * avoiding bad Wi-Fi when, for example upstream connectivity is lost or + * certain critical link failures occur. + * + * This enables the device to switch to another form of connectivity, like + * mobile, if it's available and working. + * + * The Runnable |avoidBadWifiCallback|, if given, is posted to the supplied + * Handler' whenever the computed "avoid bad wifi" value changes. + * + * Disabling this reverts the device to a level of networking sophistication + * circa 2012-13 by disabling disparate code paths each of which contribute to + * maintaining continuous, working Internet connectivity. + * + * @hide + */ +public class MultinetworkPolicyTracker { + private static String TAG = MultinetworkPolicyTracker.class.getSimpleName(); + + private final Context mContext; + private final ConnectivityResources mResources; + private final Handler mHandler; + private final Runnable mAvoidBadWifiCallback; + private final List<Uri> mSettingsUris; + private final ContentResolver mResolver; + private final SettingObserver mSettingObserver; + private final BroadcastReceiver mBroadcastReceiver; + + private volatile boolean mAvoidBadWifi = true; + private volatile int mMeteredMultipathPreference; + private int mActiveSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + + // Mainline module can't use internal HandlerExecutor, so add an identical executor here. + private static class HandlerExecutor implements Executor { + @NonNull + private final Handler mHandler; + + HandlerExecutor(@NonNull Handler handler) { + mHandler = handler; + } + @Override + public void execute(Runnable command) { + if (!mHandler.post(command)) { + throw new RejectedExecutionException(mHandler + " is shutting down"); + } + } + } + + @VisibleForTesting + protected class ActiveDataSubscriptionIdListener extends TelephonyCallback + implements TelephonyCallback.ActiveDataSubscriptionIdListener { + @Override + public void onActiveDataSubscriptionIdChanged(int subId) { + mActiveSubId = subId; + reevaluateInternal(); + } + } + + public MultinetworkPolicyTracker(Context ctx, Handler handler) { + this(ctx, handler, null); + } + + public MultinetworkPolicyTracker(Context ctx, Handler handler, Runnable avoidBadWifiCallback) { + mContext = ctx; + mResources = new ConnectivityResources(ctx); + mHandler = handler; + mAvoidBadWifiCallback = avoidBadWifiCallback; + mSettingsUris = Arrays.asList( + Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI), + Settings.Global.getUriFor(NETWORK_METERED_MULTIPATH_PREFERENCE)); + mResolver = mContext.getContentResolver(); + mSettingObserver = new SettingObserver(); + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reevaluateInternal(); + } + }; + + ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback( + new HandlerExecutor(handler), new ActiveDataSubscriptionIdListener()); + + updateAvoidBadWifi(); + updateMeteredMultipathPreference(); + } + + public void start() { + for (Uri uri : mSettingsUris) { + mResolver.registerContentObserver(uri, false, mSettingObserver); + } + + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + mContext.registerReceiverForAllUsers(mBroadcastReceiver, intentFilter, + null /* broadcastPermission */, mHandler); + + reevaluate(); + } + + public void shutdown() { + mResolver.unregisterContentObserver(mSettingObserver); + + mContext.unregisterReceiver(mBroadcastReceiver); + } + + public boolean getAvoidBadWifi() { + return mAvoidBadWifi; + } + + // TODO: move this to MultipathPolicyTracker. + public int getMeteredMultipathPreference() { + return mMeteredMultipathPreference; + } + + /** + * Whether the device or carrier configuration disables avoiding bad wifi by default. + */ + public boolean configRestrictsAvoidBadWifi() { + // TODO: use R.integer.config_networkAvoidBadWifi directly + final int id = mResources.get().getIdentifier("config_networkAvoidBadWifi", + "integer", mResources.getResourcesContext().getPackageName()); + return (getResourcesForActiveSubId().getInteger(id) == 0); + } + + @NonNull + private Resources getResourcesForActiveSubId() { + return SubscriptionManager.getResourcesForSubId( + mResources.getResourcesContext(), mActiveSubId); + } + + /** + * Whether we should display a notification when wifi becomes unvalidated. + */ + public boolean shouldNotifyWifiUnvalidated() { + return configRestrictsAvoidBadWifi() && getAvoidBadWifiSetting() == null; + } + + public String getAvoidBadWifiSetting() { + return Settings.Global.getString(mResolver, NETWORK_AVOID_BAD_WIFI); + } + + @VisibleForTesting + public void reevaluate() { + mHandler.post(this::reevaluateInternal); + } + + /** + * Reevaluate the settings. Must be called on the handler thread. + */ + private void reevaluateInternal() { + if (updateAvoidBadWifi() && mAvoidBadWifiCallback != null) { + mAvoidBadWifiCallback.run(); + } + updateMeteredMultipathPreference(); + } + + public boolean updateAvoidBadWifi() { + final boolean settingAvoidBadWifi = "1".equals(getAvoidBadWifiSetting()); + final boolean prev = mAvoidBadWifi; + mAvoidBadWifi = settingAvoidBadWifi || !configRestrictsAvoidBadWifi(); + return mAvoidBadWifi != prev; + } + + /** + * The default (device and carrier-dependent) value for metered multipath preference. + */ + public int configMeteredMultipathPreference() { + // TODO: use R.integer.config_networkMeteredMultipathPreference directly + final int id = mResources.get().getIdentifier("config_networkMeteredMultipathPreference", + "integer", mResources.getResourcesContext().getPackageName()); + return mResources.get().getInteger(id); + } + + public void updateMeteredMultipathPreference() { + String setting = Settings.Global.getString(mResolver, NETWORK_METERED_MULTIPATH_PREFERENCE); + try { + mMeteredMultipathPreference = Integer.parseInt(setting); + } catch (NumberFormatException e) { + mMeteredMultipathPreference = configMeteredMultipathPreference(); + } + } + + private class SettingObserver extends ContentObserver { + public SettingObserver() { + super(null); + } + + @Override + public void onChange(boolean selfChange) { + Log.wtf(TAG, "Should never be reached."); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (!mSettingsUris.contains(uri)) { + Log.wtf(TAG, "Unexpected settings observation: " + uri); + } + reevaluate(); + } + } +} diff --git a/service/Android.bp b/service/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..28bcdcba89d79d6f64cd4b887c5652295ad6f2c6 --- /dev/null +++ b/service/Android.bp @@ -0,0 +1,115 @@ +// +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library_shared { + name: "libservice-connectivity", + min_sdk_version: "30", + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + srcs: [ + "jni/com_android_server_TestNetworkService.cpp", + "jni/onload.cpp", + ], + stl: "libc++_static", + header_libs: [ + "libbase_headers", + ], + shared_libs: [ + "liblog", + "libnativehelper", + ], + apex_available: [ + "com.android.tethering", + ], +} + +java_library { + name: "service-connectivity-pre-jarjar", + sdk_version: "system_server_current", + min_sdk_version: "30", + srcs: [ + "src/**/*.java", + ":framework-connectivity-shared-srcs", + ":services-connectivity-shared-srcs", + // TODO: move to net-utils-device-common, enable shrink optimization to avoid extra classes + ":net-module-utils-srcs", + ], + libs: [ + // TODO (b/183097033) remove once system_server_current includes core_current + "stable.core.platform.api.stubs", + "android_system_server_stubs_current", + "framework-annotations-lib", + "framework-connectivity-annotations", + "framework-connectivity.impl", + "framework-tethering.stubs.module_lib", + "framework-wifi.stubs.module_lib", + "unsupportedappusage", + "ServiceConnectivityResources", + ], + static_libs: [ + "dnsresolver_aidl_interface-V8-java", + "modules-utils-os", + "net-utils-device-common", + "net-utils-framework-common", + "netd-client", + "netlink-client", + "networkstack-client", + "PlatformProperties", + "service-connectivity-protos", + ], + apex_available: [ + "com.android.tethering", + ], +} + +java_library { + name: "service-connectivity-protos", + sdk_version: "system_current", + min_sdk_version: "30", + proto: { + type: "nano", + }, + srcs: [ + ":system-messages-proto-src", + ], + libs: ["libprotobuf-java-nano"], + apex_available: [ + "com.android.tethering", + ], +} + +java_library { + name: "service-connectivity", + sdk_version: "system_server_current", + min_sdk_version: "30", + installable: true, + static_libs: [ + "service-connectivity-pre-jarjar", + ], + jarjar_rules: "jarjar-rules.txt", + apex_available: [ + "com.android.tethering", + ], +} diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..f491cc762b3cfeee135146707d222fa54df8cdfe --- /dev/null +++ b/service/ServiceConnectivityResources/Android.bp @@ -0,0 +1,40 @@ +// +// 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. +// + +// APK to hold all the wifi overlayable resources. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_app { + name: "ServiceConnectivityResources", + sdk_version: "module_30", + min_sdk_version: "30", + resource_dirs: [ + "res", + ], + privileged: true, + export_package_resources: true, + apex_available: [ + "com.android.tethering", + ], + certificate: ":com.android.connectivity.resources.certificate", +} + +android_app_certificate { + name: "com.android.connectivity.resources.certificate", + certificate: "resources-certs/com.android.connectivity.resources", +} diff --git a/service/ServiceConnectivityResources/AndroidManifest.xml b/service/ServiceConnectivityResources/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..2c303026158ef04cc9dcc33102378e2bd2227926 --- /dev/null +++ b/service/ServiceConnectivityResources/AndroidManifest.xml @@ -0,0 +1,37 @@ +<?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. + */ +--> +<!-- Manifest for connectivity resources APK --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.connectivity.resources" + coreApp="true" + android:versionCode="1" + android:versionName="S-initial"> + <application + android:label="@string/connectivityResourcesAppLabel" + android:defaultToDeviceProtectedStorage="true" + android:directBootAware="true"> + <!-- This is only used to identify this app by resolving the action. + The activity is never actually triggered. --> + <activity android:name="android.app.Activity" android:exported="true" android:enabled="true"> + <intent-filter> + <action android:name="com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/service/ServiceConnectivityResources/res/drawable-hdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-hdpi/stat_notify_rssi_in_range.png new file mode 100644 index 0000000000000000000000000000000000000000..74977e6aefffb385b33ee169faf7bd0c63cf38a2 Binary files /dev/null and b/service/ServiceConnectivityResources/res/drawable-hdpi/stat_notify_rssi_in_range.png differ diff --git a/service/ServiceConnectivityResources/res/drawable-mdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-mdpi/stat_notify_rssi_in_range.png new file mode 100644 index 0000000000000000000000000000000000000000..62e4fe94a35c105a8d3ba2800210a7c48bfe8ac5 Binary files /dev/null and b/service/ServiceConnectivityResources/res/drawable-mdpi/stat_notify_rssi_in_range.png differ diff --git a/service/ServiceConnectivityResources/res/drawable-xhdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-xhdpi/stat_notify_rssi_in_range.png new file mode 100644 index 0000000000000000000000000000000000000000..c0586d8bc21ee7fcd6b8a0ef7ba972e40bde68e5 Binary files /dev/null and b/service/ServiceConnectivityResources/res/drawable-xhdpi/stat_notify_rssi_in_range.png differ diff --git a/service/ServiceConnectivityResources/res/drawable-xxhdpi/stat_notify_rssi_in_range.png b/service/ServiceConnectivityResources/res/drawable-xxhdpi/stat_notify_rssi_in_range.png new file mode 100644 index 0000000000000000000000000000000000000000..86c34ed18d736bcf1632dc906827552d1badf0b6 Binary files /dev/null and b/service/ServiceConnectivityResources/res/drawable-xxhdpi/stat_notify_rssi_in_range.png differ diff --git a/service/ServiceConnectivityResources/res/drawable/stat_notify_wifi_in_range.xml b/service/ServiceConnectivityResources/res/drawable/stat_notify_wifi_in_range.xml new file mode 100644 index 0000000000000000000000000000000000000000..a271ca5224c7ac5704dfae496d08724653f01eff --- /dev/null +++ b/service/ServiceConnectivityResources/res/drawable/stat_notify_wifi_in_range.xml @@ -0,0 +1,27 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="26.0dp" + android:height="24.0dp" + android:viewportWidth="26.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#4DFFFFFF" + android:pathData="M19.1,14l-3.4,0l0,-1.5c0,-1.8 0.8,-2.8 1.5,-3.4C18.1,8.3 19.200001,8 20.6,8c1.2,0 2.3,0.3 3.1,0.8l1.9,-2.3C25.1,6.1 20.299999,2.1 13,2.1S0.9,6.1 0.4,6.5L13,22l0,0l0,0l0,0l0,0l6.5,-8.1L19.1,14z"/> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19.5,17.799999c0,-0.8 0.1,-1.3 0.2,-1.6c0.2,-0.3 0.5,-0.7 1.1,-1.2c0.4,-0.4 0.7,-0.8 1,-1.1s0.4,-0.8 0.4,-1.2c0,-0.5 -0.1,-0.9 -0.4,-1.2c-0.3,-0.3 -0.7,-0.4 -1.2,-0.4c-0.4,0 -0.8,0.1 -1.1,0.3c-0.3,0.2 -0.4,0.6 -0.4,1.1l-1.9,0c0,-1 0.3,-1.7 1,-2.2c0.6,-0.5 1.5,-0.8 2.5,-0.8c1.1,0 2,0.3 2.6,0.8c0.6,0.5 0.9,1.3 0.9,2.3c0,0.7 -0.2,1.3 -0.6,1.8c-0.4,0.6 -0.9,1.1 -1.5,1.6c-0.3,0.3 -0.5,0.5 -0.6,0.7c-0.1,0.2 -0.1,0.6 -0.1,1L19.5,17.700001zM21.4,21l-1.9,0l0,-1.8l1.9,0L21.4,21z"/> +</vector> diff --git a/service/ServiceConnectivityResources/res/values-af/strings.xml b/service/ServiceConnectivityResources/res/values-af/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..550ab8a65d0a5953b024e30277f67310d6c49fb2 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-af/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Stelselkonnektiwiteithulpbronne"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Meld aan by Wi-Fi-netwerk"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Meld by netwerk aan"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het geen internettoegang nie"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tik vir opsies"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Selnetwerk het nie internettoegang nie"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Netwerk het nie internettoegang nie"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Daar kan nie by private DNS-bediener ingegaan word nie"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> het beperkte konnektiwiteit"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tik om in elk geval te koppel"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Het oorgeskakel na <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Toestel gebruik <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wanneer <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> geen internettoegang het nie. Heffings kan geld."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Het oorgeskakel van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobiele data"</item> + <item msgid="6341719431034774569">"Wi-fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"\'n onbekende netwerktipe"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-am/strings.xml b/service/ServiceConnectivityResources/res/values-am/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7f1a9dbe51ef14bd9b4253d95da2be35cefd98d1 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-am/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"የስáˆá‹“ት áŒáŠ•áŠ™áŠá‰µ መáˆáŒƒá‹Žá‰½"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ወደ Wi-Fi አá‹á‰³áˆ¨ መረብ በመለያ áŒá‰£"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ወደ አá‹á‰³áˆ¨ መረብ በመለያ á‹áŒá‰¡"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> áˆáŠ•áˆ የበá‹áŠ መረብ መዳረሻ የለáˆ"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ለአማራጮች መታ ያድáˆáŒ‰"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"የተንቀሳቃሽ ስáˆáŠ አá‹á‰³áˆ¨ መረብ የበá‹áŠáˆ˜áˆ¨á‰¥ መዳረሻ የለá‹áˆ"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"አá‹á‰³áˆ¨ መረብ የበá‹áŠáˆ˜áˆ¨á‰¥ መዳረሻ የለá‹áˆ"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"የáŒáˆ ዲኤንኤስ አገáˆáŒ‹á‹ ሊደረስበት አá‹á‰½áˆáˆ"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> የተገደበáŒáŠ•áŠ™áŠá‰µ አለá‹"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ለማንኛá‹áˆ ለማገናኘት መታ ያድáˆáŒ‰"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"ወደ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ተቀá‹áˆ¯áˆ"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> áˆáŠ•áˆ á‹“á‹áŠá‰µ የበá‹áŠáˆ˜áˆ¨á‰¥ áŒáŠ•áŠ™áŠá‰µ በማá‹áŠ–ረዠጊዜ መሣሪያዎች <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ን á‹áŒ ቀማሉᢠáŠáያዎች ተáˆáŒ»áˆš ሊሆኑ á‹á‰½áˆ‹áˆ‰á¢"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"ከ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ወደ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ተቀá‹áˆ¯áˆ"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"የተንቀሳቃሽ ስáˆáŠ á‹áˆ‚ብ"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"ብሉቱá‹"</item> + <item msgid="1160736166977503463">"ኢተáˆáŠ”ት"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"አንድ á‹«áˆá‰³á‹ˆá‰€ አá‹á‰³áˆ¨ መረብ á‹“á‹áŠá‰µ"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..b7a62c5e1df0c27e708154767a02ec6089b63684 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"مصادر إمكانية اتصال الخادم"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"â€ØªØ³Ø¬ÙŠÙ„ الدخول إلى شبكة Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"تسجيل الدخول إلى الشبكة"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"لا يتوÙّر ÙÙŠ <xliff:g id="NETWORK_SSID">%1$s</xliff:g> إمكانية الاتصال بالإنترنت."</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"انقر للØصول على الخيارات."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"شبكة الجوّال هذه غير متصلة بالإنترنت"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"الشبكة غير متصلة بالإنترنت"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"إمكانية اتصال <xliff:g id="NETWORK_SSID">%1$s</xliff:g> Ù…Øدودة."</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"يمكنك النقر للاتصال على أي Øال."</string> + <string name="network_switch_metered" msgid="2814798852883117872">"تم التبديل إلى <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"يستخدم الجهاز <xliff:g id="NEW_NETWORK">%1$s</xliff:g> عندما لا يتوÙر اتصال بالإنترنت ÙÙŠ شبكة <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ØŒ ويمكن أن يتم Ùرض رسوم مقابل ذلك."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"تم التبديل من <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> إلى <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"بيانات الجوّال"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"بلوتوث"</item> + <item msgid="1160736166977503463">"إيثرنت"</item> + <item msgid="7347618872551558605">"â€Ø´Ø¨ÙƒØ© اÙتراضية خاصة (VPN)"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"نوع شبكة غير معروÙ"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf8e6ac9d4092b92061f0ac6340ea5b70740c9ff --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-as/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ছিষà§à¦Ÿà§‡à¦® সংযোগৰ উৎস"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ৱাই-ফাই নেটৱৰà§à¦•à¦¤ ছাইন ইন কৰক"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"নেটৱৰà§à¦•à¦¤ ছাইন ইন কৰক"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ ইণà§à¦Ÿà¦¾à§°à¦¨à§‡à¦Ÿà§° à¦à¦•à§à¦¸à§‡à¦› নাই"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"অধিক বিকলà§à¦ªà§° বাবে টিপক"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ম’বাইল নেটৱৰà§à¦•à§° কোনো ইণà§à¦Ÿà¦¾à§°à¦¨à§‡à¦Ÿà§° à¦à¦•à§à¦¸à§‡à¦› নাই"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"নেটৱৰà§à¦•à§° কোনো ইণà§à¦Ÿà¦¾à§°à¦¨à§‡à¦Ÿà§° à¦à¦•à§à¦¸à§‡à¦› নাই"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"বà§à¦¯à¦•à§à¦¤à¦¿à¦—ত DNS ছাৰà§à¦à¦¾à§° à¦à¦•à§à¦¸à§‡à¦› কৰিব নোৱাৰি"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ৰ সকলো সেৱাৰ à¦à¦•à§à¦¸à§‡à¦› নাই"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"যিকোনো পà§à§°à¦•à¦¾à§°à§‡ সংযোগ কৰিবলৈ টিপক"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>লৈ সলনি কৰা হ’ল"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"যেতিয়া <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ত ইণà§à¦Ÿà¦¾à§°à¦¨à§‡à¦Ÿ নাথাকে, তেতিয়া ডিà¦à¦¾à¦‡à¦šà§‡ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ক বà§à¦¯à§±à¦¹à¦¾à§° কৰে। মাচà§à¦² পà§à§°à¦¯à§‹à¦œà§à¦¯ হ\'ব পাৰে।"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>ৰ পৰা <xliff:g id="NEW_NETWORK">%2$s</xliff:g> লৈ সলনি কৰা হ’ল"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ম’বাইল ডেটা"</item> + <item msgid="6341719431034774569">"ৱাই-ফাই"</item> + <item msgid="5081440868800877512">"বà§à¦²à§à¦Ÿà§à¦¥"</item> + <item msgid="1160736166977503463">"ইথাৰনেট"</item> + <item msgid="7347618872551558605">"à¦à¦¿à¦ªà¦¿à¦à¦¨"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"অজà§à¦žà¦¾à¦¤ পà§à§°à¦•à¦¾à§°à§° নেটৱৰà§à¦•"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-az/strings.xml b/service/ServiceConnectivityResources/res/values-az/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e927ed41b5c816007f98b65bd6e74d55517e412 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-az/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistem BaÄŸlantı Resursları"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi ÅŸÉ™bÉ™kÉ™sinÉ™ daxil ol"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ŞəbÉ™kÉ™yÉ™ daxil olun"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> üçün internet giriÅŸi É™lçatan deyil"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"SeçimlÉ™r üçün tıklayın"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobil ÅŸÉ™bÉ™kÉ™nin internetÉ™ giriÅŸi yoxdur"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ŞəbÉ™kÉ™nin internetÉ™ giriÅŸi yoxdur"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ÖzÉ™l DNS serverinÉ™ giriÅŸ mümkün deyil"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> baÄŸlantını mÉ™hdudlaÅŸdırdı"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ä°stÉ™nilÉ™n halda kliklÉ™yin"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ÅŸÉ™bÉ™kÉ™ növünÉ™ keçirildi"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ÅŸÉ™bÉ™kÉ™sinin internetÉ™ giriÅŸi olmadıqda, cihaz <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ÅŸÉ™bÉ™kÉ™sini istifadÉ™ edir. XidmÉ™t haqqı tutula bilÉ™r."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ÅŸÉ™bÉ™kÉ™sindÉ™n <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ÅŸÉ™bÉ™kÉ™sinÉ™ keçirildi"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobil data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"namÉ™lum ÅŸÉ™bÉ™kÉ™ növü"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..3f1b976fb93983e8ae293c73ff879edcaf032722 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resursi za povezivanje sa sistemom"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijavljivanje na WiFi mrežu"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Prijavite se na mrežu"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dodirnite za opcije"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilna mreža nema pristup internetu"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Mreža nema pristup internetu"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Pristup privatnom DNS serveru nije uspeo"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograniÄenu vezu"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dodirnite da biste se ipak povezali"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"PreÅ¡li ste na tip mreže <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"UreÄ‘aj koristi tip mreže <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kada tip mreže <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu. Možda će se naplaćivati troÅ¡kovi."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"PreÅ¡li ste sa tipa mreže <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na tip mreže <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobilni podaci"</item> + <item msgid="6341719431034774569">"WiFi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Eternet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nepoznat tip mreže"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-be/strings.xml b/service/ServiceConnectivityResources/res/values-be/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..21edf24054817ca2e8ab79106ea934a5534cd8cc --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-be/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ð ÑÑурÑÑ‹ Ð´Ð»Ñ Ð¿Ð°Ð´ÐºÐ»ÑŽÑ‡ÑÐ½Ð½Ñ Ð´Ð° ÑÑ–ÑÑ‚Ñмы"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Уваход у Ñетку Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Увайдзіце Ñž Ñетку"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> не мае доÑтупу Ñž інтÑрнÑÑ‚"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ДакраніцеÑÑ, каб убачыць параметры"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ÐœÐ°Ð±Ñ–Ð»ÑŒÐ½Ð°Ñ Ñетка не мае доÑтупу Ñž інтÑрнÑÑ‚"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Сетка не мае доÑтупу Ñž інтÑрнÑÑ‚"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ðе ўдалоÑÑ Ð°Ñ‚Ñ€Ñ‹Ð¼Ð°Ñ†ÑŒ доÑтуп да прыватнага DNS-Ñервера"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> мае абмежаваную магчымаÑць падключÑннÑ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ÐаціÑніце, каб падключыцца"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Выкананы пераход да <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Прылада выкарыÑтоўвае Ñетку <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, калі Ñž Ñетцы <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нÑма доÑтупу да інтÑрнÑту. Можа ÑпаганÑцца плата."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Выкананы пераход з <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> да <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"Ð¼Ð°Ð±Ñ–Ð»ÑŒÐ½Ð°Ñ Ð¿ÐµÑ€Ð°Ð´Ð°Ñ‡Ð° даных"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"невÑдомы тып Ñеткі"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-bg/strings.xml b/service/ServiceConnectivityResources/res/values-bg/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..c3c2d722f42bdc4ef8c51fbe346c5358dedb8082 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-bg/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"РеÑурÑи за ÑвързаноÑтта на ÑиÑтемата"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Влизане в Wi-Fi мрежа"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Вход в мрежата"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нÑма доÑтъп до интернет"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ДокоÑнете за опции"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилната мрежа нÑма доÑтъп до интернет"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Мрежата нÑма доÑтъп до интернет"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ðе може да Ñе оÑъщеÑтви доÑтъп до чаÑÑ‚Ð½Ð¸Ñ DNS Ñървър"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена ÑвързаноÑÑ‚"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ДокоÑнете, за да Ñе Ñвържете въпреки това"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Превключи Ñе към <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"УÑтройÑтвото използва <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, когато <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нÑма доÑтъп до интернет. Възможно е да бъдете такÑувани."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Превключи Ñе от <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> към <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобилни данни"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"неизвеÑтен тип мрежа"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-bn/strings.xml b/service/ServiceConnectivityResources/res/values-bn/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0f693bd60c62934167a85e0ca93f11018db3284c --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-bn/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"সিসà§à¦Ÿà§‡à¦® কানেকà§à¦Ÿà¦¿à¦à¦¿à¦Ÿà¦¿ রিসোরà§à¦¸à§‡à¦¸"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ওয়াই-ফাই নেটওয়ারà§à¦•à§‡ সাইন-ইন করà§à¦¨"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"নেটওয়ারà§à¦•à§‡ সাইন-ইন করà§à¦¨"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-à¦à¦° ইনà§à¦Ÿà¦¾à¦°à¦¨à§‡à¦Ÿà§‡ অà§à¦¯à¦¾à¦•à§à¦¸à§‡à¦¸ নেই"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"বিকলà§à¦ªà¦—à§à¦²à¦¿à¦° জনà§à¦¯ আলতো চাপà§à¦¨"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"মোবাইল নেটওয়ারà§à¦•à§‡ কোনও ইনà§à¦Ÿà¦¾à¦°à¦¨à§‡à¦Ÿ অà§à¦¯à¦¾à¦•à§à¦¸à§‡à¦¸ নেই"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"নেটওয়ারà§à¦•à§‡ কোনও ইনà§à¦Ÿà¦¾à¦°à¦¨à§‡à¦Ÿ অà§à¦¯à¦¾à¦•à§à¦¸à§‡à¦¸ নেই"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"বà§à¦¯à¦•à§à¦¤à¦¿à¦—ত ডিà¦à¦¨à¦à¦¸ সারà§à¦à¦¾à¦° অà§à¦¯à¦¾à¦•à§à¦¸à§‡à¦¸ করা যাবে না"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-à¦à¦° সীমিত কানেকà§à¦Ÿà¦¿à¦à¦¿à¦Ÿà¦¿ আছে"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"তবà§à¦“ কানেকà§à¦Ÿ করতে টà§à¦¯à¦¾à¦ª করà§à¦¨"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ঠপালà§à¦Ÿà¦¾à¦¨à§‹ হয়েছে"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ঠইনà§à¦Ÿà¦¾à¦°à¦¨à§‡à¦Ÿ অà§à¦¯à¦¾à¦•à§à¦¸à§‡à¦¸ না থাকলে <xliff:g id="NEW_NETWORK">%1$s</xliff:g> বà§à¦¯à¦¬à¦¹à¦¾à¦° করা হয়৷ ডেটা চারà§à¦œ পà§à¦°à¦¯à§‹à¦œà§à¦¯à§·"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> থেকে <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ঠপালà§à¦Ÿà¦¾à¦¨à§‹ হয়েছে"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"মোবাইল ডেটা"</item> + <item msgid="6341719431034774569">"ওয়াই-ফাই"</item> + <item msgid="5081440868800877512">"বà§à¦²à§à¦Ÿà§à¦¥"</item> + <item msgid="1160736166977503463">"ইথারনেট"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"à¦à¦‡ নেটওয়ারà§à¦•à§‡à¦° ধরন অজানা"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-bs/strings.xml b/service/ServiceConnectivityResources/res/values-bs/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..33d6ed9147280a7af024739407e41558ab582b7e --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-bs/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Izvori povezivosti sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijavljivanje na WiFi mrežu"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Prijava na mrežu"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dodirnite za opcije"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilna mreža nema pristup internetu"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Mreža nema pristup internetu"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nije moguće pristupiti privatnom DNS serveru"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Mreža <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograniÄenu povezivost"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dodirnite da se ipak povežete"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"PrebaÄeno na: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Kada <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu, ureÄ‘aj koristi mrežu <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Moguća je naplata usluge."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"PrebaÄeno iz mreže <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> u <xliff:g id="NEW_NETWORK">%2$s</xliff:g> mrežu"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"prijenos podataka na mobilnoj mreži"</item> + <item msgid="6341719431034774569">"WiFi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nepoznata vrsta mreže"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ca/strings.xml b/service/ServiceConnectivityResources/res/values-ca/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..04f6bd211c83315d45d2595748f63a080a235963 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ca/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de connectivitat del sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Inicia la sessió a la xarxa Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Inicia la sessió a la xarxa"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no té accés a Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toca per veure les opcions"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"La xarxa mòbil no té accés a Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"La xarxa no té accés a Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"No es pot accedir al servidor DNS privat"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> té una connectivitat limitada"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toca per connectar igualment"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Actualment en ús: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"El dispositiu utilitza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> en cas que <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tingui accés a Internet. És possible que s\'hi apliquin cà rrecs."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Abans es feia servir la xarxa <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>; ara s\'utilitza <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"dades mòbils"</item> + <item msgid="6341719431034774569">"Wi‑Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipus de xarxa desconegut"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-cs/strings.xml b/service/ServiceConnectivityResources/res/values-cs/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..6309e788e631762bb0ca784409c715806d94da8e --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-cs/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Zdroje pro pÅ™ipojenà systému"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"PÅ™ihlásit se k sÃti Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"PÅ™ihlásit se k sÃti"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"SÃÅ¥ <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá pÅ™Ãstup k internetu"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"KlepnutÃm zobrazÃte možnosti"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnà sÃÅ¥ nemá pÅ™Ãstup k internetu"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"SÃÅ¥ nemá pÅ™Ãstup k internetu"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nelze zÃskat pÅ™Ãstup k soukromému serveru DNS"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"SÃÅ¥ <xliff:g id="NETWORK_SSID">%1$s</xliff:g> umožňuje jen omezené pÅ™ipojenÃ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"KlepnutÃm se i pÅ™esto pÅ™ipojÃte"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"PÅ™echod na sÃÅ¥ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Když sÃÅ¥ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nebude mÃt pÅ™Ãstup k internetu, zaÅ™Ãzenà použije sÃÅ¥ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Mohou být úÄtovány poplatky."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"PÅ™echod ze sÃtÄ› <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sÃÅ¥ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobilnà data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"neznámý typ sÃtÄ›"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-da/strings.xml b/service/ServiceConnectivityResources/res/values-da/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..57c58afc414737bc68fbed2ae93ceab6225abe6c --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-da/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Systemets forbindelsesressourcer"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Log ind pÃ¥ Wi-Fi-netværk"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Log ind pÃ¥ netværk"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetforbindelse"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tryk for at se valgmuligheder"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnetværket har ingen internetadgang"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Netværket har ingen internetadgang"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Der er ikke adgang til den private DNS-server"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrænset forbindelse"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tryk for at oprette forbindelse alligevel"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Der blev skiftet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Enheden benytter <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, nÃ¥r der ikke er internetadgang via <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Der opkræves muligvis betaling."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Der blev skiftet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobildata"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"en ukendt netværkstype"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0c255197ac8f38fa2c92fc7b497bd59c9f283b9 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-de/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Systemverbindungsressourcen"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"In WLAN anmelden"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Im Netzwerk anmelden"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> hat keinen Internetzugriff"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Für Optionen tippen"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiles Netzwerk hat keinen Internetzugriff"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Netzwerk hat keinen Internetzugriff"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Schlechte Verbindung mit <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tippen, um die Verbindung trotzdem herzustellen"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Zu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> gewechselt"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Auf dem Gerät werden <xliff:g id="NEW_NETWORK">%1$s</xliff:g> genutzt, wenn über <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> kein Internet verfügbar ist. Eventuell fallen Gebühren an."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Von \"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>\" zu \"<xliff:g id="NEW_NETWORK">%2$s</xliff:g>\" gewechselt"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"Mobile Daten"</item> + <item msgid="6341719431034774569">"WLAN"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ein unbekannter Netzwerktyp"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-el/strings.xml b/service/ServiceConnectivityResources/res/values-el/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..1c2838dfd470042a01dc2820ffe22685a19e8f6f --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-el/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Î ÏŒÏοι συνδεσιμότητας συστήματος"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Συνδεθείτε στο δίκτυο Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ΣÏνδεση στο δίκτυο"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Η εφαÏμογή <xliff:g id="NETWORK_SSID">%1$s</xliff:g> δεν Îχει Ï€Ïόσβαση στο διαδίκτυο"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Πατήστε για να δείτε τις επιλογÎÏ‚"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Το δίκτυο κινητής τηλεφωνίας δεν Îχει Ï€Ïόσβαση στο διαδίκτυο."</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Το δίκτυο δεν Îχει Ï€Ïόσβαση στο διαδίκτυο."</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Δεν είναι δυνατή η Ï€Ïόσβαση στον ιδιωτικό διακομιστή DNS."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Το δίκτυο <xliff:g id="NETWORK_SSID">%1$s</xliff:g> Îχει πεÏιοÏισμÎνη συνδεσιμότητα"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Πατήστε για σÏνδεση οÏτως ή άλλως"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Μετάβαση σε δίκτυο <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Η συσκευή χÏησιμοποιεί το δίκτυο <xliff:g id="NEW_NETWORK">%1$s</xliff:g> όταν το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> δεν Îχει Ï€Ïόσβαση στο διαδίκτυο. ΜποÏεί να ισχÏουν χÏεώσεις."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Μετάβαση από το δίκτυο <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> στο δίκτυο <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"δεδομÎνα κινητής τηλεφωνίας"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"άγνωστος Ï„Ïπος δικτÏου"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..db5ad7029b6f0a3c679b7b383fbd508eea632fd8 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobile data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..db5ad7029b6f0a3c679b7b383fbd508eea632fd8 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobile data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..db5ad7029b6f0a3c679b7b383fbd508eea632fd8 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobile data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..db5ad7029b6f0a3c679b7b383fbd508eea632fd8 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System connectivity resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Sign in to a Wi-Fi network"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Sign in to network"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has no Internet access"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tap for options"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobile network has no Internet access"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Network has no Internet access"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Private DNS server cannot be accessed"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> has limited connectivity"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tap to connect anyway"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Switched to <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Device uses <xliff:g id="NEW_NETWORK">%1$s</xliff:g> when <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> has no Internet access. Charges may apply."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Switched from <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> to <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobile data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"an unknown network type"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..2602bfa065314d5baae5344f581b5f782b188f07 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€â€Žâ€Žâ€â€â€Žâ€Žâ€Žâ€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€â€â€â€â€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€Žâ€â€â€Žâ€â€â€â€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€â€Žâ€â€â€ŽSystem Connectivity Resources‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€â€â€Žâ€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€â€â€â€â€Žâ€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€Žâ€â€Žâ€â€Žâ€â€â€Žâ€Žâ€â€ŽSign in to Wi-Fi network‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€â€â€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€â€â€Žâ€Žâ€â€â€Žâ€â€â€â€â€â€â€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€â€Žâ€Žâ€â€â€Žâ€Žâ€Žâ€ŽSign in to network‎â€â€Žâ€Žâ€â€Ž"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€Žâ€â€â€Žâ€â€â€Žâ€â€â€â€â€â€Žâ€â€Žâ€â€â€Žâ€Žâ€Žâ€â€â€Žâ€â€Žâ€â€â€â€Žâ€Žâ€â€Žâ€Žâ€â€â€Žâ€â€â€Žâ€Žâ€â€â€Žâ€â€â€â€Žâ€â€â€â€â€Žâ€Žâ€Žâ€â€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€â€Ž<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Ž has no internet access‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€â€Žâ€â€â€Žâ€â€Žâ€Žâ€â€â€â€â€Žâ€â€â€Žâ€Žâ€â€â€â€â€Žâ€â€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€ŽTap for options‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€Žâ€â€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€â€Žâ€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€â€Žâ€â€Žâ€â€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€â€Žâ€â€Žâ€â€â€â€ŽMobile network has no internet access‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€â€â€â€â€â€â€Žâ€â€â€Žâ€â€Žâ€Žâ€Žâ€â€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€â€â€â€â€â€Žâ€â€Žâ€â€â€ŽNetwork has no internet access‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€â€â€â€â€â€â€â€Žâ€â€â€Žâ€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€â€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€â€â€Žâ€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€ŽPrivate DNS server cannot be accessed‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€Žâ€â€Žâ€Žâ€â€Žâ€â€Žâ€â€Žâ€â€Žâ€â€â€â€Žâ€â€â€â€â€â€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€Žâ€Žâ€â€â€â€Žâ€â€Žâ€â€â€â€Žâ€â€Žâ€Žâ€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€â€Ž<xliff:g id="NETWORK_SSID">%1$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Ž has limited connectivity‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€â€â€Žâ€â€â€Žâ€â€â€Žâ€Žâ€â€â€â€â€â€â€Žâ€Žâ€â€â€Žâ€â€â€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€â€Žâ€â€Žâ€â€Žâ€â€Žâ€â€Žâ€ŽTap to connect anyway‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€Žâ€â€Žâ€Žâ€â€â€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€â€Žâ€â€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€â€â€Žâ€â€â€â€Žâ€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€ŽSwitched to ‎â€â€Žâ€Žâ€â€â€Ž<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Žâ€Žâ€â€Žâ€Žâ€â€Ž"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€â€â€Žâ€â€Žâ€â€Žâ€â€Žâ€â€â€Žâ€â€â€â€Žâ€Žâ€â€â€Žâ€â€â€â€Žâ€Žâ€â€â€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€â€â€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€â€ŽDevice uses ‎â€â€Žâ€Žâ€â€â€Ž<xliff:g id="NEW_NETWORK">%1$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Ž when ‎â€â€Žâ€Žâ€â€â€Ž<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Ž has no internet access. Charges may apply.‎â€â€Žâ€Žâ€â€Ž"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€â€â€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€â€â€â€â€Žâ€Žâ€â€Žâ€â€Žâ€â€â€â€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€Žâ€â€â€â€â€Žâ€â€â€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€ŽSwitched from ‎â€â€Žâ€Žâ€â€â€Ž<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Ž to ‎â€â€Žâ€Žâ€â€â€Ž<xliff:g id="NEW_NETWORK">%2$s</xliff:g>‎â€â€Žâ€Žâ€â€â€â€Žâ€Žâ€â€Žâ€Žâ€â€Ž"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€Žâ€Žâ€â€Žâ€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€â€â€â€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€â€â€Žâ€â€Žâ€â€â€â€Žâ€â€â€Žâ€Žâ€â€Žâ€â€Žâ€â€â€Žmobile data‎â€â€Žâ€Žâ€â€Ž"</item> + <item msgid="6341719431034774569">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€Žâ€Žâ€â€ŽWi-Fi‎â€â€Žâ€Žâ€â€Ž"</item> + <item msgid="5081440868800877512">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€Žâ€Žâ€Žâ€â€â€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€â€â€Žâ€Žâ€â€â€â€â€Žâ€Žâ€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€â€Žâ€â€Žâ€Žâ€â€â€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€ŽBluetooth‎â€â€Žâ€Žâ€â€Ž"</item> + <item msgid="1160736166977503463">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€â€â€â€â€Žâ€Žâ€Žâ€Žâ€â€â€Žâ€â€â€Žâ€Žâ€â€Žâ€Žâ€â€â€Žâ€â€â€â€â€Žâ€â€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€â€â€Žâ€Žâ€â€â€â€ŽEthernet‎â€â€Žâ€Žâ€â€Ž"</item> + <item msgid="7347618872551558605">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€Žâ€Žâ€â€Žâ€â€â€â€â€â€Žâ€â€â€â€â€â€â€â€â€â€Žâ€Žâ€Žâ€â€â€Žâ€â€â€â€â€Žâ€â€â€Žâ€â€Žâ€â€â€Žâ€â€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€â€Žâ€â€â€â€Žâ€Žâ€â€â€Žâ€â€ŽVPN‎â€â€Žâ€Žâ€â€Ž"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"‎â€â€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€â€â€â€Žâ€Žâ€Žâ€Žâ€Žâ€Žâ€â€Žâ€Žâ€â€Žâ€Žâ€Žâ€Žâ€â€â€â€â€â€â€â€â€â€Žâ€â€â€Žâ€Žâ€â€Žâ€Žâ€â€â€â€Žâ€Žâ€â€Žâ€â€â€Žâ€â€Žâ€â€â€â€â€Žâ€â€Žâ€â€â€Žâ€Žâ€â€â€â€Žâ€â€Žâ€â€Žâ€â€Žâ€Žâ€â€Žâ€â€â€Žâ€Žâ€â€Žâ€â€Žâ€â€â€Žâ€â€â€â€â€Žâ€Žâ€Žan unknown network type‎â€â€Žâ€Žâ€â€Ž"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..e5f1833fdd6b56de40ba3c5207af88429d85efec --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividad del sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Accede a una red Wi-Fi."</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Acceder a la red"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>no tiene acceso a Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Presiona para ver opciones"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"La red móvil no tiene acceso a Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"La red no tiene acceso a Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"No se puede acceder al servidor DNS privado"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene conectividad limitada"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Presiona para conectarte de todas formas"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Se cambió a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"El dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cuando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tiene acceso a Internet. Es posible que se apliquen cargos."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Se cambió de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"Datos móviles"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipo de red desconocido"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-es/strings.xml b/service/ServiceConnectivityResources/res/values-es/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..e4f4307f338206e9e421d25ce4c72330781dad32 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-es/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividad del sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Iniciar sesión en red Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Iniciar sesión en la red"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> no tiene acceso a Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toca para ver opciones"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"La red móvil no tiene acceso a Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"La red no tiene acceso a Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"No se ha podido acceder al servidor DNS privado"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiene una conectividad limitada"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toca para conectarte de todas formas"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Se ha cambiado a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"El dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cuando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> no tiene acceso a Internet. Es posible que se apliquen cargos."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Se ha cambiado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"datos móviles"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipo de red desconocido"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-et/strings.xml b/service/ServiceConnectivityResources/res/values-et/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..cec408fd948369bd15dcf99bd9562660befd8e02 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-et/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Süsteemi ühenduvuse allikad"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Logi sisse WiFi-võrku"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Võrku sisselogimine"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Võrgul <xliff:g id="NETWORK_SSID">%1$s</xliff:g> puudub Interneti-ühendus"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Puudutage valikute nägemiseks"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiilsidevõrgul puudub Interneti-ühendus"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Võrgul puudub Interneti-ühendus"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Privaatsele DNS-serverile ei pääse juurde"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Võrgu <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ühendus on piiratud"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Puudutage, kui soovite siiski ühenduse luua"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Lülitati võrgule <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Seade kasutab võrku <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, kui võrgul <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> puudub juurdepääs Internetile. Rakenduda võivad tasud."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Lülitati võrgult <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> võrgule <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobiilne andmeside"</item> + <item msgid="6341719431034774569">"WiFi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"tundmatu võrgutüüp"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..f3ee9b1287c2523ad179ceb94bbd3c3821bdff9a --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistemaren konexio-baliabideak"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Hasi saioa Wi-Fi sarean"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Hasi saioa sarean"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Ezin da konektatu Internetera <xliff:g id="NETWORK_SSID">%1$s</xliff:g> sarearen bidez"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Sakatu aukerak ikusteko"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Sare mugikorra ezin da konektatu Internetera"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Sarea ezin da konektatu Internetera"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ezin da atzitu DNS zerbitzari pribatua"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sareak konektagarritasun murriztua du"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Sakatu hala ere konektatzeko"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> erabiltzen ari zara orain"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> Internetera konektatzeko gauza ez denean, <xliff:g id="NEW_NETWORK">%1$s</xliff:g> erabiltzen du gailuak. Agian kostuak ordaindu beharko dituzu."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> erabiltzen ari zinen, baina <xliff:g id="NEW_NETWORK">%2$s</xliff:g> erabiltzen ari zara orain"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"datu-konexioa"</item> + <item msgid="6341719431034774569">"Wifia"</item> + <item msgid="5081440868800877512">"Bluetooth-a"</item> + <item msgid="1160736166977503463">"Ethernet-a"</item> + <item msgid="7347618872551558605">"VPNa"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"sare mota ezezaguna"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0c5b147e5afef402f55fb0e794bd5ac064b25503 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"منابع اتصال سیستم"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"â€ÙˆØ±ÙˆØ¯ به شبکه Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ورود به سیستم شبکه"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> به اینترنت دسترسی ندارد"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"برای گزینه‌ها ضربه بزنید"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"شبکه تلÙÙ† همراه به اینترنت دسترسی ندارد"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"شبکه به اینترنت دسترسی ندارد"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"â€Ø³Ø±ÙˆØ± DNS خصوصی قابل دسترسی نیست"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> اتصال Ù…Øدودی دارد"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"به‌هرصورت، برای اتصال ضربه بزنید"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"به <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> تغییر کرد"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"وقتی <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> به اینترنت دسترسی نداشته باشد، دستگاه از <xliff:g id="NEW_NETWORK">%1$s</xliff:g> استÙاده می‌کند. ممکن است هزینه‌هایی اعمال شود."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"از <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> به <xliff:g id="NEW_NETWORK">%2$s</xliff:g> تغییر کرد"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"داده تلÙÙ† همراه"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"بلوتوث"</item> + <item msgid="1160736166977503463">"اترنت"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"نوع شبکه نامشخص"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-fi/strings.xml b/service/ServiceConnectivityResources/res/values-fi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..84c0034f8b2e53eeba3bd55e7b3e5143a5e39ed1 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-fi/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Järjestelmän yhteysresurssit"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Kirjaudu Wi-Fi-verkkoon"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Kirjaudu verkkoon"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ei ole yhteydessä internetiin"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Näytä vaihtoehdot napauttamalla."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiiliverkko ei ole yhteydessä internetiin"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Verkko ei ole yhteydessä internetiin"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ei pääsyä yksityiselle DNS-palvelimelle"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> toimii rajoitetulla yhteydellä"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Yhdistä napauttamalla"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> otettiin käyttöön"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> otetaan käyttöön, kun <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ei voi muodostaa yhteyttä internetiin. Veloitukset ovat mahdollisia."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> poistettiin käytöstä ja <xliff:g id="NEW_NETWORK">%2$s</xliff:g> otettiin käyttöön."</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobiilidata"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"tuntematon verkon tyyppi"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0badf1be0f9b10553cf337f0344020dc8bd3eb10 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ressources de connectivité système"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Connectez-vous au réseau Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Connectez-vous au réseau"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> n\'offre aucun accès à Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Touchez pour afficher les options"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Le réseau cellulaire n\'offre aucun accès à Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Le réseau n\'offre aucun accès à Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Impossible d\'accéder au serveur DNS privé"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Le réseau <xliff:g id="NETWORK_SSID">%1$s</xliff:g> offre une connectivité limitée"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Touchez pour vous connecter quand même"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Passé au réseau <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"L\'appareil utilise <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quand <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> n\'a pas d\'accès à Internet. Des frais peuvent s\'appliquer."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Passé du réseau <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> au <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"données cellulaires"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"RPV"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un type de réseau inconnu"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-fr/strings.xml b/service/ServiceConnectivityResources/res/values-fr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4835258d9e3262c84576f0cf320597968523936 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-fr/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ressources de connectivité système"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Connectez-vous au réseau Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Se connecter au réseau"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Aucune connexion à Internet pour <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Appuyez ici pour afficher des options."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Le réseau mobile ne dispose d\'aucun accès à Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Le réseau ne dispose d\'aucun accès à Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Impossible d\'accéder au serveur DNS privé"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"La connectivité de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> est limitée"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Appuyer pour se connecter quand même"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Nouveau réseau : <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"L\'appareil utilise <xliff:g id="NEW_NETWORK">%1$s</xliff:g> lorsque <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> n\'a pas de connexion Internet. Des frais peuvent s\'appliquer."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Ancien réseau : <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>. Nouveau réseau : <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"données mobiles"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"type de réseau inconnu"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-gl/strings.xml b/service/ServiceConnectivityResources/res/values-gl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..dfe813746e81b9496b424ce66f01bafdbcf1da32 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-gl/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividade do sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Inicia sesión na rede wifi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Inicia sesión na rede"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ten acceso a Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toca para ver opcións."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"A rede de telefonÃa móbil non ten acceso a Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede non ten acceso a Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Non se puido acceder ao servidor DNS privado"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"A conectividade de <xliff:g id="NETWORK_SSID">%1$s</xliff:g> é limitada"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toca para conectarte de todas formas"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Cambiouse a: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> cando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> non ten acceso a Internet. Pódense aplicar cargos."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Cambiouse de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"datos móbiles"</item> + <item msgid="6341719431034774569">"wifi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tipo de rede descoñecido"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-gu/strings.xml b/service/ServiceConnectivityResources/res/values-gu/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..e49b11d21ecf52ebbbd790faa2e768cffe247fa4 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-gu/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"સિસà«àªŸàª®àª¨à«€ કનેકà«àªŸàª¿àªµàª¿àªŸà«€àª¨àª¾àª‚ સાધનો"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"વાઇ-ફાઇ નેટવરà«àª• પર સાઇન ઇન કરો"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"નેટવરà«àª• પર સાઇન ઇન કરો"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ઇનà«àªŸàª°àª¨à«‡àªŸ àªàª•à«àª¸à«‡àª¸ ધરાવતà«àª‚ નથી"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"વિકલà«àªªà«‹ માટે ટૅપ કરો"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"મોબાઇલ નેટવરà«àª• કોઈ ઇનà«àªŸàª°àª¨à«‡àªŸ àªàª•à«àª¸à«‡àª¸ ધરાવતà«àª‚ નથી"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"નેટવરà«àª• કોઈ ઇનà«àªŸàª°àª¨à«‡àªŸ àªàª•à«àª¸à«‡àª¸ ધરાવતà«àª‚ નથી"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ખાનગી DNS સરà«àªµàª° àªàª•à«àª¸à«‡àª¸ કરી શકાતા નથી"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> મરà«àª¯àª¾àª¦àª¿àª¤ કનેકà«àªŸàª¿àªµàª¿àªŸà«€ ધરાવે છે"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"છતાં કનેકà«àªŸ કરવા માટે ટૅપ કરો"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> પર સà«àªµàª¿àªš કરà«àª¯à«àª‚"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"જà«àª¯àª¾àª°à«‡ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> પાસે કોઈ ઇનà«àªŸàª°àª¨à«‡àªŸ àªàª•à«àª¸à«‡àª¸ ન હોય તà«àª¯àª¾àª°à«‡ ઉપકરણ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>નો ઉપયોગ કરે છે. શà«àª²à«àª• લાગૠથઈ શકે છે."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> પરથી <xliff:g id="NEW_NETWORK">%2$s</xliff:g> પર સà«àªµàª¿àªš કરà«àª¯à«àª‚"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"મોબાઇલ ડેટા"</item> + <item msgid="6341719431034774569">"વાઇ-ફાઇ"</item> + <item msgid="5081440868800877512">"બà«àª²à«‚ટૂથ"</item> + <item msgid="1160736166977503463">"ઇથરનેટ"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"કોઈ અજાણà«àª¯à«‹ નેટવરà«àª•àª¨à«‹ પà«àª°àª•àª¾àª°"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-hi/strings.xml b/service/ServiceConnectivityResources/res/values-hi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..80ed699768e8b0d5eed4d18e7d529fa55a65e183 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-hi/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"सिसà¥à¤Ÿà¤® कनेकà¥à¤Ÿà¤¿à¤µà¤¿à¤Ÿà¥€ के संसाधन"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"वाई-फ़ाई नेटवरà¥à¤• में साइन इन करें"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"नेटवरà¥à¤• में साइन इन करें"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> का इंटरनेट नहीं चल रहा है"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"विकलà¥à¤ªà¥‹à¤‚ के लिठटैप करें"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"मोबाइल नेटवरà¥à¤• पर इंटरनेट à¤à¤•à¥à¤¸à¥‡à¤¸ नहीं है"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"इस नेटवरà¥à¤• पर इंटरनेट à¤à¤•à¥à¤¸à¥‡à¤¸ नहीं है"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"निजी डीà¤à¤¨à¤à¤¸ सरà¥à¤µà¤° को à¤à¤•à¥à¤¸à¥‡à¤¸ नहीं किया जा सकता"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> की कनेकà¥à¤Ÿà¤¿à¤µà¤¿à¤Ÿà¥€ सीमित है"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"फिर à¤à¥€ कनेकà¥à¤Ÿ करने के लिठटैप करें"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> पर ले जाया गया"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> में इंटरनेट की सà¥à¤µà¤¿à¤§à¤¾ नहीं होने पर डिवाइस <xliff:g id="NEW_NETWORK">%1$s</xliff:g> का इसà¥à¤¤à¥‡à¤®à¤¾à¤² करता है. इसके लिठशà¥à¤²à¥à¤• लिया जा सकता है."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> से <xliff:g id="NEW_NETWORK">%2$s</xliff:g> पर ले जाया गया"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"मोबाइल डेटा"</item> + <item msgid="6341719431034774569">"वाई-फ़ाई"</item> + <item msgid="5081440868800877512">"बà¥à¤²à¥‚टूथ"</item> + <item msgid="1160736166977503463">"ईथरनेट"</item> + <item msgid="7347618872551558605">"वीपीà¤à¤¨"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"अजà¥à¤žà¤¾à¤¤ नेटवरà¥à¤• टाइप"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-hr/strings.xml b/service/ServiceConnectivityResources/res/values-hr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..24bb22fd69210c181e27b406a8d7aa56b6a64591 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-hr/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resursi za povezivost sustava"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijava na Wi-Fi mrežu"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Prijava na mrežu"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nema pristup internetu"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dodirnite za opcije"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilna mreža nema pristup internetu"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Mreža nema pristup internetu"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nije moguće pristupiti privatnom DNS poslužitelju"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ima ograniÄenu povezivost"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dodirnite da biste se ipak povezali"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Prelazak na drugu mrežu: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Kada <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nema pristup internetu, na ureÄ‘aju se upotrebljava <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Moguća je naplata naknade."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Mreža je promijenjena: <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> > <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobilni podaci"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nepoznata vrsta mreže"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-hu/strings.xml b/service/ServiceConnectivityResources/res/values-hu/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..47a1142ac32fe8f7212732312e1e3726e66370c5 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-hu/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Rendszerkapcsolat erÅ‘forrásai"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Bejelentkezés Wi-Fi hálózatba"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Bejelentkezés a hálózatba"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózaton nincs internet-hozzáférés"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Koppintson a beállÃtások megjelenÃtéséhez"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"A mobilhálózaton nincs internet-hozzáférés"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"A hálózaton nincs internet-hozzáférés"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"A privát DNS-kiszolgálóhoz nem lehet hozzáférni"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"A(z) <xliff:g id="NETWORK_SSID">%1$s</xliff:g> hálózat korlátozott kapcsolatot biztosÃt"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Koppintson, ha mindenképpen csatlakozni szeretne"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Ãtváltva erre: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> használata, ha nincs internet-hozzáférés <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>-kapcsolaton keresztül. A szolgáltató dÃjat számÃthat fel."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Ãtváltva <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-hálózatról erre: <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobiladatok"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ismeretlen hálózati tÃpus"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-hy/strings.xml b/service/ServiceConnectivityResources/res/values-hy/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..dd951e81827403ae46d7698a8ea4b88856b28f0c --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-hy/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Õ„Õ¸Ö‚Õ¿Ö„ Õ£Õ¸Ö€Õ®Õ¥Ö„ Wi-Fi ÖÕ¡Õ¶Ö"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Õ„Õ¸Ö‚Õ¿Ö„ Õ£Õ¸Ö€Õ®Õ¥Ö„ ÖÕ¡Õ¶Ö"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ÖÕ¡Õ¶ÖÕ¨ Õ¹Õ¸Ö‚Õ¶Õ« Õ´Õ¸Ö‚Õ¿Ö„ Õ«Õ¶Õ¿Õ¥Ö€Õ¶Õ¥Õ¿Õ«Õ¶"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Õ€ÕºÕ¥Ö„Õ Õ¨Õ¶Õ¿Ö€Õ¡Õ¶Ö„Õ¶Õ¥Ö€Õ¨ Õ¿Õ¥Õ½Õ¶Õ¥Õ¬Õ¸Ö‚ Õ°Õ¡Õ´Õ¡Ö€"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Ô²Õ»Õ»Õ¡ÕµÕ«Õ¶ ÖÕ¡Õ¶ÖÕ¨ Õ¹Õ« Õ¡ÕºÕ¡Õ°Õ¸Õ¾Õ¸Ö‚Õ´ Õ«Õ¶Õ¿Õ¥Ö€Õ¶Õ¥Õ¿ Õ¯Õ¡Õº"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Õ‘Õ¡Õ¶ÖÕ¨ Õ´Õ«Õ¡ÖÕ¾Õ¡Õ® Õ¹Õ§ Õ«Õ¶Õ¿Õ¥Ö€Õ¶Õ¥Õ¿Õ«Õ¶"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Õ„Õ¡Õ½Õ¶Õ¡Õ¾Õ¸Ö€ DNS Õ½Õ¥Ö€Õ¾Õ¥Ö€Õ¶ Õ¡Õ¶Õ°Õ¡Õ½Õ¡Õ¶Õ¥Õ¬Õ« Õ§"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ÖÕ¡Õ¶ÖÕ« Õ¯Õ¡ÕºÕ¨ Õ½Õ¡Õ°Õ´Õ¡Õ¶Õ¡ÖƒÕ¡Õ¯ Õ§"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Õ€ÕºÕ¥Ö„Õ Õ´Õ«Õ¡Õ¶Õ¡Õ¬Õ¸Ö‚ Õ°Õ¡Õ´Õ¡Ö€"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Ô±Õ¶ÖÕ¥Õ¬ Õ§ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ÖÕ¡Õ¶ÖÕ«"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"ÔµÖ€Õ¢ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ÖÕ¡Õ¶ÖÕ¸Ö‚Õ´ Õ«Õ¶Õ¿Õ¥Ö€Õ¶Õ¥Õ¿ Õ¯Õ¡Õº Õ¹Õ« Õ¬Õ«Õ¶Õ¸Ö‚Õ´, Õ½Õ¡Ö€Ö„Õ¶ Õ¡Õ¶ÖÕ¶Õ¸Ö‚Õ´ Õ§ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ÖÕ¡Õ¶ÖÕ«: Õ†Õ´Õ¡Õ¶ Õ¤Õ¥ÕºÖ„Õ¸Ö‚Õ´ Õ¯Õ¡Ö€Õ¸Õ² Õ¥Õ¶ Õ¾Õ³Õ¡Ö€Õ¶Õ¥Ö€ Õ£Õ¡Õ¶Õ±Õ¾Õ¥Õ¬:"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ÖÕ¡Õ¶ÖÕ«Ö Õ¡Õ¶ÖÕ¥Õ¬ Õ§ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ÖÕ¡Õ¶ÖÕ«"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"Õ¢Õ»Õ»Õ¡ÕµÕ«Õ¶ Õ«Õ¶Õ¿Õ¥Ö€Õ¶Õ¥Õ¿"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ÖÕ¡Õ¶ÖÕ« Õ¡Õ¶Õ°Õ¡ÕµÕ¿ Õ¿Õ¥Õ½Õ¡Õ¯"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-in/strings.xml b/service/ServiceConnectivityResources/res/values-in/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..d559f6b11e8960d5c990bb00fd9b093c8a40bc10 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-in/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resource Konektivitas Sistem"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Login ke jaringan Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Login ke jaringan"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tidak memiliki akses internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ketuk untuk melihat opsi"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Jaringan seluler tidak memiliki akses internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Jaringan tidak memiliki akses internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Server DNS pribadi tidak dapat diakses"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> memiliki konektivitas terbatas"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ketuk untuk tetap menyambungkan"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Dialihkan ke <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Perangkat menggunakan <xliff:g id="NEW_NETWORK">%1$s</xliff:g> jika <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tidak memiliki akses internet. Tarif mungkin berlaku."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Dialihkan dari <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ke <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"data seluler"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"jenis jaringan yang tidak dikenal"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-is/strings.xml b/service/ServiceConnectivityResources/res/values-is/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..877c85f147928c81c28b28f243ebc69298db418a --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-is/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Tengigögn kerfis"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Skrá inn á Wi-Fi net"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Skrá inn á net"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> er ekki með internetaðgang"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ãttu til að sjá valkosti"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"FarsÃmakerfið er ekki tengt við internetið"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Netkerfið er ekki tengt við internetið"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ekki næst à DNS-einkaþjón"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Tengigeta <xliff:g id="NETWORK_SSID">%1$s</xliff:g> er takmörkuð"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ãttu til að tengjast samt"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Skipt yfir á <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Tækið notar <xliff:g id="NEW_NETWORK">%1$s</xliff:g> þegar <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> er ekki með internetaðgang. Gjöld kunna að eiga við."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Skipt úr <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> yfir à <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"farsÃmagögn"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"óþekkt tegund netkerfis"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-it/strings.xml b/service/ServiceConnectivityResources/res/values-it/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..bcac393c9c57db739d8ab651829d8b4538cc8a73 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-it/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Risorse per connettività di sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Accedi a rete Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Accedi alla rete"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> non ha accesso a Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tocca per le opzioni"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"La rete mobile non ha accesso a Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"La rete non ha accesso a Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Non è possibile accedere al server DNS privato"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ha una connettività limitata"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tocca per connettere comunque"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Passato a <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Il dispositivo utilizza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> non ha accesso a Internet. Potrebbero essere applicati costi."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Passato da <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> a <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"dati mobili"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"tipo di rete sconosciuto"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-iw/strings.xml b/service/ServiceConnectivityResources/res/values-iw/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..d6684ce1062e6e7fbdf2d8119998a8c73c7614a4 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-iw/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"מש×בי קישוריות מערכת"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"â€×”×™×›× ×¡ לרשת Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"×”×™×›× ×¡ לרשת"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"ל-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ×ין גישה ל××™× ×˜×¨× ×˜"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"הקש לקבלת ×פשרויות"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"לרשת הסלולרית ×ין גישה ל××™× ×˜×¨× ×˜"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"לרשת ×ין גישה ל××™× ×˜×¨× ×˜"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"â€×œ× × ×™×ª×Ÿ לגשת לשרת DNS הפרטי"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"הקישוריות של <xliff:g id="NETWORK_SSID">%1$s</xliff:g> מוגבלת"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"כדי להתחבר למרות ×–×ת יש להקיש"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"מעבר ×ל <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"המכשיר משתמש ברשת <xliff:g id="NEW_NETWORK">%1$s</xliff:g> כשלרשת <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ×ין גישה ל××™× ×˜×¨× ×˜. ×¢×©×•×™×™× ×œ×—×•×œ חיובי×."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"עבר מרשת <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> לרשת <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"חבילת גלישה"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"××ª×¨× ×˜"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"סוג רשת ×œ× ×ž×–×•×”×”"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ja/strings.xml b/service/ServiceConnectivityResources/res/values-ja/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..fa4a30aedb225b31ae5941692552763633321562 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ja/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"システム接続リソース"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fiãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«ãƒã‚°ã‚¤ãƒ³"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«ãƒã‚°ã‚¤ãƒ³ã—ã¦ãã ã•ã„"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ã¯ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"タップã—ã¦ãã®ä»–ã®ã‚ªãƒ—ションを表示"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"モãƒã‚¤ãƒ« ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ãŒã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã›ã‚“"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ãŒã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆã«æŽ¥ç¶šã•ã‚Œã¦ã„ã¾ã›ã‚“"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"プライベート DNS サーãƒãƒ¼ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ã®æŽ¥ç¶šãŒåˆ¶é™ã•ã‚Œã¦ã„ã¾ã™"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"接続ã™ã‚‹ã«ã¯ã‚¿ãƒƒãƒ—ã—ã¦ãã ã•ã„"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"「<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>ã€ã«åˆ‡ã‚Šæ›¿ãˆã¾ã—ãŸ"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"デãƒã‚¤ã‚¹ã§ã€Œ<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ã€ã«ã‚ˆã‚‹ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆæŽ¥ç¶šãŒã§ããªã„å ´åˆã«ã€Œ<xliff:g id="NEW_NETWORK">%1$s</xliff:g>ã€ã‚’使用ã—ã¾ã™ã€‚通信料ãŒç™ºç”Ÿã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"「<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>ã€ã‹ã‚‰ã€Œ<xliff:g id="NEW_NETWORK">%2$s</xliff:g>ã€ã«åˆ‡ã‚Šæ›¿ãˆã¾ã—ãŸ"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"モãƒã‚¤ãƒ«ãƒ‡ãƒ¼ã‚¿"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"イーサãƒãƒƒãƒˆ"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ä¸æ˜Žãªãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ タイプ"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ka/strings.xml b/service/ServiceConnectivityResources/res/values-ka/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..418331036efcb158f07be9dc56744d9aceb008f5 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ka/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"სისტემის კáƒáƒ•áƒ¨áƒ˜áƒ ის რესურსები"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi ქსელთáƒáƒœ დáƒáƒ™áƒáƒ•áƒ¨áƒ˜áƒ ებáƒ"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ქსელში შესვლáƒ"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ს áƒáƒ áƒáƒ¥áƒ•áƒ¡ ინტერნეტზე წვდáƒáƒ›áƒ"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"შეეხეთ ვáƒáƒ იáƒáƒœáƒ¢áƒ”ბის სáƒáƒœáƒáƒ®áƒáƒ•áƒáƒ“"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"მáƒáƒ‘ილურქსელს áƒáƒ áƒáƒ¥áƒ•áƒ¡ ინტერნეტზე წვდáƒáƒ›áƒ"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ქსელს áƒáƒ áƒáƒ¥áƒ•áƒ¡ ინტერნეტზე წვდáƒáƒ›áƒ"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"პირáƒáƒ“ DNS სერვერზე წვდáƒáƒ›áƒ შეუძლებელიáƒ"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-ის კáƒáƒ•áƒ¨áƒ˜áƒ ები შეზღუდულიáƒ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"შეეხეთ, თუ მáƒáƒ˜áƒœáƒª გსურთ დáƒáƒ™áƒáƒ•áƒ¨áƒ˜áƒ ებáƒ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"áƒáƒ®áƒšáƒ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებრ<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"თუ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ინტერნეტთáƒáƒœ კáƒáƒ•áƒ¨áƒ˜áƒ ს დáƒáƒ™áƒáƒ გáƒáƒ•áƒ¡, მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ის მიერ<xliff:g id="NEW_NETWORK">%1$s</xliff:g> იქნებრგáƒáƒ›áƒáƒ§áƒ”ნებული, რáƒáƒ›áƒáƒª შეიძლებრდáƒáƒ›áƒáƒ¢áƒ”ბითი ხáƒáƒ ჯები გáƒáƒ›áƒáƒ˜áƒ¬áƒ•áƒ˜áƒáƒ¡."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"áƒáƒ®áƒšáƒ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებრ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> (გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებáƒáƒ“რ<xliff:g id="NEW_NETWORK">%2$s</xliff:g>)"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"მáƒáƒ‘ილური ინტერნეტი"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"უცნáƒáƒ‘ი ტიპის ქსელი"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-kk/strings.xml b/service/ServiceConnectivityResources/res/values-kk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..54d5eb30cd1d9c914c19c95bbc8af35b6031ea6a --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-kk/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Жүйе байланыÑÑ‹ реÑурÑтары"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi желіÑіне кіру"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Желіге кіру"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желіÑінің интернетті пайдалану мүмкіндігі шектеулі."</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ОпциÑлар үшін түртіңіз"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобильдік желі интернетке қоÑылмаған."</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Желі интернетке қоÑылмаған."</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Жеке DNS Ñерверіне кіру мүмкін емеÑ."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> желіÑінің қоÑылу мүмкіндігі шектеулі."</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Бәрібір жалғау үшін түртіңіз."</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> желіÑіне ауыÑÑ‚Ñ‹"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Құрылғы <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> желіÑінде интернетпен Ð±Ð°Ð¹Ð»Ð°Ð½Ñ‹Ñ Ð¶Ð¾Ò“Ð°Ð»Ò“Ð°Ð½ жағдайда <xliff:g id="NEW_NETWORK">%1$s</xliff:g> желіÑін пайдаланады. Деректер ақыÑÑ‹ алынуы мүмкін."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> желіÑінен <xliff:g id="NEW_NETWORK">%2$s</xliff:g> желіÑіне ауыÑÑ‚Ñ‹"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобильдік деректер"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"желі түрі белгіÑіз"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-km/strings.xml b/service/ServiceConnectivityResources/res/values-km/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..bd778a1623b05070fc9033c28f99b49e49ffe0fa --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-km/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ធនធាន​ážáž—្ជាប់​ប្រពáŸáž“្ធ"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ចូល​បណ្ដាញ​វ៉ាយហ្វាយ"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ចូលទៅបណ្ážáž¶áž‰"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មិនមាន​ការážáž—្ជាប់អ៊ីនធឺណិážâ€‹áž‘áŸ"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ប៉ះសម្រាប់ជម្រើស"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"បណ្ដាញ​ទូរសព្ទ​ចលáŸážâ€‹áž˜áž·áž“មានការážáž—្ជាប់​អ៊ីនធឺណិážáž‘áŸ"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"បណ្ដាញ​មិនមាន​ការážáž—្ជាប់​អ៊ីនធឺណិážáž‘áŸ"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"មិនអាច​ចូលប្រើ​ម៉ាស៊ីនម០DNS ឯកជន​បានទáŸ"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> មានការážáž—្ជាប់​មានកម្រិáž"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"មិន​អី​ទ០ចុច​​ភ្ជាប់​ចុះ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"បានប្ážáž¼ážšáž‘ៅ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"ឧបករណáŸâ€‹áž”្រើ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> នៅ​ពáŸáž›â€‹ážŠáŸ‚áž› <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> មិនមាន​ការ​ážáž—្ជាប់​អ៊ីនធឺណិážáŸ” អាច​គិážážáŸ’លៃ​លើការ​ប្រើប្រាស់​ទិន្ននáŸáž™áŸ”"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"បានប្ážáž¼ážšáž–ី <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ទៅ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ទិន្ននáŸáž™â€‹áž‘ូរសព្ទចលáŸáž"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"ប៊្លូធូស"</item> + <item msgid="1160736166977503463">"អ៊ីសឺរណិáž"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ប្រភáŸáž‘បណ្ážáž¶áž‰ážŠáŸ‚លមិនស្គាល់"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-kn/strings.xml b/service/ServiceConnectivityResources/res/values-kn/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7f3a420dac78688c2f22109a5a930c1e3a826bf0 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-kn/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ಸಿಸà³à²Ÿà²‚ ಸಂಪರà³à²• ಕಲà³à²ªà²¿à²¸à³à²µà²¿à²•à³† ಮಾಹಿತಿಯ ಮೂಲಗಳà³"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ವೈ-ಫೈ ನೆಟà³â€à²µà²°à³à²•à³â€Œà²—ೆ ಸೈನೠಇನೠಮಾಡಿ"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ನೆಟà³â€Œà²µà²°à³à²•à³â€Œà²—ೆ ಸೈನೠಇನೠಮಾಡಿ"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಯಾವà³à²¦à³‡ ಇಂಟರà³à²¨à³†à²Ÿà³ ಸಂಪರà³à²•à²µà²¨à³à²¨à³ ಹೊಂದಿಲà³à²²"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ಆಯà³à²•à³†à²—ಳಿಗೆ ಟà³à²¯à²¾à²ªà³ ಮಾಡಿ"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ಮೊಬೈಲೠನೆಟà³â€Œà²µà²°à³à²•à³â€Œ ಯಾವà³à²¦à³‡ ಇಂಟರà³à²¨à³†à²Ÿà³ ಪà³à²°à²µà³‡à²¶à²µà²¨à³à²¨à³ ಹೊಂದಿಲà³à²²"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ನೆಟà³â€Œà²µà²°à³à²•à³â€Œ ಇಂಟರà³à²¨à³†à²Ÿà³â€Œ ಪà³à²°à²µà³‡à²¶à²µà²¨à³à²¨à³ ಹೊಂದಿಲà³à²²"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ಖಾಸಗಿ DNS ಸರà³à²µà²°à³ ಅನà³à²¨à³ ಪà³à²°à²µà³‡à²¶à²¿à²¸à²²à³ ಸಾಧà³à²¯à²µà²¿à²²à³à²²"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ಸೀಮಿತ ಸಂಪರà³à²• ಕಲà³à²ªà²¿à²¸à³à²µà²¿à²•à³†à²¯à²¨à³à²¨à³ ಹೊಂದಿದೆ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ಹೇಗಾದರೂ ಸಂಪರà³à²•à²¿à²¸à²²à³ ಟà³à²¯à²¾à²ªà³ ಮಾಡಿ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ಇಂಟರà³à²¨à³†à²Ÿà³ ಪà³à²°à²µà³‡à²¶ ಹೊಂದಿಲà³à²²à²¦à²¿à²°à³à²µà²¾à²—, ಸಾಧನವೠ<xliff:g id="NEW_NETWORK">%1$s</xliff:g> ಬಳಸà³à²¤à³à²¤à²¦à³†. ಶà³à²²à³à²•à²—ಳೠಅನà³à²µà²¯à²µà²¾à²—ಬಹà³à²¦à³."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ರಿಂದ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ಮೊಬೈಲೠಡೇಟಾ"</item> + <item msgid="6341719431034774569">"ವೈ-ಫೈ"</item> + <item msgid="5081440868800877512">"ಬà³à²²à³‚ಟೂತà³"</item> + <item msgid="1160736166977503463">"ಇಥರà³à²¨à³†à²Ÿà³"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ಅಪರಿಚಿತ ನೆಟà³â€Œà²µà²°à³à²•à³ ಪà³à²°à²•à²¾à²°"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ko/strings.xml b/service/ServiceConnectivityResources/res/values-ko/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a763cc5e9cb2ef6f469e6669a25bab3582c4c98d --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ko/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"시스템 ì—°ê²° 리소스"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi 네트워í¬ì— 로그ì¸"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"네트워í¬ì— 로그ì¸"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ì´(ê°€) ì¸í„°ë„·ì— ì•¡ì„¸ìŠ¤í• ìˆ˜ 없습니다."</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"íƒí•˜ì—¬ 옵션 보기"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ëª¨ë°”ì¼ ë„¤íŠ¸ì›Œí¬ì— ì¸í„°ë„·ì´ ì—°ê²°ë˜ì–´ 있지 않습니다."</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"네트워í¬ì— ì¸í„°ë„·ì´ ì—°ê²°ë˜ì–´ 있지 않습니다."</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"비공개 DNS ì„œë²„ì— ì•¡ì„¸ìŠ¤í• ìˆ˜ 없습니다."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ì—ì„œ ì—°ê²°ì„ ì œí•œí–ˆìŠµë‹ˆë‹¤."</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ê³„ì† ì—°ê²°í•˜ë ¤ë©´ íƒí•˜ì„¸ìš”."</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>(으)ë¡œ ì „í™˜"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>(으)ë¡œ ì¸í„°ë„·ì— ì—°ê²°í• ìˆ˜ 없는 경우 기기ì—ì„œ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ì´(ê°€) 사용ë©ë‹ˆë‹¤. ìš”ê¸ˆì´ ë¶€ê³¼ë 수 있습니다."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>ì—ì„œ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>(으)ë¡œ ì „í™˜"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ëª¨ë°”ì¼ ë°ì´í„°"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"블루투스"</item> + <item msgid="1160736166977503463">"ì´ë”ë„·"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ì•Œ 수 없는 ë„¤íŠ¸ì›Œí¬ ìœ í˜•"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ky/strings.xml b/service/ServiceConnectivityResources/res/values-ky/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..3550af80cdf9af5a0a4eff9cf9638bad59c4adc3 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ky/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Тутумдун байланыш булагы"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi түйүнүнө кирүү"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Тармакка кирүү"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Интернетке туташууÑу жок"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Параметрлерди ачуу үчүн таптап коюңуз"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилдик Интернет жок"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Тармактын Интернет жок"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Жеке DNS Ñервери жеткиликÑиз"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> байланышы чектелген"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Баары бир туташуу үчүн таптаңыз"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> тармагына которуштурулду"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> тармагы Интернетке туташпай турганда, түзмөгүңүз <xliff:g id="NEW_NETWORK">%1$s</xliff:g> тармагын колдонот. Ðкы алынышы мүмкүн."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> дегенден <xliff:g id="NEW_NETWORK">%2$s</xliff:g> тармагына которуштурулду"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобилдик трафик"</item> + <item msgid="6341719431034774569">"Wi‑Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"белгиÑиз тармак түрү"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-lo/strings.xml b/service/ServiceConnectivityResources/res/values-lo/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b3056f540489cf7f4b6e1e80b1cd9a5c1252067 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-lo/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"à»àº«àº¼à»ˆàº‡àº‚à»à»‰àº¡àº¹àº™àºàº²àº™à»€àºŠàº·à»ˆàºàº¡àº•à»à»ˆàº¥àº°àºšàº»àºš"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ເຂົ້າສູ່ລະບົບເຄືàºàº‚່າຠWi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ລົງຊື່ເຂົ້າເຄືàºàº‚່າàº"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ບà»à»ˆàº¡àºµàºàº²àº™à»€àºŠàº·à»ˆàºàº¡àº•à»à»ˆàºàº´àº™à»€àº•àºµà»€àº™àº±àº”"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"à»àº•àº°à»€àºžàº·à»ˆàºà»€àºšàº´à»ˆàº‡àº•àº»àº§à»€àº¥àº·àºàº"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ເຄືàºàº‚່າàºàº¡àº·àº–ືບà»à»ˆàºªàº²àº¡àº²àº”ເຂົ້າເຖິງàºàº´àº™à»€àº•àºµà»€àº™àº±àº”ໄດ້"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ເຄືàºàº‚່າàºàºšà»à»ˆàºªàº²àº¡àº²àº”ເຂົ້າເຖິງàºàº´àº™à»€àº•àºµà»€àº™àº±àº”ໄດ້"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ບà»à»ˆàºªàº²àº¡àº²àº”ເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ມີàºàº²àº™à»€àºŠàº·à»ˆàºàº¡àº•à»à»ˆàº—ີ່ຈຳàºàº±àº”"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"à»àº•àº°à»€àºžàº·à»ˆàºàº¢àº·àº™àº¢àº±àº™àºàº²àº™à»€àºŠàº·à»ˆàºàº¡àº•à»à»ˆ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"ສະຫຼັບໄປໃຊ້ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> à»àº¥à»‰àº§"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"àºàº¸àº›àº°àºàºàº™àºˆàº°à»ƒàºŠà»‰ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ເມື່ຠ<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ບà»à»ˆàº¡àºµàºàº²àº™à»€àºŠàº·à»ˆàºàº¡àº•à»à»ˆàºàº´àº™à»€àº•àºµà»€àº™àº±àº”. àºàº²àº”ມີàºàº²àº™àº®àº½àºà»€àºàº±àºšàº„່າບà»àº¥àº´àºàº²àº™."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"ສະຫຼັບຈາຠ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ໄປໃຊ້ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> à»àº¥à»‰àº§"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"àºàº´àº™à»€àº•àºµà»€àº™àº±àº”ມືຖື"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"àºàºµà»€àº—ີເນັດ"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ບà»à»ˆàº®àº¹à»‰àºˆàº±àºàº›àº°à»€àºžàº”ເຄືàºàº‚່າàº"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-lt/strings.xml b/service/ServiceConnectivityResources/res/values-lt/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..8eb41f1efa279e30ef4d5496f1c27cc2ff29fe1f --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-lt/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prisijungti prie „Wi-Fi“ tinklo"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Prisijungti prie tinklo"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ negali pasiekti interneto"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Palieskite, kad bÅ«tų rodomos parinktys."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiliojo ryÅ¡io tinkle nÄ—ra prieigos prie interneto"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Tinkle nÄ—ra prieigos prie interneto"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Privataus DNS serverio negalima pasiekti"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"„<xliff:g id="NETWORK_SSID">%1$s</xliff:g>“ ryÅ¡ys apribotas"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Palieskite, jei vis tiek norite prisijungti"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Perjungta į tinklÄ… <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Ä®renginyje naudojamas kitas tinklas (<xliff:g id="NEW_NETWORK">%1$s</xliff:g>), kai dabartiniame tinkle (<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>) nÄ—ra interneto ryÅ¡io. Gali bÅ«ti taikomi mokesÄiai."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Perjungta iÅ¡ tinklo <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> į tinklÄ… <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobiliojo ryÅ¡io duomenys"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Eternetas"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nežinomas tinklo tipas"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0647a4fb077e0084810eb3eee892411132cfb805 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"SistÄ“mas savienojamÄ«bas resursi"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Pierakstieties Wi-Fi tÄ«klÄ"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"PierakstÄ«Å¡anÄs tÄ«klÄ"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"TÄ«klÄ <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nav piekļuves internetam"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Pieskarieties, lai skatÄ«tu iespÄ“jas."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"MobilajÄ tÄ«klÄ nav piekļuves internetam."</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"TÄ«klÄ nav piekļuves internetam."</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Nevar piekļūt privÄtam DNS serverim."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"TÄ«klÄ <xliff:g id="NETWORK_SSID">%1$s</xliff:g> ir ierobežota savienojamÄ«ba"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Lai tik un tÄ izveidotu savienojumu, pieskarieties"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"PÄrslÄ“dzÄs uz tÄ«klu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Kad vienÄ tÄ«klÄ (<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>) nav piekļuves internetam, ierÄ«cÄ“ tiek izmantots cits tÄ«kls (<xliff:g id="NEW_NETWORK">%1$s</xliff:g>). Var tikt piemÄ“rota maksa."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"PÄrslÄ“dzÄs no tÄ«kla <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> uz tÄ«klu <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobilie dati"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nezinÄms tÄ«kla veids"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-mcc204-mnc04/config.xml b/service/ServiceConnectivityResources/res/values-mcc204-mnc04/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e7025fb042f7597e3cf8c3b4e05dbadb35097f1 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-mcc204-mnc04/config.xml @@ -0,0 +1,27 @@ +<?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 values for ConnectivityService + DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources + Overlay package following the overlayable.xml configuration in the same directory: + https://source.android.com/devices/architecture/rros --> +<resources> + <!-- Whether the device should automatically switch away from Wi-Fi networks that lose + Internet access. Actual device behaviour is controlled by + Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. --> + <integer translatable="false" name="config_networkAvoidBadWifi">0</integer> +</resources> \ No newline at end of file diff --git a/service/ServiceConnectivityResources/res/values-mcc310-mnc004/config.xml b/service/ServiceConnectivityResources/res/values-mcc310-mnc004/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e7025fb042f7597e3cf8c3b4e05dbadb35097f1 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-mcc310-mnc004/config.xml @@ -0,0 +1,27 @@ +<?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 values for ConnectivityService + DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources + Overlay package following the overlayable.xml configuration in the same directory: + https://source.android.com/devices/architecture/rros --> +<resources> + <!-- Whether the device should automatically switch away from Wi-Fi networks that lose + Internet access. Actual device behaviour is controlled by + Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. --> + <integer translatable="false" name="config_networkAvoidBadWifi">0</integer> +</resources> \ No newline at end of file diff --git a/service/ServiceConnectivityResources/res/values-mcc311-mnc480/config.xml b/service/ServiceConnectivityResources/res/values-mcc311-mnc480/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e7025fb042f7597e3cf8c3b4e05dbadb35097f1 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-mcc311-mnc480/config.xml @@ -0,0 +1,27 @@ +<?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 values for ConnectivityService + DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources + Overlay package following the overlayable.xml configuration in the same directory: + https://source.android.com/devices/architecture/rros --> +<resources> + <!-- Whether the device should automatically switch away from Wi-Fi networks that lose + Internet access. Actual device behaviour is controlled by + Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. --> + <integer translatable="false" name="config_networkAvoidBadWifi">0</integer> +</resources> \ No newline at end of file diff --git a/service/ServiceConnectivityResources/res/values-mk/strings.xml b/service/ServiceConnectivityResources/res/values-mk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..b0024e296cb379a5538cad9aeff100f3ee435f89 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-mk/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Ðајавете Ñе на мрежа на Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Ðајавете Ñе на мрежа"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема интернет-приÑтап"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Допрете за опции"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилната мрежа нема интернет-приÑтап"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Мрежата нема интернет-приÑтап"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ðе може да Ñе приÑтапи до приватниот DNS-Ñервер"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничена поврзливоÑÑ‚"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Допрете за да Ñе поврзете и покрај тоа"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Префрлено на <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Уредот кориÑти <xliff:g id="NEW_NETWORK">%1$s</xliff:g> кога <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нема приÑтап до интернет. Може да Ñе наплатат трошоци."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Префрлено од <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобилен интернет"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Етернет"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"непознат тип мрежа"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ml/strings.xml b/service/ServiceConnectivityResources/res/values-ml/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..8ce7667fbf963a42984d86ce130b9bfdea44b89b --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ml/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"സിസàµâ€Œà´±àµà´±à´‚ കണകàµâ€Œà´±àµà´±à´¿à´µà´¿à´±àµà´±à´¿ ഉറവിടങàµà´™àµ¾"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"വൈഫൈ നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•à´¿à´²àµ‡à´•àµà´•àµ സൈൻ ഇൻ ചെയàµà´¯àµà´•"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•à´¿à´²àµ‡à´•àµà´•àµ സൈൻ ഇൻ ചെയàµà´¯àµà´•"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> à´Žà´¨àµà´¨à´¤à´¿à´¨àµ ഇനàµà´±àµ¼à´¨àµ†à´±àµà´±àµ ആകàµâ€Œà´¸à´¸àµ ഇലàµà´²"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"à´“à´ªàµà´·à´¨àµà´•àµ¾à´•àµà´•àµ ടാപàµà´ªàµà´šàµ†à´¯àµà´¯àµà´•"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"മൊബെെൽ നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•à´¿à´¨àµ ഇനàµà´±àµ¼à´¨àµ†à´±àµà´±àµ ആകàµâ€Œà´¸à´¸àµ ഇലàµà´²"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•à´¿à´¨àµ ഇനàµà´±àµ¼à´¨àµ†à´±àµà´±àµ ആകàµâ€Œà´¸à´¸àµ ഇലàµà´²"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"à´¸àµà´µà´•à´¾à´°àµà´¯ DNS സെർവർ ആകàµâ€Œà´¸à´¸àµ ചെയàµà´¯à´¾à´¨à´¾à´µà´¿à´²àµà´²"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> à´Žà´¨àµà´¨à´¤à´¿à´¨àµ പരിമിതമായ കണകàµà´±àµà´±à´¿à´µà´¿à´±àµà´±à´¿ ഉണàµà´Ÿàµ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"à´à´¤àµà´µà´¿à´§àµ‡à´¨à´¯àµà´‚ കണകàµâ€Œà´±àµà´±àµ ചെയàµà´¯à´¾àµ» ടാപàµà´ªàµ ചെയàµà´¯àµà´•"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> à´Žà´¨àµà´¨à´¤à´¿à´²àµ‡à´•àµà´•àµ മാറി"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>-നൠഇനàµà´±àµ¼à´¨àµ†à´±àµà´±àµ ആകàµâ€Œà´¸à´¸àµ ഇലàµà´²à´¾à´¤àµà´¤à´ªàµà´ªàµ‹àµ¾ ഉപകരണം <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ഉപയോഗികàµà´•àµà´¨àµà´¨àµ. നിരകàµà´•àµà´•àµ¾ ബാധകമായേകàµà´•à´¾à´‚."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•à´¿àµ½ നിനàµà´¨àµ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•à´¿à´²àµ‡à´•àµà´•àµ മാറി"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"മൊബൈൽ ഡാറàµà´±"</item> + <item msgid="6341719431034774569">"വൈഫൈ"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"ഇതർനെറàµà´±àµ"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"à´…à´œàµà´žà´¾à´¤à´®à´¾à´¯ നെറàµà´±àµâ€Œà´µàµ¼à´•àµà´•àµ തരം"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-mn/strings.xml b/service/ServiceConnectivityResources/res/values-mn/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..be8b59202baaace7b8082d8f3f64e7552b1aa13f --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-mn/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"СиÑтемийн холболтын нөөцүүд"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi ÑүлжÑÑнд нÑвтÑÑ€Ð½Ñ Ò¯Ò¯"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"СүлжÑÑнд нÑвтÑÑ€Ð½Ñ Ò¯Ò¯"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>-д интернÑтийн хандалт алга"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Сонголт хийхийн тулд товшино уу"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобайл ÑүлжÑÑнд интернÑÑ‚ хандалт байхгүй байна"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"СүлжÑÑнд интернÑÑ‚ хандалт байхгүй байна"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Хувийн DNS Ñерверт хандах боломжгүй байна"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> зарим үйлчилгÑÑнд хандах боломжгүй байна"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ямар ч тохиолдолд холбогдохын тулд товших"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> руу шилжүүлÑÑн"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> интернет холболтгүй үед төхөөрөмж <xliff:g id="NEW_NETWORK">%1$s</xliff:g>-г ашигладаг. Төлбөр гарч болзошгүй."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>-Ñ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> руу шилжүүлÑÑн"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобайл дата"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"ÐтернÑÑ‚"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"үл мÑдÑгдÑÑ… ÑүлжÑÑний төрөл"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-mr/strings.xml b/service/ServiceConnectivityResources/res/values-mr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..fe7df841b147414067eae7a216c5e7a5972c602c --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-mr/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"सिसà¥à¤Ÿà¤® कनेकà¥à¤Ÿà¤¿à¤µà¥à¤¹à¤¿à¤Ÿà¥€ चे सà¥à¤°à¥‹à¤¤"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"वाय-फाय नेटवरà¥à¤•à¤®à¤§à¥â€à¤¯à¥‡ साइन इन करा"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"नेटवरà¥à¤•à¤µà¤° साइन इन करा"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला इंटरनेट अâ€à¥…कà¥à¤¸à¥‡à¤¸ नाही"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"परà¥à¤¯à¤¾à¤¯à¤¾à¤‚साठी टॅप करा"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"मोबाइल नेटवरà¥à¤•à¤²à¤¾ इंटरनेट ॲकà¥à¤¸à¥‡à¤¸ नाही"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"नेटवरà¥à¤•à¤²à¤¾ इंटरनेट ॲकà¥à¤¸à¥‡à¤¸ नाही"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"खाजगी DNS सरà¥à¤µà¥à¤¹à¤° ॲकà¥à¤¸à¥‡à¤¸ करू शकत नाही"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ला मरà¥à¤¯à¤¾à¤¦à¤¿à¤¤ कनेकà¥à¤Ÿà¤¿à¤µà¥à¤¹à¤¿à¤Ÿà¥€ आहे"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"तरीही कनेकà¥à¤Ÿ करणà¥à¤¯à¤¾à¤¸à¤¾à¤ ी टॅप करा"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> वर सà¥à¤µà¤¿à¤š केले"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> कडे इंटरनेटचा अâ€à¥…कà¥à¤¸à¥‡à¤¸ नसताना डिवà¥à¤¹à¤¾à¤‡à¤¸ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> वापरते. शà¥à¤²à¥à¤• लागू शकते."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> वरून <xliff:g id="NEW_NETWORK">%2$s</xliff:g> वर सà¥à¤µà¤¿à¤š केले"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"मोबाइल डेटा"</item> + <item msgid="6341719431034774569">"वाय-फाय"</item> + <item msgid="5081440868800877512">"बà¥à¤²à¥‚टूथ"</item> + <item msgid="1160736166977503463">"इथरनेट"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"अजà¥à¤žà¤¾à¤¤ नेटवरà¥à¤• पà¥à¤°à¤•à¤¾à¤°"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ms/strings.xml b/service/ServiceConnectivityResources/res/values-ms/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..54b49a20f7eb4f9fa330166c3289dd04641245cd --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ms/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sumber Kesambungan Sistem"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Log masuk ke rangkaian Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Log masuk ke rangkaian"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tiada akses Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ketik untuk mendapatkan pilihan"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Rangkaian mudah alih tiada akses Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Rangkaian tiada akses Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Pelayan DNS peribadi tidak boleh diakses"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> mempunyai kesambungan terhad"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ketik untuk menyambung juga"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Beralih kepada <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Peranti menggunakan <xliff:g id="NEW_NETWORK">%1$s</xliff:g> apabila <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tiada akses Internet. Bayaran mungkin dikenakan."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Beralih daripada <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kepada <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"data mudah alih"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"jenis rangkaian tidak diketahui"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-my/strings.xml b/service/ServiceConnectivityResources/res/values-my/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..15b75f0fc3a15034418dcd92cd88f2e16a68f8cf --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-my/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"စနစ်á€á€»á€á€á€ºá€†á€€á€ºá€”á€á€¯á€„်မှု ရင်းမြစ်များ"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"á€á€á€¯á€„်ဖá€á€¯á€„်ကွန်ရက်သá€á€¯á€· လက်မှá€á€ºá€‘á€á€¯á€¸á€á€„်ပါ"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ကွန်ယက်သá€á€¯á€· လက်မှá€á€ºá€‘á€á€¯á€¸á€á€„်ရန်"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> á€á€½á€„် အင်á€á€¬á€”က်အသုံးပြုá€á€½á€„့် မရှá€á€•á€«"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"အá€á€¼á€¬á€¸á€›á€½á€±á€¸á€á€»á€šá€ºá€…ရာများကá€á€¯á€€á€¼á€Šá€·á€ºá€›á€”် á€á€á€¯á€·á€•á€«"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"မá€á€¯á€˜á€á€¯á€„်းကွန်ရက်á€á€½á€„် အင်á€á€¬á€”က်á€á€»á€á€á€ºá€†á€€á€ºá€™á€¾á€¯ မရှá€á€•á€«"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ကွန်ရက်á€á€½á€„် အင်á€á€¬á€”က်အသုံးပြုá€á€½á€„့် မရှá€á€•á€«"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကá€á€¯ သုံးáမရပါá‹"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> á€á€½á€„် á€á€»á€á€á€ºá€†á€€á€ºá€™á€¾á€¯á€€á€á€¯ ကန့်သá€á€ºá€‘ားသည်"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"မည်သá€á€¯á€·á€•á€„်ဖြစ်စေ á€á€»á€á€á€ºá€†á€€á€ºá€›á€”် á€á€á€¯á€·á€•á€«"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> သá€á€¯á€· ပြောင်းလá€á€¯á€€á€ºá€•á€¼á€®"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ဖြင့် အင်á€á€¬á€”က် အသုံးမပြုနá€á€¯á€„်သည့်အá€á€»á€á€”်á€á€½á€„် စက်ပစ္စည်းသည် <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ကá€á€¯ သုံးပါသည်ዠဒေá€á€¬á€žá€¯á€¶á€¸á€…ွဲဠကျသင့်နá€á€¯á€„်ပါသည်á‹"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> မှ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> သá€á€¯á€· ပြောင်းလá€á€¯á€€á€ºá€•á€¼á€®"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"မá€á€¯á€˜á€á€¯á€„်းဒေá€á€¬"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"ဘလူးá€á€¯á€žá€º"</item> + <item msgid="1160736166977503463">"အီသာနက်"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"အမည်မသá€á€€á€½á€”်ရက်အမျá€á€¯á€¸á€¡á€…ား"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a561def65c50f4c634b48e1aabf8a41c9f61198c --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Ressurser for systemtilkobling"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Logg pÃ¥ Wi-Fi-nettverket"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Logg pÃ¥ nettverk"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internettilkobling"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Trykk for Ã¥ fÃ¥ alternativer"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnettverket har ingen internettilgang"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Nettverket har ingen internettilgang"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Den private DNS-tjeneren kan ikke nÃ¥s"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begrenset tilkobling"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Trykk for Ã¥ koble til likevel"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Byttet til <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Enheten bruker <xliff:g id="NEW_NETWORK">%1$s</xliff:g> nÃ¥r <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ikke har Internett-tilgang. Avgifter kan pÃ¥løpe."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Byttet fra <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> til <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobildata"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"en ukjent nettverkstype"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ne/strings.xml b/service/ServiceConnectivityResources/res/values-ne/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..f74542d9421f2435507ca5647fc37e5fe5218dcb --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ne/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"सिसà¥à¤Ÿà¤® कनेकà¥à¤Ÿà¤¿à¤à¤¿à¤Ÿà¥€à¤•à¤¾ सà¥à¤°à¥‹à¤¤à¤¹à¤°à¥‚"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi नेटवरà¥à¤•à¤®à¤¾ साइन इन गरà¥à¤¨à¥à¤¹à¥‹à¤¸à¥"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"सञà¥à¤œà¤¾à¤²à¤®à¤¾ साइन इन गरà¥à¤¨à¥à¤¹à¥‹à¤¸à¥"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को इनà¥à¤Ÿà¤°à¤¨à¥‡à¤Ÿà¤®à¤¾à¤¥à¤¿ पहà¥à¤à¤š छैन"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"विकलà¥à¤ªà¤¹à¤°à¥‚का लागि टà¥à¤¯à¤¾à¤ª गरà¥à¤¨à¥à¤¹à¥‹à¤¸à¥"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"मोबाइल नेटवरà¥à¤•à¤•à¥‹ इनà¥à¤Ÿà¤°à¤¨à¥‡à¤Ÿà¤®à¤¾à¤¥à¤¿ पहà¥à¤à¤š छैन"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"नेटवरà¥à¤•à¤•à¥‹ इनà¥à¤Ÿà¤°à¤¨à¥‡à¤Ÿà¤®à¤¾à¤¥à¤¿ पहà¥à¤à¤š छैन"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"निजी DNS सरà¥à¤à¤°à¤®à¤¾à¤¥à¤¿ पहà¥à¤à¤š पà¥à¤°à¤¾à¤ªà¥à¤¤ गरà¥à¤¨ सकिà¤à¤¦à¥ˆà¤¨"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> को जडान सीमित छ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"जसरी à¤à¤ पनि जडान गरà¥à¤¨ टà¥à¤¯à¤¾à¤ª गरà¥à¤¨à¥à¤¹à¥‹à¤¸à¥"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> मा बदलà¥à¤¨à¥à¤¹à¥‹à¤¸à¥"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> मारà¥à¤«à¤¤ इनà¥à¤Ÿà¤°à¤¨à¥‡à¤Ÿà¤®à¤¾à¤¥à¤¿ पहà¥à¤à¤š राखà¥à¤¨ नसकेको अवसà¥à¤¥à¤¾à¤®à¤¾ यनà¥à¤¤à¥à¤°à¤²à¥‡ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> पà¥à¤°à¤¯à¥‹à¤— गरà¥à¤¦à¤›à¥¤ शà¥à¤²à¥à¤• लागà¥à¤¨ सकà¥à¤›à¥¤"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> बाट <xliff:g id="NEW_NETWORK">%2$s</xliff:g> मा परिवरà¥à¤¤à¤¨ गरियो"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"मोबाइल डेटा"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"बà¥à¤²à¥à¤Ÿà¥à¤¥"</item> + <item msgid="1160736166977503463">"इथरनेट"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"नेटवरà¥à¤•à¤•à¥‹ कà¥à¤¨à¥ˆ अजà¥à¤žà¤¾à¤¤ पà¥à¤°à¤•à¤¾à¤°"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-nl/strings.xml b/service/ServiceConnectivityResources/res/values-nl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0f3203beb5b9c9228c220f2462ceb58979829b96 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-nl/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resources voor systeemconnectiviteit"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Inloggen bij wifi-netwerk"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Inloggen bij netwerk"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft geen internettoegang"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tik voor opties"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobiel netwerk heeft geen internettoegang"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Netwerk heeft geen internettoegang"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Geen toegang tot privé-DNS-server"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> heeft beperkte connectiviteit"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tik om toch verbinding te maken"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Overgeschakeld naar <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Apparaat gebruikt <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wanneer <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> geen internetverbinding heeft. Er kunnen kosten in rekening worden gebracht."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Overgeschakeld van <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> naar <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobiele data"</item> + <item msgid="6341719431034774569">"Wifi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"een onbekend netwerktype"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-or/strings.xml b/service/ServiceConnectivityResources/res/values-or/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ecf4d694d07da5aa73589be6769ecf837b094762 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-or/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ସିଷàଟମର ସଂଯà‹à¬— ସମàବନàଧିତ ରିସà‹à¬°àସଗàଡ଼ିକ"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"à±à¬¾à¬‡-ଫାଇ ନà‡à¬Ÿà±à¬°àà¬•à¬°à‡ à¬¸à¬¾à¬‡à¬¨àâ€-ଇନà†କରନàତà"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ନà‡à¬Ÿà‌à±à¬°àà¬•à¬°à‡ à¬¸à¬¾à¬‡à¬¨à†ଇନà†କରନàତà"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ଇଣàଟରàନà‡à¬Ÿà ଆକàସà‡à¬¸à ନାହିà¬"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ବିକଳàପ ପାଇଠଟାପà†କରନàତà"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ମà‹à¬¬à¬¾à¬‡à¬²à ନà‡à¬Ÿà‌à±à¬¾à¬°àà¬•à¬°à‡ à¬‡à¬£àଟରàନà‡à¬Ÿà ଆକàସà‡à¬¸à ନାହିà¬"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ନà‡à¬Ÿà‌à±à¬¾à¬°àà¬•à¬°à‡ à¬‡à¬£àଟରàନà‡à¬Ÿà ଆକàସà‡à¬¸à ନାହିà¬"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ବààŸà¬•àତିଗତ DNS ସରàà¬à¬°à ଆକàସà‡à¬¸à କରିହà‡à¬¬ ନାହିà¬"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>ର ସà€à¬®à¬¿à¬¤ ସଂଯà‹à¬— ଅଛି"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ତଥାପି ଯà‹à¬—ାଯà‹à¬— କରିବାକà ଟାପà କରନàତà"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>କà ବଦଳାଗଲା"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ର ଇଣàଟରନà‡à¬Ÿà†ଆକàସà‡à¬¸à ନଥିବାବà‡à¬³à‡ ଡିà¬à¬¾à¬‡à¬¸à†<xliff:g id="NEW_NETWORK">%1$s</xliff:g> ବààŸà¬¬à¬¹à¬¾à¬° କରିଥାà¬à¥¤ ଶàଳàକ ଲାଗà ହà‹à¬‡à¬ªà¬¾à¬°à‡à¥¤"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ରà <xliff:g id="NEW_NETWORK">%2$s</xliff:g>କà ବଦଳାଗଲା"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ମà‹à¬¬à¬¾à¬‡à¬² ଡାଟା"</item> + <item msgid="6341719431034774569">"à±à¬¾à¬‡-ଫାଇ"</item> + <item msgid="5081440868800877512">"ବàଲàଟàଥà"</item> + <item msgid="1160736166977503463">"ଇଥରନà‡à¬Ÿà"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"à¬à¬• ଅଜଣା ନà‡à¬Ÿà±à¬¾à¬°àକ ପàରକାର"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-pa/strings.xml b/service/ServiceConnectivityResources/res/values-pa/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..43280549e773dda2eaccd7b7033eeedc85a49979 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-pa/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ਸਿਸਟਮ ਕਨੈਕਟੀਵਿਟੀ ਸਰੋਤ"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ਵਾਈ-ਫਾਈ ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹà©à©°à¨š ਨਹੀਂ ਹੈ"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹà©à©°à¨š ਨਹੀਂ ਹੈ"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹà©à©°à¨š ਨਹੀਂ ਹੈ"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪà©à¨°à¨£à¨¾à¨²à©€ (DNS) ਸਰਵਰ \'ਤੇ ਪਹà©à©°à¨š ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ਕੋਲ ਸੀਮਤ ਕਨੈਕਟੀਵਿਟੀ ਹੈ"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ਫਿਰ ਵੀ ਕਨੈਕਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"ਬਦਲਕੇ <xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ਲਿਆਂਦਾ ਗਿਆ"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ਦੀ ਇੰਟਰਨੈੱਟ \'ਤੇ ਪਹà©à©°à¨š ਨਾ ਹੋਣ \'ਤੇ ਡੀਵਾਈਸ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ।"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ਤੋਂ ਬਦਲਕੇ <xliff:g id="NEW_NETWORK">%2$s</xliff:g> \'ਤੇ ਕੀਤਾ ਗਿਆ"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ਮੋਬਾਈਲ ਡਾਟਾ"</item> + <item msgid="6341719431034774569">"ਵਾਈ-ਫਾਈ"</item> + <item msgid="5081440868800877512">"ਬਲੂਟà©à©±à¨¥"</item> + <item msgid="1160736166977503463">"ਈਥਰਨੈੱਟ"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ਕੋਈ ਅਗਿਆਤ ਨੈੱਟਵਰਕ ਦੀ ਕਿਸਮ"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-pl/strings.xml b/service/ServiceConnectivityResources/res/values-pl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..e6b3a0cff6ee1403bf6979a1470b86ce8d5664e8 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-pl/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Zasoby systemowe dotyczÄ…ce Å‚Ä…cznoÅ›ci"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Zaloguj siÄ™ w sieci Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Zaloguj siÄ™ do sieci"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nie ma dostÄ™pu do internetu"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Kliknij, by wyÅ›wietlić opcje"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Sieć komórkowa nie ma dostÄ™pu do internetu"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Sieć nie ma dostÄ™pu do internetu"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Brak dostÄ™pu do prywatnego serwera DNS"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ma ograniczonÄ… Å‚Ä…czność"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Kliknij, by mimo to nawiÄ…zać poÅ‚Ä…czenie"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Zmieniono na poÅ‚Ä…czenie typu <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"UrzÄ…dzenie korzysta z poÅ‚Ä…czenia typu <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, gdy <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nie dostÄ™pu do internetu. MogÄ… zostać naliczone opÅ‚aty."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"PrzeÅ‚Ä…czono z poÅ‚Ä…czenia typu <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na <xliff:g id="NEW_NETWORK">%2$s</xliff:g>."</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobilna transmisja danych"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nieznany typ sieci"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1d0bc0b0cd51f665eeccc6577427ceb21246f61 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividade do sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Fazer login na rede Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Fazer login na rede"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toque para ver opções"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"A rede móvel não tem acesso à Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede não tem acesso à Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Não é possÃvel acessar o servidor DNS privado"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toque para conectar mesmo assim"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Esse serviço pode ser cobrado."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"dados móveis"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"um tipo de rede desconhecido"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..163d70b0818996cc49dae82ab9e2c3d898c91bbb --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conetividade do sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Iniciar sessão na rede Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"InÃcio de sessão na rede"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toque para obter mais opções"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"A rede móvel não tem acesso à Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede não tem acesso à Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Não é possÃvel aceder ao servidor DNS."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conetividade limitada."</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toque para ligar mesmo assim."</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Mudou para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo utiliza <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Podem aplicar-se custos."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Mudou de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"dados móveis"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"um tipo de rede desconhecido"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-pt/strings.xml b/service/ServiceConnectivityResources/res/values-pt/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1d0bc0b0cd51f665eeccc6577427ceb21246f61 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-pt/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Recursos de conectividade do sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Fazer login na rede Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Fazer login na rede"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> não tem acesso à Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Toque para ver opções"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"A rede móvel não tem acesso à Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"A rede não tem acesso à Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Não é possÃvel acessar o servidor DNS privado"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> tem conectividade limitada"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Toque para conectar mesmo assim"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Alternado para <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"O dispositivo usa <xliff:g id="NEW_NETWORK">%1$s</xliff:g> quando <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> não tem acesso à Internet. Esse serviço pode ser cobrado."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Alternado de <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> para <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"dados móveis"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"um tipo de rede desconhecido"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ro/strings.xml b/service/ServiceConnectivityResources/res/values-ro/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..221261c66c0680c38b644244f4827f07be29a653 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ro/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resurse pentru conectivitatea sistemului"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ConectaÈ›i-vă la reÈ›eaua Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ConectaÈ›i-vă la reÈ›ea"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nu are acces la internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"AtingeÈ›i pentru opÈ›iuni"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ReÈ›eaua mobilă nu are acces la internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ReÈ›eaua nu are acces la internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Serverul DNS privat nu poate fi accesat"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> are conectivitate limitată"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"AtingeÈ›i pentru a vă conecta oricum"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"S-a comutat la <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Dispozitivul foloseÈ™te <xliff:g id="NEW_NETWORK">%1$s</xliff:g> când <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nu are acces la internet. Se pot aplica taxe."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"S-a comutat de la <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> la <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"date mobile"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"un tip de reÈ›ea necunoscut"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ru/strings.xml b/service/ServiceConnectivityResources/res/values-ru/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..ba179b7394e6c54417e507362477a741898fe8a7 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ru/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"System Connectivity Resources"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Подключение к Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"РегиÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ð² Ñети"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Сеть \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" не подключена к Интернету"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Ðажмите, чтобы показать варианты."</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ÐœÐ¾Ð±Ð¸Ð»ÑŒÐ½Ð°Ñ Ñеть не подключена к Интернету"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Сеть не подключена к Интернету"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ДоÑтупа к чаÑтному DNS-Ñерверу нет."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Подключение к Ñети \"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>\" ограничено"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ðажмите, чтобы подключитьÑÑ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Ðовое подключение: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"УÑтройÑтво иÑпользует <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, еÑли подключение к Ñети <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> недоÑтупно. Может взиматьÑÑ Ð¿Ð»Ð°Ñ‚Ð° за передачу данных."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"УÑтройÑтво отключено от Ñети <xliff:g id="NEW_NETWORK">%2$s</xliff:g> и теперь иÑпользует <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобильный интернет"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"неизвеÑтный тип Ñети"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-si/strings.xml b/service/ServiceConnectivityResources/res/values-si/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..1c493a78dc7b46c13a63a10562d1c4b20c0c1604 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-si/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"පද්ධà¶à·’ සබà·à¶³à·”ම් à·„à·à¶šà·’ය෠සම්පà¶à·Š"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi ජà·à¶½à¶ºà¶§ පුරනය වන්න"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ජà·à¶½à¶ºà¶§ පුරනය වන්න"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට අන්à¶à¶»à·Šà¶¢à·à¶½ ප්â€à¶»à·€à·šà·à¶º නà·à¶"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"විකල්ප සඳහ෠à¶à¶§à·Šà¶§à·” කරන්න"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"ජංගම ජà·à¶½à·€à¶½à¶§ අන්à¶à¶»à·Šà¶¢à·à¶½ ප්â€à¶»à·€à·šà·à¶º නà·à¶"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"ජà·à¶½à¶ºà¶§ අන්à¶à¶»à·Šà¶¢à·à¶½ ප්â€à¶»à·€à·šà·à¶º නà·à¶"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"පුද්ගලික DNS සේවà·à¶¯à·à¶ºà¶šà¶ºà¶§ ප්â€à¶»à·€à·šà· වීමට නොහà·à¶šà·’ය"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> හට සීමිචසබà·à¶³à·”ම් à·„à·à¶šà·’යà·à·€à¶šà·Š ඇà¶"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"කෙසේ වෙà¶à¶à·Š ඉදිරියට යà·à¶¸à¶§ à¶à¶§à·Šà¶§à·” කරන්න"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> වෙචමà·à¶»à·” විය"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"උපà·à¶‚ගය <xliff:g id="NEW_NETWORK">%1$s</xliff:g> <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> සඳහ෠අන්à¶à¶»à·Šà¶¢à·à¶½ ප්â€à¶»à·€à·šà·à¶º නà·à¶à·’ විට භà·à·€à·’චකරයි. ගà·à·ƒà·Šà¶à·” අදà·à·… විය à·„à·à¶šà·’ය."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> සිට <xliff:g id="NEW_NETWORK">%2$s</xliff:g> වෙචමà·à¶»à·” විය"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"ජංගම දà¶à·Šà¶"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"බ්ලූටූà¶à·Š"</item> + <item msgid="1160736166977503463">"ඊà¶à¶»à·Šà¶±à·™à¶§à·Š"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"නොදන්න෠ජà·à¶½ වර්ගයකි"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-sk/strings.xml b/service/ServiceConnectivityResources/res/values-sk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..1b9313af170fdcdaa081f42e672c8cce255b0b5d --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-sk/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Zdroje možnostà pripojenia systému"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"PrihlásiÅ¥ sa do siete Wi‑Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Prihlásenie do siete"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nemá prÃstup k internetu"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"KlepnutÃm zÃskate možnosti"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilná sieÅ¥ nemá prÃstup k internetu"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"SieÅ¥ nemá prÃstup k internetu"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"K súkromnému serveru DNS sa nepodarilo zÃskaÅ¥ prÃstup"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> má obmedzené pripojenie"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Ak sa chcete aj napriek tomu pripojiÅ¥, klepnite"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Prepnuté na sieÅ¥: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"KeÄ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nemá prÃstup k internetu, zariadenie použÃva <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Môžu sa úÄtovaÅ¥ poplatky."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Prepnuté zo siete <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sieÅ¥ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobilné dáta"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"neznámy typ siete"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-sl/strings.xml b/service/ServiceConnectivityResources/res/values-sl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..739fb8e49156ae1fae159f20f5a774ddb67c6b24 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-sl/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Viri povezljivosti sistema"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Prijavite se v omrežje Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Prijava v omrežje"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Omrežje <xliff:g id="NETWORK_SSID">%1$s</xliff:g> nima dostopa do interneta"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Dotaknite se za možnosti"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilno omrežje nima dostopa do interneta"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Omrežje nima dostopa do interneta"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Do zasebnega strežnika DNS ni mogoÄe dostopati"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Povezljivost omrežja <xliff:g id="NETWORK_SSID">%1$s</xliff:g> je omejena"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Dotaknite se, da kljub temu vzpostavite povezavo"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Preklopljeno na omrežje vrste <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Naprava uporabi omrežje vrste <xliff:g id="NEW_NETWORK">%1$s</xliff:g>, ko omrežje vrste <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nima dostopa do interneta. Prenos podatkov se lahko zaraÄuna."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Preklopljeno z omrežja vrste <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na omrežje vrste <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"prenos podatkov v mobilnem omrežju"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"neznana vrsta omrežja"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf8cf3b40e738deb6ed05780fcec51645f56af39 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Burimet e lidhshmërisë së sistemit"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Identifikohu në rrjetin Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Identifikohu në rrjet"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nuk ka qasje në internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Trokit për opsionet"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Rrjeti celular nuk ka qasje në internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Rrjeti nuk ka qasje në internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Serveri privat DNS nuk mund të qaset"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ka lidhshmëri të kufizuar"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Trokit për t\'u lidhur gjithsesi"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Kaloi te <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Pajisja përdor <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kur <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> nuk ka qasje në internet. Mund të zbatohen tarifa."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Kaloi nga <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> te <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"të dhënat celulare"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Eternet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"një lloj rrjeti i panjohur"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-sr/strings.xml b/service/ServiceConnectivityResources/res/values-sr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..1f7c95c94d9e152a138bd0a9087267cac27f44ff --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-sr/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"РеÑурÑи за повезивање Ñа ÑиÑтемом"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Пријављивање на WiFi мрежу"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Пријавите Ñе на мрежу"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> нема приÑтуп интернету"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Додирните за опције"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобилна мрежа нема приÑтуп интернету"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Мрежа нема приÑтуп интернету"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"ПриÑтуп приватном DNS Ñерверу није уÑпео"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> има ограничену везу"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Додирните да биÑте Ñе ипак повезали"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Прешли Ñте на тип мреже <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Уређај кориÑти тип мреже <xliff:g id="NEW_NETWORK">%1$s</xliff:g> када тип мреже <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> нема приÑтуп интернету. Можда ће Ñе наплаћивати трошкови."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Прешли Ñте Ñа типа мреже <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на тип мреже <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобилни подаци"</item> + <item msgid="6341719431034774569">"WiFi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Етернет"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"непознат тип мреже"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-sv/strings.xml b/service/ServiceConnectivityResources/res/values-sv/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7314005f8be53eafebb68acec404fd3052b8ebc2 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-sv/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Resurser för systemanslutning"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Logga in pÃ¥ ett Wi-Fi-nätverk"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Logga in pÃ¥ nätverket"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har ingen internetanslutning"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Tryck för alternativ"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobilnätverket har ingen internetanslutning"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Nätverket har ingen internetanslutning"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Det gÃ¥r inte att komma Ã¥t den privata DNS-servern."</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> har begränsad anslutning"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Tryck för att ansluta ändÃ¥"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Byte av nätverk till <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="NEW_NETWORK">%1$s</xliff:g> används pÃ¥ enheten när det inte finns internetÃ¥tkomst via <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Avgifter kan tillkomma."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Byte av nätverk frÃ¥n <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> till <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobildata"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"en okänd nätverkstyp"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c4d59410de80a1f9b8be9cb751a7be134ff426e --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Nyenzo za Muunganisho wa Mfumo"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Ingia kwa mtandao wa Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Ingia katika mtandao"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> haina uwezo wa kufikia intaneti"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Gusa ili upate chaguo"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mtandao wa simu hauna uwezo wa kufikia intaneti"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Mtandao hauna uwezo wa kufikia intaneti"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Seva ya faragha ya DNS haiwezi kufikiwa"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ina muunganisho unaofikia huduma chache."</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Gusa ili uunganishe tu"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Sasa inatumia <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Kifaa hutumia <xliff:g id="NEW_NETWORK">%1$s</xliff:g> wakati <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> haina intaneti. Huenda ukalipishwa."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Imebadilisha mtandao kutoka <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> na sasa inatumia <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"data ya mtandao wa simu"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethaneti"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"aina ya mtandao isiyojulikana"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ta/strings.xml b/service/ServiceConnectivityResources/res/values-ta/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..90f89c988e21d48a7f68ce87febc388a132ac5af --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ta/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"சிஸà¯à®Ÿà®®à¯ இணைபà¯à®ªà¯ மூலஙà¯à®•à®³à¯"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"வைஃபை நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à®¿à®²à¯ உளà¯à®¨à¯à®´à¯ˆà®¯à®µà¯à®®à¯"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à®¿à®²à¯ உளà¯à®¨à¯à®´à¯ˆà®¯à®µà¯à®®à¯"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à®¿à®±à¯à®•à¯ இணைய அணà¯à®•à®²à¯ இலà¯à®²à¯ˆ"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"விரà¯à®ªà¯à®ªà®™à¯à®•à®³à¯à®•à¯à®•à¯, தடà¯à®Ÿà®µà¯à®®à¯"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"மொபைல௠நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à®¿à®±à¯à®•à¯ இணைய அணà¯à®•à®²à¯ இலà¯à®²à¯ˆ"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à®¿à®±à¯à®•à¯ இணைய அணà¯à®•à®²à¯ இலà¯à®²à¯ˆ"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"தனிபà¯à®ªà®Ÿà¯à®Ÿ DNS சேவையகதà¯à®¤à¯ˆ அணà¯à®• இயலாதà¯"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> வரமà¯à®ªà®¿à®±à¯à®•à¯ உடà¯à®ªà®Ÿà¯à®Ÿ இணைபà¯à®ªà¯à®¨à®¿à®²à¯ˆà®¯à¯ˆà®•à¯ கொணà¯à®Ÿà¯à®³à¯à®³à®¤à¯"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"எபà¯à®ªà®Ÿà®¿à®¯à¯‡à®©à¯à®®à¯ இணைபà¯à®ªà®¤à®±à¯à®•à¯à®¤à¯ தடà¯à®Ÿà®µà¯à®®à¯"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>கà¯à®•à¯ மாறà¯à®±à®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à®¿à®²à¯ இணà¯à®Ÿà®°à¯à®¨à¯†à®Ÿà¯ அணà¯à®•à®²à¯ இலà¯à®²à®¾à®¤à®¤à®¾à®²à¯, சாதனமானத௠<xliff:g id="NEW_NETWORK">%1$s</xliff:g> நெடà¯à®µà¯Šà®°à¯à®•à¯à®•à¯ˆà®ªà¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯à®•à®¿à®±à®¤à¯. கடà¯à®Ÿà®£à®™à¯à®•à®³à¯ விதிகà¯à®•à®ªà¯à®ªà®Ÿà®²à®¾à®®à¯."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> இலிரà¯à®¨à¯à®¤à¯ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>கà¯à®•à¯ மாறà¯à®±à®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"மொபைல௠டேடà¯à®Ÿà®¾"</item> + <item msgid="6341719431034774569">"வைஃபை"</item> + <item msgid="5081440868800877512">"பà¯à®³à¯‚டூதà¯"</item> + <item msgid="1160736166977503463">"ஈதரà¯à®¨à¯†à®Ÿà¯"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"தெரியாத நெடà¯à®µà¯Šà®°à¯à®•à¯ வகை"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-te/strings.xml b/service/ServiceConnectivityResources/res/values-te/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..c69b599e19008e46cc23f5434a77d6ce3e47538c --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-te/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"సిసà±à°Ÿà°®à± కనెకà±à°Ÿà°¿à°µà°¿à°Ÿà±€ రిసోరà±à°¸à±â€Œà°²à±"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi నెటà±â€Œà°µà°°à±à°•à±â€Œà°•à°¿ సైనౠఇనౠచేయండి"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"నెటà±â€Œà°µà°°à±à°•à±â€Œà°•à°¿ సైనౠఇనౠచేయండి"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>à°•à°¿ ఇంటరà±à°¨à±†à°Ÿà± యాకà±à°¸à±†à°¸à± లేదà±"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ఎంపికల కోసం నొకà±à°•à°‚à°¡à°¿"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"మొబైలౠనెటà±â€Œà°µà°°à±à°•à±â€Œà°•à± ఇంటరà±à°¨à±†à°Ÿà± యాకà±à°¸à±†à°¸à± లేదà±"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"నెటà±â€Œà°µà°°à±à°•à±â€Œà°•à± ఇంటరà±à°¨à±†à°Ÿà± యాకà±à°¸à±†à°¸à± లేదà±"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"à°ªà±à°°à±ˆà°µà±‡à°Ÿà± DNS సరà±à°µà°°à±â€Œà°¨à± యాకà±à°¸à±†à°¸à± చేయడం సాధà±à°¯à°ªà°¡à°¦à±"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> పరిమిత కనెకà±à°Ÿà°¿à°µà°¿à°Ÿà±€à°¨à°¿ కలిగి ఉంది"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"à°à°¦à±‡à°®à±ˆà°¨à°¾ కనెకà±à°Ÿà± చేయడానికి నొకà±à°•à°‚à°¡à°¿"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>à°•à°¿ మారà±à°šà°¬à°¡à°¿à°‚ది"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"పరికరం <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>à°•à°¿ ఇంటరà±à°¨à±†à°Ÿà± యాకà±à°¸à±†à°¸à± లేనపà±à°ªà±à°¡à± <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ని ఉపయోగిసà±à°¤à±à°‚ది. ఛారà±à°œà±€à°²à± వరà±à°¤à°¿à°‚చవచà±à°šà±."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> à°¨à±à°‚à°¡à°¿ <xliff:g id="NEW_NETWORK">%2$s</xliff:g>à°•à°¿ మారà±à°šà°¬à°¡à°¿à°‚ది"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"మొబైలౠడేటా"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"à°¬à±à°²à±‚టూతà±"</item> + <item msgid="1160736166977503463">"ఈథరà±â€Œà°¨à±†à°Ÿà±"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"తెలియని నెటà±â€Œà°µà°°à±à°•à± à°°à°•à°‚"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-th/strings.xml b/service/ServiceConnectivityResources/res/values-th/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..eee5a3520d67395e1baadce09c55fda53f8fc9bf --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-th/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"ทรัพยาà¸à¸£à¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸à¸‚à¸à¸‡à¸£à¸°à¸šà¸š"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"ลงชื่à¸à¹€à¸‚้าใช้เครืà¸à¸‚่าย WiFi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"ลงชื่à¸à¹€à¸‚้าใช้เครืà¸à¸‚่าย"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> เข้าถึงà¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸™à¹‡à¸•à¹„ม่ได้"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"à¹à¸•à¸°à¹€à¸žà¸·à¹ˆà¸à¸”ูตัวเลืà¸à¸"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"เครืà¸à¸‚่ายมืà¸à¸–ืà¸à¹„ม่มีà¸à¸²à¸£à¹€à¸‚้าถึงà¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸™à¹‡à¸•"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"เครืà¸à¸‚่ายไม่มีà¸à¸²à¸£à¹€à¸‚้าถึงà¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸™à¹‡à¸•"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"เข้าถึงเซิร์ฟเวà¸à¸£à¹Œ DNS ไม่ได้"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> มีà¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸à¸ˆà¸³à¸à¸±à¸”"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"à¹à¸•à¸°à¹€à¸žà¸·à¹ˆà¸à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"เปลี่ยนเป็น <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"à¸à¸¸à¸›à¸à¸£à¸“์จะใช้ <xliff:g id="NEW_NETWORK">%1$s</xliff:g> เมื่ภ<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> เข้าถึงà¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸™à¹‡à¸•à¹„ม่ได้ โดยà¸à¸²à¸ˆà¸¡à¸µà¸„่าบริà¸à¸²à¸£"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"เปลี่ยนจาภ<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> เป็น <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"à¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸™à¹‡à¸•à¸¡à¸·à¸à¸–ืà¸"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"บลูทูธ"</item> + <item msgid="1160736166977503463">"à¸à¸µà¹€à¸—à¸à¸£à¹Œà¹€à¸™à¹‡à¸•"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ประเภทเครืà¸à¸‚่ายที่ไม่รู้จัà¸"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-tl/strings.xml b/service/ServiceConnectivityResources/res/values-tl/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d665fe5f98be428f004b742648fec9ecaeba96a --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-tl/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Mga Resource ng Pagkakonekta ng System"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Mag-sign in sa Wi-Fi network"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Mag-sign in sa network"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Walang access sa internet ang <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"I-tap para sa mga opsyon"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Walang access sa internet ang mobile network"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Walang access sa internet ang network"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Hindi ma-access ang pribadong DNS server"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"Limitado ang koneksyon ng <xliff:g id="NETWORK_SSID">%1$s</xliff:g>"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"I-tap para kumonekta pa rin"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Lumipat sa <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Ginagamit ng device ang <xliff:g id="NEW_NETWORK">%1$s</xliff:g> kapag walang access sa internet ang <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>. Maaaring may mga malapat na singilin."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Lumipat sa <xliff:g id="NEW_NETWORK">%2$s</xliff:g> mula sa <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobile data"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"isang hindi kilalang uri ng network"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-tr/strings.xml b/service/ServiceConnectivityResources/res/values-tr/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..cfb76323ed4168c425a36c7ee7659fec4abb35fb --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-tr/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Sistem BaÄŸlantı Kaynakları"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Kablosuz aÄŸda oturum açın"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"AÄŸda oturum açın"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ağının internet baÄŸlantısı yok"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Seçenekler için dokunun"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobil ağın internet baÄŸlantısı yok"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Ağın internet baÄŸlantısı yok"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Gizli DNS sunucusuna eriÅŸilemiyor"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> sınırlı baÄŸlantıya sahip"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Yine de baÄŸlanmak için dokunun"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> ağına geçildi"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> ağının internet eriÅŸimi olmadığında cihaz <xliff:g id="NEW_NETWORK">%1$s</xliff:g> ağını kullanır. Bunun için ödeme alınabilir."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> ağından <xliff:g id="NEW_NETWORK">%2$s</xliff:g> ağına geçildi"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobil veri"</item> + <item msgid="6341719431034774569">"Kablosuz"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"bilinmeyen aÄŸ türü"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-uk/strings.xml b/service/ServiceConnectivityResources/res/values-uk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..c5da7460b3beb30352c7ab90690f291b5290a9c2 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-uk/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"РеÑурÑи Ð´Ð»Ñ Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ ÑиÑтеми"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Вхід у мережу Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Вхід у мережу"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"Мережа <xliff:g id="NETWORK_SSID">%1$s</xliff:g> не має доÑтупу до Інтернету"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"ТоркнітьÑÑ, щоб відкрити опції"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Мобільна мережа не має доÑтупу до Інтернету"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Мережа не має доÑтупу до Інтернету"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Ðемає доÑтупу до приватного DNS-Ñервера"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"ÐŸÑ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ мережі <xliff:g id="NETWORK_SSID">%1$s</xliff:g> обмежено"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ÐатиÑніть, щоб уÑе одно підключитиÑÑ"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"ПриÑтрій перейшов на мережу <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Коли мережа <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> не має доÑтупу до Інтернету, викориÑтовуєтьÑÑ <xliff:g id="NEW_NETWORK">%1$s</xliff:g>. Може ÑÑ‚ÑгуватиÑÑ Ð¿Ð»Ð°Ñ‚Ð°."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"ПриÑтрій перейшов з мережі <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> на мережу <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"мобільний Інтернет"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"невідомий тип мережі"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-ur/strings.xml b/service/ServiceConnectivityResources/res/values-ur/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..bd2a228ab43049b338750c2cb0c254e1dd6e85bc --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-ur/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"سسٹم کنیکٹوٹی Ú©Û’ وسائل"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"â€Wi-Fi نیٹ ورک میں سائن ان کریں"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"نیٹ ورک میں سائن ان کریں"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Ú©Ùˆ انٹرنیٹ تک رسائی Øاصل Ù†Ûیں ÛÛ’"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"اختیارات کیلئے تھپتھپائیں"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"موبائل نیٹ ورک Ú©Ùˆ انٹرنیٹ تک رسائی Øاصل Ù†Ûیں ÛÛ’"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"نیٹ ورک Ú©Ùˆ انٹرنیٹ تک رسائی Øاصل Ù†Ûیں ÛÛ’"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"â€Ù†Ø¬ÛŒ DNS سرور تک رسائی Øاصل Ù†Ûیں Ú©ÛŒ جا سکی"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> Ú©ÛŒ کنیکٹوٹی Ù…Øدود ÛÛ’"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"بÛر Øال منسلک کرنے Ú©Û’ لیے تھپتھپائیں"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"<xliff:g id="NETWORK_TYPE">%1$s</xliff:g> پر سوئچ ÛÙˆ گیا"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"جب <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> Ú©Ùˆ انٹرنیٹ تک رسائی Ù†Ûیں Ûوتی ÛÛ’ تو Ø¢Ù„Û <xliff:g id="NEW_NETWORK">%1$s</xliff:g> کا استعمال کرتا ÛÛ’Û” چارجز لاگو ÛÙˆ سکتے Ûیں۔"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> سے <xliff:g id="NEW_NETWORK">%2$s</xliff:g> پر سوئچ ÛÙˆ گیا"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"موبائل ڈیٹا"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"بلوٹوتھ"</item> + <item msgid="1160736166977503463">"ایتھرنیٹ"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"نامعلوم نیٹ ورک Ú©ÛŒ قسم"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-uz/strings.xml b/service/ServiceConnectivityResources/res/values-uz/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..567aa886b4a9dfd87affa8a9ca7d755e26a47d97 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-uz/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Tizim aloqa resurslari"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Wi-Fi tarmoqqa kirish"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Tarmoqqa kirish"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda internetga ruxsati yoÊ»q"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Variantlarni ko‘rsatish uchun bosing"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mobil tarmoq internetga ulanmagan"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Tarmoq internetga ulanmagan"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Xususiy DNS server ishlamayapti"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> nomli tarmoqda aloqa cheklangan"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Baribir ulash uchun bosing"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Yangi ulanish: <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Agar <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> tarmoqda internet uzilsa, qurilma <xliff:g id="NEW_NETWORK">%1$s</xliff:g>ga ulanadi. Sarflangan trafik uchun haq olinishi mumkin."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> tarmog‘idan <xliff:g id="NEW_NETWORK">%2$s</xliff:g> tarmog‘iga o‘tildi"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"mobil internet"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"nomaʼlum tarmoq turi"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-vi/strings.xml b/service/ServiceConnectivityResources/res/values-vi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..590b388c960fdc84579de427cb849df41b8e75fb --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-vi/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Tà i nguyên kết nối hệ thống"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Äăng nháºp và o mạng Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Äăng nháºp và o mạng"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> không có quyá»n truy cáºp Internet"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Nhấn để biết tùy chá»n"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Mạng di Ä‘á»™ng không có quyá»n truy cáºp Internet"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Mạng không có quyá»n truy cáºp Internet"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Không thể truy cáºp máy chủ DNS riêng tÆ°"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> có khả năng kết nối giá»›i hạn"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Nhấn để tiếp tục kết nối"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Äã chuyển sang <xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Thiết bị sá» dụng <xliff:g id="NEW_NETWORK">%1$s</xliff:g> khi <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> không có quyá»n truy cáºp Internet. Bạn có thể phải trả phÃ."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Äã chuyển từ <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> sang <xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"dữ liệu di Ä‘á»™ng"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"Bluetooth"</item> + <item msgid="1160736166977503463">"Ethernet"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"loại mạng không xác định"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..9d6cff90e2c340fb4857bd589a8e3e882b122ac3 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"系统网络连接资æº"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"登录到WLAN网络"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"登录到网络"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> æ— æ³•è®¿é—®äº’è”网"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"点按å³å¯æŸ¥çœ‹ç›¸å…³é€‰é¡¹"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"æ¤ç§»åŠ¨ç½‘ç»œæ— æ³•è®¿é—®äº’è”网"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"æ¤ç½‘ç»œæ— æ³•è®¿é—®äº’è”网"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"æ— æ³•è®¿é—®ç§äºº DNS æœåŠ¡å™¨"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的连接å—é™"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"点按å³å¯ç»§ç»è¿žæŽ¥"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"已切æ¢è‡³<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"设备会在<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>æ— æ³•è®¿é—®äº’è”网时使用<xliff:g id="NEW_NETWORK">%1$s</xliff:g>(å¯èƒ½éœ€è¦æ”¯ä»˜ç›¸åº”的费用)。"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"已从<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切æ¢è‡³<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"移动数æ®"</item> + <item msgid="6341719431034774569">"WLAN"</item> + <item msgid="5081440868800877512">"è“牙"</item> + <item msgid="1160736166977503463">"以太网"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"未知网络类型"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..c84241c8963e765d1fd66afe0acd856fff6a923e --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"系統連線資æº"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"登入 Wi-Fi 網絡"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"登入網絡"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>未有連接至互è¯ç¶²"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"輕按å³å¯æŸ¥çœ‹é¸é …"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"æµå‹•ç¶²çµ¡ä¸¦æœªé€£æŽ¥äº’è¯ç¶²"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"網絡並未連接互è¯ç¶²"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"無法å˜å–ç§äºº DNS 伺æœå™¨"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g>連線å—é™"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"ä»è¦è¼•æŒ‰ä»¥é€£çµè‡³æ¤ç¶²çµ¡"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"已切æ›è‡³<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"è£ç½®æœƒåœ¨ <xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> 無法連線至互è¯ç¶²æ™‚使用<xliff:g id="NEW_NETWORK">%1$s</xliff:g> (å¯èƒ½éœ€è¦æ”¯ä»˜ç›¸é—œè²»ç”¨)。"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"已從<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g>切æ›è‡³<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"æµå‹•æ•¸æ“š"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"è—牙"</item> + <item msgid="1160736166977503463">"以太網絡"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ä¸æ˜Žç¶²çµ¡é¡žåž‹"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..07540d18c0f957708ac6eba5bf1d9c8704d8d5c6 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"系統連線資æº"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"登入 Wi-Fi 網路"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"登入網路"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 沒有網際網路連線"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"輕觸å³å¯æŸ¥çœ‹é¸é …"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"這個行動網路沒有網際網路連線"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"這個網路沒有網際網路連線"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"無法å˜å–ç§äºº DNS 伺æœå™¨"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"<xliff:g id="NETWORK_SSID">%1$s</xliff:g> 的連線能力å—é™"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"輕觸å³å¯ç¹¼çºŒé€£ç·š"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"已切æ›è‡³<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"è£ç½®æœƒåœ¨ç„¡æ³•é€£ä¸Šã€Œ<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g>ã€æ™‚切æ›è‡³ã€Œ<xliff:g id="NEW_NETWORK">%1$s</xliff:g>ã€(å¯èƒ½éœ€è¦æ”¯ä»˜ç›¸é—œè²»ç”¨)。"</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"已從 <xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> 切æ›è‡³<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"行動數據"</item> + <item msgid="6341719431034774569">"Wi-Fi"</item> + <item msgid="5081440868800877512">"è—牙"</item> + <item msgid="1160736166977503463">"乙太網路"</item> + <item msgid="7347618872551558605">"VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"ä¸æ˜Žçš„網路類型"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values-zu/strings.xml b/service/ServiceConnectivityResources/res/values-zu/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..19f390ba61e0a4fb2fde5b90bc859b4de4f595b3 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values-zu/strings.xml @@ -0,0 +1,43 @@ +<?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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="connectivityResourcesAppLabel" msgid="8294935652079168395">"Izinsiza Zokuxhumeka Zesistimu"</string> + <string name="wifi_available_sign_in" msgid="5254156478006453593">"Ngena ngemvume kunethiwekhi ye-Wi-Fi"</string> + <string name="network_available_sign_in" msgid="7794369329839408792">"Ngena ngemvume kunethiwekhi"</string> + <!-- no translation found for network_available_sign_in_detailed (3643910593681893097) --> + <skip /> + <string name="wifi_no_internet" msgid="3961697321010262514">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> ayinakho ukufinyelela kwe-inthanethi"</string> + <string name="wifi_no_internet_detailed" msgid="1229067002306296104">"Thepha ukuze uthole izinketho"</string> + <string name="mobile_no_internet" msgid="2262524005014119639">"Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi"</string> + <string name="other_networks_no_internet" msgid="8226004998719563755">"Inethiwekhi ayinakho ukufinyelela kwenethiwekhi"</string> + <string name="private_dns_broken_detailed" msgid="3537567373166991809">"Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa"</string> + <string name="network_partial_connectivity" msgid="5957065286265771273">"I-<xliff:g id="NETWORK_SSID">%1$s</xliff:g> inokuxhumeka okukhawulelwe"</string> + <string name="network_partial_connectivity_detailed" msgid="6975752539442533034">"Thepha ukuze uxhume noma kunjalo"</string> + <string name="network_switch_metered" msgid="2814798852883117872">"Kushintshelwe ku-<xliff:g id="NETWORK_TYPE">%1$s</xliff:g>"</string> + <string name="network_switch_metered_detail" msgid="605546931076348229">"Idivayisi isebenzisa i-<xliff:g id="NEW_NETWORK">%1$s</xliff:g> uma i-<xliff:g id="PREVIOUS_NETWORK">%2$s</xliff:g> inganakho ukufinyelela kwe-inthanethi. Kungasebenza izindleko."</string> + <string name="network_switch_metered_toast" msgid="8831325515040986641">"Kushintshelewe kusuka ku-<xliff:g id="PREVIOUS_NETWORK">%1$s</xliff:g> kuya ku-<xliff:g id="NEW_NETWORK">%2$s</xliff:g>"</string> + <string-array name="network_switch_type_name"> + <item msgid="5454013645032700715">"idatha yeselula"</item> + <item msgid="6341719431034774569">"I-Wi-Fi"</item> + <item msgid="5081440868800877512">"I-Bluetooth"</item> + <item msgid="1160736166977503463">"I-Ethernet"</item> + <item msgid="7347618872551558605">"I-VPN"</item> + </string-array> + <string name="network_switch_type_name_unknown" msgid="7826330274368951740">"uhlobo olungaziwa lwenethiwekhi"</string> +</resources> diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..70ddb9a7e9fafd2bf5089a7fdc9e83899860561b --- /dev/null +++ b/service/ServiceConnectivityResources/res/values/config.xml @@ -0,0 +1,114 @@ +<?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 values for ConnectivityService + DO NOT EDIT THIS FILE for specific device configuration; instead, use a Runtime Resources + Overlay package following the overlayable.xml configuration in the same directory: + https://source.android.com/devices/architecture/rros --> +<resources> + + <!-- Configuration hook for the URL returned by ConnectivityManager#getCaptivePortalServerUrl. + If empty, the returned value is controlled by Settings.Global.CAPTIVE_PORTAL_HTTP_URL, + and if that value is empty, the framework will use a hard-coded default. + This is *NOT* a URL that will always be used by the system network validation to detect + captive portals: NetworkMonitor may use different strategies and will not necessarily use + this URL. NetworkMonitor behaviour should be configured with NetworkStack resource overlays + instead. --> + <!--suppress CheckTagEmptyBody --> + <string translatable="false" name="config_networkCaptivePortalServerUrl"></string> + + <!-- The maximum duration (in milliseconds) we expect a network transition to take --> + <integer name="config_networkTransitionTimeout">60000</integer> + + <!-- Configuration of network interfaces that support WakeOnLAN --> + <string-array translatable="false" name="config_wakeonlan_supported_interfaces"> + <!-- + <item>wlan0</item> + <item>eth0</item> + --> + </string-array> + + <string-array translatable="false" name="config_legacy_networktype_restore_timers"> + <item>2,60000</item><!-- mobile_mms --> + <item>3,60000</item><!-- mobile_supl --> + <item>4,60000</item><!-- mobile_dun --> + <item>5,60000</item><!-- mobile_hipri --> + <item>10,60000</item><!-- mobile_fota --> + <item>11,60000</item><!-- mobile_ims --> + <item>12,60000</item><!-- mobile_cbs --> + </string-array> + + <!-- Default supported concurrent socket keepalive slots per transport type, used by + ConnectivityManager.createSocketKeepalive() for calculating the number of keepalive + offload slots that should be reserved for privileged access. This string array should be + overridden by the device to present the capability of creating socket keepalives. --> + <!-- An Array of "[NetworkCapabilities.TRANSPORT_*],[supported keepalives] --> + <string-array translatable="false" name="config_networkSupportedKeepaliveCount"> + <item>0,1</item> + <item>1,3</item> + </string-array> + + <!-- Reserved privileged keepalive slots per transport. --> + <integer translatable="false" name="config_reservedPrivilegedKeepaliveSlots">2</integer> + + <!-- Allowed unprivileged keepalive slots per uid. --> + <integer translatable="false" name="config_allowedUnprivilegedKeepalivePerUid">2</integer> + + <!-- Default value for ConnectivityManager.getMultipathPreference() on metered networks. Actual + device behaviour is controlled by the metered multipath preference in + ConnectivitySettingsManager. This is the default value of that setting. --> + <integer translatable="false" name="config_networkMeteredMultipathPreference">0</integer> + + <!-- Whether the device should automatically switch away from Wi-Fi networks that lose + Internet access. Actual device behaviour is controlled by + Settings.Global.NETWORK_AVOID_BAD_WIFI. This is the default value of that setting. --> + <integer translatable="false" name="config_networkAvoidBadWifi">1</integer> + + <!-- Array of ConnectivityManager.TYPE_xxxx constants for networks that may only + be controlled by systemOrSignature apps. --> + <integer-array translatable="false" name="config_protectedNetworks"> + <item>10</item> + <item>11</item> + <item>12</item> + <item>14</item> + <item>15</item> + </integer-array> + + <!-- Whether the internal vehicle network should remain active even when no + apps requested it. --> + <bool name="config_vehicleInternalNetworkAlwaysRequested">false</bool> + + + <!-- If the hardware supports specially marking packets that caused a wakeup of the + main CPU, set this value to the mark used. --> + <integer name="config_networkWakeupPacketMark">0</integer> + + <!-- Mask to use when checking skb mark defined in config_networkWakeupPacketMark above. --> + <integer name="config_networkWakeupPacketMask">0</integer> + + <!-- Whether/how to notify the user on network switches. See LingerMonitor.java. --> + <integer translatable="false" name="config_networkNotifySwitchType">0</integer> + + <!-- What types of network switches to notify. See LingerMonitor.java. --> + <string-array translatable="false" name="config_networkNotifySwitches"> + </string-array> + + <!-- Whether to use an ongoing notification for signing in to captive portals, instead of a + notification that can be dismissed. --> + <bool name="config_ongoingSignInNotification">false</bool> + +</resources> diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd235661dc0684e4888c8ed462b5e7c7d290b2fa --- /dev/null +++ b/service/ServiceConnectivityResources/res/values/overlayable.xml @@ -0,0 +1,37 @@ +<?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. +--> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <overlayable name="ServiceConnectivityResourcesConfig"> + <policy type="product|system|vendor"> + <!-- Configuration values for ConnectivityService --> + <item type="array" name="config_legacy_networktype_restore_timers"/> + <item type="string" name="config_networkCaptivePortalServerUrl"/> + <item type="integer" name="config_networkTransitionTimeout"/> + <item type="array" name="config_wakeonlan_supported_interfaces"/> + <item type="integer" name="config_networkMeteredMultipathPreference"/> + <item type="array" name="config_networkSupportedKeepaliveCount"/> + <item type="integer" name="config_networkAvoidBadWifi"/> + <item type="array" name="config_protectedNetworks"/> + <item type="bool" name="config_vehicleInternalNetworkAlwaysRequested"/> + <item type="integer" name="config_networkWakeupPacketMark"/> + <item type="integer" name="config_networkWakeupPacketMask"/> + <item type="integer" name="config_networkNotifySwitchType"/> + <item type="array" name="config_networkNotifySwitches"/> + <item type="bool" name="config_ongoingSignInNotification"/> + + </policy> + </overlayable> +</resources> diff --git a/service/ServiceConnectivityResources/res/values/strings.xml b/service/ServiceConnectivityResources/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..b2fa5f5b41298deceb95306b6aa4e7c593b265e8 --- /dev/null +++ b/service/ServiceConnectivityResources/res/values/strings.xml @@ -0,0 +1,74 @@ +<?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. + --> +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <!-- The System Connectivity Resources package is an internal system package that provides + configuration values for system networking that were pre-configured in the device. This + is the name of the package to display in the list of system apps. [CHAR LIMIT=40] --> + <string name="connectivityResourcesAppLabel">System Connectivity Resources</string> + + <!-- A notification is shown when a wifi captive portal network is detected. This is the notification's title. --> + <string name="wifi_available_sign_in">Sign in to Wi-Fi network</string> + + <!-- A notification is shown when a captive portal network is detected. This is the notification's title. --> + <string name="network_available_sign_in">Sign in to network</string> + + <!-- A notification is shown when a captive portal network is detected. This is the notification's message. --> + <string name="network_available_sign_in_detailed"><xliff:g id="network_ssid">%1$s</xliff:g></string> + + <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's title. --> + <string name="wifi_no_internet"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has no internet access</string> + + <!-- A notification is shown when the user connects to a Wi-Fi network and the system detects that that network has no Internet access. This is the notification's message. --> + <string name="wifi_no_internet_detailed">Tap for options</string> + + <!-- A notification is shown when the user connects to a mobile network without internet access. This is the notification's title. --> + <string name="mobile_no_internet">Mobile network has no internet access</string> + + <!-- A notification is shown when the user connects to a non-mobile and non-wifi network without internet access. This is the notification's title. --> + <string name="other_networks_no_internet">Network has no internet access</string> + + <!-- A notification is shown when connected network without internet due to private dns validation failed. This is the notification's message. [CHAR LIMIT=NONE] --> + <string name="private_dns_broken_detailed">Private DNS server cannot be accessed</string> + + <!-- A notification is shown when the user connects to a network that doesn't have access to some services (e.g. Push notifications may not work). This is the notification's title. [CHAR LIMIT=50] --> + <string name="network_partial_connectivity"><xliff:g id="network_ssid" example="GoogleGuest">%1$s</xliff:g> has limited connectivity</string> + + <!-- A notification is shown when the user connects to a network that doesn't have access to some services (e.g. Push notifications may not work). This is the notification's message. [CHAR LIMIT=50] --> + <string name="network_partial_connectivity_detailed">Tap to connect anyway</string> + + <!-- A notification might be shown if the device switches to another network type (e.g., mobile data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the notification's title. %1$s is the network type that the device switched to, e.g., cellular data. It is one of the strings in the network_switch_type_name array. --> + <string name="network_switch_metered">Switched to <xliff:g id="network_type">%1$s</xliff:g></string> + + <!-- A notification might be shown if the device switches to another network type (e.g., mobile data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the notification's message. %1$s is the network that the device switched to, e.g., cellular data. %2$s is the network type the device switched from, e.g., Wi-Fi. Both are strings in the network_switch_type_name array. --> + <string name="network_switch_metered_detail">Device uses <xliff:g id="new_network">%1$s</xliff:g> when <xliff:g id="previous_network">%2$s</xliff:g> has no internet access. Charges may apply.</string> + + <!-- A toast might be shown if the device switches to another network type (e.g., mobile data) because it detects that the network it was using (e.g., Wi-Fi) has lost Internet connectivity. This is the text of the toast. %1$s is the network that the device switched from, e.g., Wi-Fi. %2$s is the network type the device switched from, e.g., cellular data. Both are strings in the network_switch_type_name array. --> + <string name="network_switch_metered_toast">Switched from <xliff:g id="previous_network">%1$s</xliff:g> to <xliff:g id="new_network">%2$s</xliff:g></string> + + <!-- Network type names used in the network_switch_metered and network_switch_metered_detail strings. These must be kept in the sync with the values NetworkCapabilities.TRANSPORT_xxx values, and in the same order. --> + <string-array name="network_switch_type_name"> + <item>mobile data</item> + <item>Wi-Fi</item> + <item>Bluetooth</item> + <item>Ethernet</item> + <item>VPN</item> + </string-array> + + <!-- Network type name displayed if one of the types is not found in network_switch_type_name. --> + <string name="network_switch_type_name_unknown">an unknown network type</string> + +</resources> diff --git a/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.pk8 b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.pk8 new file mode 100644 index 0000000000000000000000000000000000000000..bfdc28b313ec197438aa5fa9a0ab16c2a2d711f8 Binary files /dev/null and b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.pk8 differ diff --git a/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.x509.pem b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.x509.pem new file mode 100644 index 0000000000000000000000000000000000000000..70eca1cee1d0f613bb6ad1b4f089a5f2b282d631 --- /dev/null +++ b/service/ServiceConnectivityResources/resources-certs/com.android.connectivity.resources.x509.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIGQzCCBCugAwIBAgIUZY8nxBMINp/79sziXU77MLPpEXowDQYJKoZIhvcNAQEL +BQAwga8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMSswKQYDVQQDDCJjb20uYW5kcm9pZC5jb25uZWN0aXZpdHkucmVzb3VyY2Vz +MSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMCAXDTIxMDQyMjA3 +MjkxMFoYDzQ3NTkwMzE5MDcyOTEwWjCBrzELMAkGA1UEBhMCVVMxEzARBgNVBAgM +CkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEDAOBgNVBAoMB0Fu +ZHJvaWQxEDAOBgNVBAsMB0FuZHJvaWQxKzApBgNVBAMMImNvbS5hbmRyb2lkLmNv +bm5lY3Rpdml0eS5yZXNvdXJjZXMxIjAgBgkqhkiG9w0BCQEWE2FuZHJvaWRAYW5k +cm9pZC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC361NT9qSz +h3uLcLBD67HNE1QX3ykwGyw8u7ExzqpsqLCzZsOCFRJQJY+CnrgNaAz0NXeNtx7D +Lpr9OCWWbG1KTQ/ANlR8g6xCqlAk4xdixsAnIlBUJB90+RlkcWrliEY7OwcqIu3x +/qe+5UR3irIFZOApNHOm760PjRl7VWAnYZC/PhkW0iKwnBuE96ddPIJc+KuiqCcP +KflgF4/jmbHTZ+5uvVV4qkfovc744HnQtQoCDoYR8WpsJv3YL5xrAv78o3WCRzx6 +xxB+eUlJpuyyfIee2lUCG4Ly4jgOsWaupnUglLDORnz/L8fhhnpv83wLal7E0Shx +sqvzZZbb1QLuwMWy++gfzdDvGWewES3BdSFp5NwYWXQGZWSkEEFbIiorKSurU1On +9OwB0jT/H2B/CAFKYJQ2V+hQ4I7PG+z9p7ZFNR6GZbZuhEr+Dpq1CwtI3W45izr3 +RJgcc2IP6Oj7/XC2MmKGMqZkybBWcvazdyAMHzk9EZIBT2Oru3dnOl3uVUUPeZRs +xRzqaA0MAlyj+GJ9uziEr3W1j+U1CFEnNWtlD/jqcTAwmaOsn1GhWyMAo1KOrJ/o +LcJvwk5P/0XEyeli7/DSUpGjYiAgWMHWCOn9s6aYw3YFb+A/SgX3/+FIDib/vHTX +i76JZfO0CfoKsbFDCH9KOMupHM9EO3ftQwIDAQABo1MwUTAdBgNVHQ4EFgQU/KGg +gmMqXD5YOe5+B0W+YezN9LcwHwYDVR0jBBgwFoAU/KGggmMqXD5YOe5+B0W+YezN +9LcwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAhr+AaNaIlRyM +WKyJ+2Aa35fH5e44Xr/xPpriM5HHxsj0evjMCODqCQ7kzfwSEmtXh5uZYYNKb/JP +ZMDHIFcYi1QCvm6E6YOd+Qn9CVxrwaDhigJv7ylhVf8q201GTvHhJIU99yFIrzJQ +RNhxw+pNo7FYMZr3J7JZPAy60DN1KZvRV4FjZx5qiPUMyu4zVygzDkr0v5Ilncdp +l9VVjOi7ocHyBKI+7RkXl97xN4SUe3vszwZQHCVyVopBw+YrMbDBCrknrQzUEgie +BuI+kj5oOeiQ0P1i1K+UCCAjrLwhNyc9H02rKUtBHxa2AVjw7YpAJlBesb49Qvq+ +5L6JjHFVSSOEbIjboNib26zNackjbiefF74meSUbGVGfcJ1OdkZsXZWphmER8V7X +Wz3Z8JwOXW1RLPgcbjilHUR5g8pEmWBv4KrTCSg5IvOJr4w3pyyMBiiVI9NI5sB7 +g5Mi9v3ifPD1OHA4Y3wYCb26mMEpRb8ogOhMHcGNbdnL3QtIUg4cmXGqGSY/LbpU +np0sIQDSjc46o79F0boPsLlaN3US5WZIu0nc9SHkjoNhd0CJQ5r9aEn4/wNrZgxs +s8OEKsqcS7OsWiIE6nG51TMDsCuyRBrGedtSUyFFSVSpivpYIrPVNKKlHsJ/o+Nv +Udb6dBjCraPvJB8binB1aojwya3MwRs= +-----END CERTIFICATE----- diff --git a/service/ServiceConnectivityResources/resources-certs/key.pem b/service/ServiceConnectivityResources/resources-certs/key.pem new file mode 100644 index 0000000000000000000000000000000000000000..38771c261518e8cf605044d9bc0ca284d9279780 --- /dev/null +++ b/service/ServiceConnectivityResources/resources-certs/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC361NT9qSzh3uL +cLBD67HNE1QX3ykwGyw8u7ExzqpsqLCzZsOCFRJQJY+CnrgNaAz0NXeNtx7DLpr9 +OCWWbG1KTQ/ANlR8g6xCqlAk4xdixsAnIlBUJB90+RlkcWrliEY7OwcqIu3x/qe+ +5UR3irIFZOApNHOm760PjRl7VWAnYZC/PhkW0iKwnBuE96ddPIJc+KuiqCcPKflg +F4/jmbHTZ+5uvVV4qkfovc744HnQtQoCDoYR8WpsJv3YL5xrAv78o3WCRzx6xxB+ +eUlJpuyyfIee2lUCG4Ly4jgOsWaupnUglLDORnz/L8fhhnpv83wLal7E0Shxsqvz +ZZbb1QLuwMWy++gfzdDvGWewES3BdSFp5NwYWXQGZWSkEEFbIiorKSurU1On9OwB +0jT/H2B/CAFKYJQ2V+hQ4I7PG+z9p7ZFNR6GZbZuhEr+Dpq1CwtI3W45izr3RJgc +c2IP6Oj7/XC2MmKGMqZkybBWcvazdyAMHzk9EZIBT2Oru3dnOl3uVUUPeZRsxRzq +aA0MAlyj+GJ9uziEr3W1j+U1CFEnNWtlD/jqcTAwmaOsn1GhWyMAo1KOrJ/oLcJv +wk5P/0XEyeli7/DSUpGjYiAgWMHWCOn9s6aYw3YFb+A/SgX3/+FIDib/vHTXi76J +ZfO0CfoKsbFDCH9KOMupHM9EO3ftQwIDAQABAoICAQCXM/GKqtAXBIBOT/Ops0C2 +n3hYM9BRy1UgDRKNJyG3OSwkIY0ECbzHhUmpkkEwTGWx8675JB43Sr6DBUDpnPRw +zE/xrvjgcQQSvqAq40PbohhhU/WEZzoxWYVFrXS7hcBve4TVYGgMtlZEO4qBWNYo +Vxlu5r9Z89tsWI0ldzgYyD5O64eG2nVIit6Y/11p6pAmTQ4WKHYMIm7xUA2siTPH +4L8F7cQx8pQxxLI+q5WaPuweasBQShA7IAc7T1EiLRFitCOsWlJfgf6Oa7oTwhcA +Wh7JOyf+Fo4ejlqVwcTwOss6YOPGge7LgQWr5HoORbeqTuXgmy/L4Z85+EABNOs1 +5muHZvsuPXSmW6g1bCi8zvQcjFIX31yBVg8zkdG8WRezFxiVlN8UFAx4rwo03aBs +rDyU4GCxoUBvF/M9534l1gKOyr0hlQ40nQ4kBabbm2wWOKCVzmLEtFmWX9RV0tjX +pEtTCqgsGlsIypLy21+uow8SBojhkZ+xORCF2XivGu6SKtvwGvjpYXpXrI6DN4Lw +kH5J5FwSu1SNY8tnIEJEmj8IMTp+Vw20kwNVTcwdC2nJDDiezJum4PqZRdWIuupm +BWzXD3fvMXqHmT02sJTQ+FRAgiQLLWDzNAYMJUofzuIwycs4iO9MOPHjkHScvk4N +FXLrzFBSbdw+wi1DdzzMuQKCAQEA5wx07O5bHBHybs6tpwuZ0TuJ3OIVXh/ocNVR +gSOCSMirv+K4u3jToXwjfTXUc9lcn+DenZPpGmUWF0sZ83ytZm1eFVgGZpP6941C +waSeb8zGsgbEyZIQTVILfgtyPDwdtgu0d1Ip+ppj9czXmnxMY/ruHOX1Do1UfZoA +UA1ytHJSjFKU6saAhHrdk91soTVzc/E3uo7U4Ff0L8/3tT3DAEFYxDXUCH8W2IZZ +6zVvlqnPH4elxsPYM6rtIwq52reOTLNxC+SFSamK/82zu09Kjj5sQ6HKlvKJFiL5 +bULWu4lenoDfEN0lng+QopJTgZq4/tgOLum43C/Zd0PGC9R6PwKCAQEAy8fvPqwM +gPbNasni9qGVG+FfiFd/wEMlgKlVVqi+WzF6tCAnXCQXXg3A7FpLQrX8hVKdMznq +wPgM5AXP4FOguBFNk65chZmPizBIUDPJ4TNHI8FcGgcxbKGvDdVHsUpa/h5rJlvV +GLJTKV4KjcsTjl5tlRsJ48bSfpBNQHpSKlCswT6jjteiDY6Rln0GFKQIKDHqp3I6 +Zn1E4yfdiIz9VnMPfg1fbjBeR7s1zNzlrR8Dv9oK9tkzI5G1wSbdzksg2O1q2tvg +WrZrTAA3Uw6sPUMft0vk5Jw6a6CLkrcfayv3xDHwvM/4P3HgP8j9WQ8at8ttHpfD +oWyt3fZ3pBuj/QKCAQANqxH7tjoTlgg2f+mL+Ua3NwN32rQS5mZUznnM3vHlJmHq +rxnolURHyFU9IgMYe2JcXuwsfESM+C/vXtUBL33+kje/oX53cQemv2eUlw18ZavX +ekkH96kZOeJOKZUvdQr46wZZDLZJCfsh3mVe0T2fqIePlBcELl4yM/sSwUjo3d5+ +SKBgpy+RJseW6MF1Y/kZgcqfMbXsM6fRcEciJK41hKggq2KIwiPy2TfWj0mzqwYC +wn6PHKTcoZ73tLm786Hqba8hWfp8mhgL+/pG+XDaq1yyP48BkQWFFrqUuSCE5aKA +U/VeRQblq9wNkgR4pVOOV++23MK/2+DMimjb6Ez3AoIBABIXK7wKlgmU32ONjKKM +capJ9asq6WJuE5Q6dCL/U/bQi64V9KiPY6ur2OailW/UrBhB30a+64I6AxrzESM/ +CVON5a8omXoayc13edP05QUjAjvAXKbK4K5eJCY8OuMYUL+if6ymFmLc4dkYSiOQ +Vaob4+qKvfQEoIcv1EvXEBhFlTCKmQaDShWeBHqxmqqWbUr0M3qt/1U95bGsxlPr +AEp+aG+uTDyB+ryvd/U53wHhcPnFJ5gGbC3KL7J3+tTngoD/gq7vOhmTfC8BDehH +sy61GMmy6R0KaX1IgVuC+j0PaC14qYB5jfZD675930/asWqDmqpOmsVn2n+L888T +zRkCggEBAIMuNhhfGGY6E4PLUcPM0LZA4tI/wTpeYEahunU1hWIYo/iZB9od2biz +EeYY4BtkzCoE5ZWYXqTgiMxN4hJ4ufB+5umZ4BO0Gyx4p2/Ik2uv1BXu++GbM+TI +eeFmaBh00dTtjccpeZEDgNkjAO7Rh9GV2ifl3uhqg0MnFXywPUX2Vm2bmwQXnfV9 +wY2TXgOmBN2epFBOArJwiA5IfV+bSqXCFCx8fgyOWpMNq9+zDRd6KCeHyge54ahm +jMhCncp1OPDPaV+gnUdgWDGcywYg0KQvu5dLuCFfvucnsWoH2txsVZrXFha5XSM4 +/4Pif3Aj5E9dm1zkUtZJYQbII5SKQ94= +-----END PRIVATE KEY----- diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..5caa11b11ae4b35e4b45c9802418111c7a490532 --- /dev/null +++ b/service/jarjar-rules.txt @@ -0,0 +1,17 @@ +rule android.sysprop.** com.android.connectivity.sysprop.@1 +rule com.android.net.module.util.** com.android.connectivity.net-utils.@1 +rule com.android.modules.utils.** com.android.connectivity.modules-utils.@1 + +# internal util classes +# Exclude AsyncChannel. TODO: remove AsyncChannel usage in ConnectivityService +rule com.android.internal.util.AsyncChannel* @0 +# Exclude LocationPermissionChecker. This is going to be moved to libs/net +rule com.android.internal.util.LocationPermissionChecker* @0 +rule android.util.LocalLog* com.android.connectivity.util.LocalLog@1 +# android.util.IndentingPrintWriter* should use a different package name from +# the one in com.android.internal.util +rule android.util.IndentingPrintWriter* android.connectivity.util.IndentingPrintWriter@1 +rule com.android.internal.util.** com.android.connectivity.util.@1 + +rule com.android.internal.messages.** com.android.connectivity.messages.@1 +rule com.google.protobuf.** com.android.connectivity.protobuf.@1 diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e7a40e5ea66bea34c464d71197829d527262d6d6 --- /dev/null +++ b/service/jni/com_android_server_TestNetworkService.cpp @@ -0,0 +1,104 @@ +/* + * 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. + */ + +#define LOG_NDEBUG 0 + +#define LOG_TAG "TestNetworkServiceJni" + +#include <arpa/inet.h> +#include <errno.h> +#include <fcntl.h> +#include <linux/if.h> +#include <linux/if_tun.h> +#include <linux/ipv6_route.h> +#include <linux/route.h> +#include <netinet/in.h> +#include <stdio.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <log/log.h> + +#include "jni.h" +#include <android-base/stringprintf.h> +#include <android-base/unique_fd.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedUtfChars.h> + +namespace android { + +//------------------------------------------------------------------------------ + +static void throwException(JNIEnv* env, int error, const char* action, const char* iface) { + const std::string& msg = "Error: " + std::string(action) + " " + std::string(iface) + ": " + + std::string(strerror(error)); + jniThrowException(env, "java/lang/IllegalStateException", msg.c_str()); +} + +static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) { + base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK)); + ifreq ifr{}; + + // Allocate interface. + ifr.ifr_flags = (isTun ? IFF_TUN : IFF_TAP) | IFF_NO_PI; + strlcpy(ifr.ifr_name, iface, IFNAMSIZ); + if (ioctl(tun.get(), TUNSETIFF, &ifr)) { + throwException(env, errno, "allocating", ifr.ifr_name); + return -1; + } + + // Activate interface using an unconnected datagram socket. + base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0)); + ifr.ifr_flags = IFF_UP; + + if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) { + throwException(env, errno, "activating", ifr.ifr_name); + return -1; + } + + return tun.release(); +} + +//------------------------------------------------------------------------------ + +static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) { + ScopedUtfChars iface(env, jIface); + if (!iface.c_str()) { + jniThrowNullPointerException(env, "iface"); + return -1; + } + + int tun = createTunTapInterface(env, isTun, iface.c_str()); + + // Any exceptions will be thrown from the createTunTapInterface call + return tun; +} + +//------------------------------------------------------------------------------ + +static const JNINativeMethod gMethods[] = { + {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create}, +}; + +int register_android_server_TestNetworkService(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/TestNetworkService", gMethods, + NELEM(gMethods)); +} + +}; // namespace android diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp new file mode 100644 index 0000000000000000000000000000000000000000..00128794bcd06cb845336eb4440f860fa5150984 --- /dev/null +++ b/service/jni/onload.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 <log/log.h> + +namespace android { + +int register_android_server_TestNetworkService(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) { + ALOGE("GetEnv failed"); + return JNI_ERR; + } + + if (register_android_server_TestNetworkService(env) < 0) { + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} + +}; diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml new file mode 100644 index 0000000000000000000000000000000000000000..119b64ff95afb85ad6316a2e6c19e0e3b2fa2cfa --- /dev/null +++ b/service/lint-baseline.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="5" by="lint 4.1.0" client="cli" variant="all" version="4.1.0"> + + <issue + id="NewApi" + message="Call requires API level 31 (current min is 30): `android.telephony.TelephonyManager#isDataCapable`" + errorLine1=" if (tm.isDataCapable()) {" + errorLine2=" ~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java" + line="787" + column="20"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 31 (current min is 30): `android.content.Context#sendStickyBroadcast`" + errorLine1=" mUserAllContext.sendStickyBroadcast(intent, options);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java" + line="2681" + column="33"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 31 (current min is 30): `android.content.pm.PackageManager#getTargetSdkVersion`" + errorLine1=" final int callingVersion = pm.getTargetSdkVersion(callingPackageName);" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java" + line="5851" + column="43"/> + </issue> + +</issues> diff --git a/service/proto/connectivityproto.proto b/service/proto/connectivityproto.proto new file mode 100644 index 0000000000000000000000000000000000000000..a992d7c26368a8c0f24bb6c762bdf11e8af59296 --- /dev/null +++ b/service/proto/connectivityproto.proto @@ -0,0 +1,20 @@ +/* + * 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. + */ + +syntax = "proto2"; + +// Connectivity protos can be created in this directory. Note this file must be included before +// building system-messages-proto, otherwise it will not build by itself. diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java new file mode 100644 index 0000000000000000000000000000000000000000..5c47f27ef58f17c4c8a6af9e140f73f59719516d --- /dev/null +++ b/service/src/com/android/server/ConnectivityService.java @@ -0,0 +1,10077 @@ +/* + * 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 com.android.server; + +import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE; +import static android.content.pm.PackageManager.FEATURE_BLUETOOTH; +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.PERMISSION_GRANTED; +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.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.ConnectivityManager.BLOCKED_METERED_REASON_MASK; +import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN; +import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; +import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +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_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_NONE; +import static android.net.ConnectivityManager.TYPE_PROXY; +import static android.net.ConnectivityManager.TYPE_VPN; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIFI_P2P; +import static android.net.ConnectivityManager.getNetworkTypeName; +import static android.net.ConnectivityManager.isNetworkTypeValid; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; +import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST; +import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequired; +import static android.os.Process.INVALID_UID; +import static android.os.Process.VPN_UID; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.IPPROTO_UDP; + +import static java.util.Map.Entry; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.app.BroadcastOptions; +import android.app.PendingIntent; +import android.app.usage.NetworkStatsManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.net.CaptivePortal; +import android.net.CaptivePortalData; +import android.net.ConnectionInfo; +import android.net.ConnectivityAnnotations.RestrictBackgroundStatus; +import android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import android.net.ConnectivityDiagnosticsManager.DataStallReport; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.BlockedReason; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.ConnectivityResources; +import android.net.ConnectivitySettingsManager; +import android.net.DataStallReportParcelable; +import android.net.DnsResolverServiceManager; +import android.net.ICaptivePortal; +import android.net.IConnectivityDiagnosticsCallback; +import android.net.IConnectivityManager; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.INetworkActivityListener; +import android.net.INetworkAgent; +import android.net.INetworkMonitor; +import android.net.INetworkMonitorCallbacks; +import android.net.INetworkOfferCallback; +import android.net.IOnCompleteListener; +import android.net.IQosCallback; +import android.net.ISocketKeepaliveCallback; +import android.net.InetAddresses; +import android.net.IpMemoryStore; +import android.net.IpPrefix; +import android.net.LinkProperties; +import android.net.MatchAllNetworkSpecifier; +import android.net.NativeNetworkConfig; +import android.net.NativeNetworkType; +import android.net.NattSocketKeepalive; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkMonitorManager; +import android.net.NetworkPolicyManager; +import android.net.NetworkPolicyManager.NetworkPolicyCallback; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.NetworkSpecifier; +import android.net.NetworkStack; +import android.net.NetworkState; +import android.net.NetworkStateSnapshot; +import android.net.NetworkTestResultParcelable; +import android.net.NetworkUtils; +import android.net.NetworkWatchlistManager; +import android.net.OemNetworkPreferences; +import android.net.PrivateDnsConfigParcel; +import android.net.ProxyInfo; +import android.net.QosCallbackException; +import android.net.QosFilter; +import android.net.QosSocketFilter; +import android.net.QosSocketInfo; +import android.net.RouteInfo; +import android.net.RouteInfoParcel; +import android.net.SocketKeepalive; +import android.net.TetheringManager; +import android.net.TransportInfo; +import android.net.UidRange; +import android.net.UidRangeParcel; +import android.net.UnderlyingNetworkInfo; +import android.net.Uri; +import android.net.VpnManager; +import android.net.VpnTransportInfo; +import android.net.metrics.IpConnectivityLog; +import android.net.metrics.NetworkEvent; +import android.net.netlink.InetDiagMessage; +import android.net.networkstack.ModuleNetworkStackClient; +import android.net.networkstack.NetworkStackClientBase; +import android.net.resolv.aidl.DnsHealthEventParcel; +import android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener; +import android.net.resolv.aidl.Nat64PrefixEventParcel; +import android.net.resolv.aidl.PrivateDnsValidationEventParcel; +import android.net.shared.PrivateDnsConfig; +import android.net.util.MultinetworkPolicyTracker; +import android.os.BatteryStatsManager; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.sysprop.NetworkProperties; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.LocalLog; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import com.android.connectivity.resources.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.MessageUtils; +import com.android.modules.utils.BasicShellCommandHandler; +import com.android.net.module.util.BaseNetdUnsolicitedEventListener; +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult; +import com.android.net.module.util.LinkPropertiesUtils.CompareResult; +import com.android.net.module.util.LocationPermissionChecker; +import com.android.net.module.util.NetworkCapabilitiesUtils; +import com.android.net.module.util.PermissionUtils; +import com.android.server.connectivity.AutodestructReference; +import com.android.server.connectivity.DnsManager; +import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate; +import com.android.server.connectivity.FullScore; +import com.android.server.connectivity.KeepaliveTracker; +import com.android.server.connectivity.LingerMonitor; +import com.android.server.connectivity.MockableSystemProperties; +import com.android.server.connectivity.NetworkAgentInfo; +import com.android.server.connectivity.NetworkDiagnostics; +import com.android.server.connectivity.NetworkNotificationManager; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; +import com.android.server.connectivity.NetworkOffer; +import com.android.server.connectivity.NetworkRanker; +import com.android.server.connectivity.PermissionMonitor; +import com.android.server.connectivity.ProfileNetworkPreferences; +import com.android.server.connectivity.ProxyTracker; +import com.android.server.connectivity.QosCallbackTracker; + +import libcore.io.IoUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +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.SortedSet; +import java.util.StringJoiner; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @hide + */ +public class ConnectivityService extends IConnectivityManager.Stub + implements PendingIntent.OnFinished { + private static final String TAG = ConnectivityService.class.getSimpleName(); + + private static final String DIAG_ARG = "--diag"; + public static final String SHORT_ARG = "--short"; + private static final String NETWORK_ARG = "networks"; + private static final String REQUEST_ARG = "requests"; + + private static final boolean DBG = true; + private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); + + private static final boolean LOGD_BLOCKED_NETWORKINFO = true; + + /** + * Default URL to use for {@link #getCaptivePortalServerUrl()}. This should not be changed + * by OEMs for configuration purposes, as this value is overridden by + * ConnectivitySettingsManager.CAPTIVE_PORTAL_HTTP_URL. + * R.string.config_networkCaptivePortalServerUrl should be overridden instead for this purpose + * (preferably via runtime resource overlays). + */ + private static final String DEFAULT_CAPTIVE_PORTAL_HTTP_URL = + "http://connectivitycheck.gstatic.com/generate_204"; + + // TODO: create better separation between radio types and network types + + // how long to wait before switching back to a radio's default network + private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000; + // system property that can override the above value + private static final String NETWORK_RESTORE_DELAY_PROP_NAME = + "android.telephony.apn-restore"; + + // How long to wait before putting up a "This network doesn't have an Internet connection, + // connect anyway?" dialog after the user selects a network that doesn't validate. + private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000; + + // Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing. + private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger"; + private static final int DEFAULT_LINGER_DELAY_MS = 30_000; + private static final int DEFAULT_NASCENT_DELAY_MS = 5_000; + + // The maximum number of network request allowed per uid before an exception is thrown. + private static final int MAX_NETWORK_REQUESTS_PER_UID = 100; + + // The maximum number of network request allowed for system UIDs before an exception is thrown. + @VisibleForTesting + static final int MAX_NETWORK_REQUESTS_PER_SYSTEM_UID = 250; + + @VisibleForTesting + protected int mLingerDelayMs; // Can't be final, or test subclass constructors can't change it. + @VisibleForTesting + protected int mNascentDelayMs; + + // How long to delay to removal of a pending intent based request. + // See ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS + private final int mReleasePendingIntentDelayMs; + + private MockableSystemProperties mSystemProperties; + + @VisibleForTesting + protected final PermissionMonitor mPermissionMonitor; + + private final PerUidCounter mNetworkRequestCounter; + @VisibleForTesting + final PerUidCounter mSystemNetworkRequestCounter; + + private volatile boolean mLockdownEnabled; + + /** + * Stale copy of uid blocked reasons provided by NPMS. As long as they are accessed only in + * internal handler thread, they don't need a lock. + */ + private SparseIntArray mUidBlockedReasons = new SparseIntArray(); + + private final Context mContext; + private final ConnectivityResources mResources; + // The Context is created for UserHandle.ALL. + private final Context mUserAllContext; + private final Dependencies mDeps; + // 0 is full bad, 100 is full good + private int mDefaultInetConditionPublished = 0; + + @VisibleForTesting + protected IDnsResolver mDnsResolver; + @VisibleForTesting + protected INetd mNetd; + private NetworkStatsManager mStatsManager; + private NetworkPolicyManager mPolicyManager; + private final NetdCallback mNetdCallback; + + /** + * TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple + * instances. + */ + @GuardedBy("mTNSLock") + private TestNetworkService mTNS; + + private final Object mTNSLock = new Object(); + + private String mCurrentTcpBufferSizes; + + private static final SparseArray<String> sMagicDecoderRing = MessageUtils.findMessageNames( + new Class[] { ConnectivityService.class, NetworkAgent.class, NetworkAgentInfo.class }); + + private enum ReapUnvalidatedNetworks { + // Tear down networks that have no chance (e.g. even if validated) of becoming + // the highest scoring network satisfying a NetworkRequest. This should be passed when + // all networks have been rematched against all NetworkRequests. + REAP, + // Don't reap networks. This should be passed when some networks have not yet been + // rematched against all NetworkRequests. + DONT_REAP + } + + private enum UnneededFor { + LINGER, // Determine whether this network is unneeded and should be lingered. + TEARDOWN, // Determine whether this network is unneeded and should be torn down. + } + + /** + * used internally to clear a wakelock when transitioning + * from one net to another. Clear happens when we get a new + * network - EVENT_EXPIRE_NET_TRANSITION_WAKELOCK happens + * after a timeout if no network is found (typically 1 min). + */ + private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8; + + /** + * used internally to reload global proxy settings + */ + private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY = 9; + + /** + * PAC manager has received new port. + */ + private static final int EVENT_PROXY_HAS_CHANGED = 16; + + /** + * used internally when registering NetworkProviders + * obj = NetworkProviderInfo + */ + private static final int EVENT_REGISTER_NETWORK_PROVIDER = 17; + + /** + * used internally when registering NetworkAgents + * obj = Messenger + */ + private static final int EVENT_REGISTER_NETWORK_AGENT = 18; + + /** + * used to add a network request + * includes a NetworkRequestInfo + */ + private static final int EVENT_REGISTER_NETWORK_REQUEST = 19; + + /** + * indicates a timeout period is over - check if we had a network yet or not + * and if not, call the timeout callback (but leave the request live until they + * cancel it. + * includes a NetworkRequestInfo + */ + private static final int EVENT_TIMEOUT_NETWORK_REQUEST = 20; + + /** + * used to add a network listener - no request + * includes a NetworkRequestInfo + */ + private static final int EVENT_REGISTER_NETWORK_LISTENER = 21; + + /** + * used to remove a network request, either a listener or a real request + * arg1 = UID of caller + * obj = NetworkRequest + */ + private static final int EVENT_RELEASE_NETWORK_REQUEST = 22; + + /** + * used internally when registering NetworkProviders + * obj = Messenger + */ + private static final int EVENT_UNREGISTER_NETWORK_PROVIDER = 23; + + /** + * used internally to expire a wakelock when transitioning + * from one net to another. Expire happens when we fail to find + * a new network (typically after 1 minute) - + * EVENT_CLEAR_NET_TRANSITION_WAKELOCK happens if we had found + * a replacement network. + */ + private static final int EVENT_EXPIRE_NET_TRANSITION_WAKELOCK = 24; + + /** + * used to add a network request with a pending intent + * obj = NetworkRequestInfo + */ + private static final int EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT = 26; + + /** + * used to remove a pending intent and its associated network request. + * arg1 = UID of caller + * obj = PendingIntent + */ + private static final int EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT = 27; + + /** + * used to specify whether a network should be used even if unvalidated. + * arg1 = whether to accept the network if it's unvalidated (1 or 0) + * arg2 = whether to remember this choice in the future (1 or 0) + * obj = network + */ + private static final int EVENT_SET_ACCEPT_UNVALIDATED = 28; + + /** + * used to ask the user to confirm a connection to an unvalidated network. + * obj = network + */ + private static final int EVENT_PROMPT_UNVALIDATED = 29; + + /** + * used internally to (re)configure always-on networks. + */ + private static final int EVENT_CONFIGURE_ALWAYS_ON_NETWORKS = 30; + + /** + * used to add a network listener with a pending intent + * obj = NetworkRequestInfo + */ + private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31; + + /** + * used to specify whether a network should not be penalized when it becomes unvalidated. + */ + private static final int EVENT_SET_AVOID_UNVALIDATED = 35; + + /** + * used to trigger revalidation of a network. + */ + private static final int EVENT_REVALIDATE_NETWORK = 36; + + // Handle changes in Private DNS settings. + private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37; + + // Handle private DNS validation status updates. + private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38; + + /** + * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has + * been tested. + * obj = {@link NetworkTestedResults} representing information sent from NetworkMonitor. + * data = PersistableBundle of extras passed from NetworkMonitor. If {@link + * NetworkMonitorCallbacks#notifyNetworkTested} is called, this will be null. + */ + private static final int EVENT_NETWORK_TESTED = 41; + + /** + * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the private DNS + * config was resolved. + * obj = PrivateDnsConfig + * arg2 = netid + */ + private static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = 42; + + /** + * Request ConnectivityService display provisioning notification. + * arg1 = Whether to make the notification visible. + * arg2 = NetID. + * obj = Intent to be launched when notification selected by user, null if !arg1. + */ + private static final int EVENT_PROVISIONING_NOTIFICATION = 43; + + /** + * Used to specify whether a network should be used even if connectivity is partial. + * arg1 = whether to accept the network if its connectivity is partial (1 for true or 0 for + * false) + * arg2 = whether to remember this choice in the future (1 for true or 0 for false) + * obj = network + */ + private static final int EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY = 44; + + /** + * Event for NetworkMonitor to inform ConnectivityService that the probe status has changed. + * Both of the arguments are bitmasks, and the value of bits come from + * INetworkMonitor.NETWORK_VALIDATION_PROBE_*. + * arg1 = A bitmask to describe which probes are completed. + * arg2 = A bitmask to describe which probes are successful. + */ + public static final int EVENT_PROBE_STATUS_CHANGED = 45; + + /** + * Event for NetworkMonitor to inform ConnectivityService that captive portal data has changed. + * arg1 = unused + * arg2 = netId + * obj = captive portal data + */ + private static final int EVENT_CAPPORT_DATA_CHANGED = 46; + + /** + * Used by setRequireVpnForUids. + * arg1 = whether the specified UID ranges are required to use a VPN. + * obj = Array of UidRange objects. + */ + private static final int EVENT_SET_REQUIRE_VPN_FOR_UIDS = 47; + + /** + * Used internally when setting the default networks for OemNetworkPreferences. + * obj = Pair<OemNetworkPreferences, listener> + */ + private static final int EVENT_SET_OEM_NETWORK_PREFERENCE = 48; + + /** + * Used to indicate the system default network becomes active. + */ + private static final int EVENT_REPORT_NETWORK_ACTIVITY = 49; + + /** + * Used internally when setting a network preference for a user profile. + * obj = Pair<ProfileNetworkPreference, Listener> + */ + private static final int EVENT_SET_PROFILE_NETWORK_PREFERENCE = 50; + + /** + * Event to specify that reasons for why an uid is blocked changed. + * arg1 = uid + * arg2 = blockedReasons + */ + private static final int EVENT_UID_BLOCKED_REASON_CHANGED = 51; + + /** + * Event to register a new network offer + * obj = NetworkOffer + */ + private static final int EVENT_REGISTER_NETWORK_OFFER = 52; + + /** + * Event to unregister an existing network offer + * obj = INetworkOfferCallback + */ + private static final int EVENT_UNREGISTER_NETWORK_OFFER = 53; + + /** + * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification + * should be shown. + */ + private static final int PROVISIONING_NOTIFICATION_SHOW = 1; + + /** + * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification + * should be hidden. + */ + private static final int PROVISIONING_NOTIFICATION_HIDE = 0; + + private static String eventName(int what) { + return sMagicDecoderRing.get(what, Integer.toString(what)); + } + + private static IDnsResolver getDnsResolver(Context context) { + final DnsResolverServiceManager dsm = context.getSystemService( + DnsResolverServiceManager.class); + return IDnsResolver.Stub.asInterface(dsm.getService()); + } + + /** Handler thread used for all of the handlers below. */ + @VisibleForTesting + protected final HandlerThread mHandlerThread; + /** Handler used for internal events. */ + final private InternalHandler mHandler; + /** Handler used for incoming {@link NetworkStateTracker} events. */ + final private NetworkStateTrackerHandler mTrackerHandler; + /** Handler used for processing {@link android.net.ConnectivityDiagnosticsManager} events */ + @VisibleForTesting + final ConnectivityDiagnosticsHandler mConnectivityDiagnosticsHandler; + + private final DnsManager mDnsManager; + private final NetworkRanker mNetworkRanker; + + private boolean mSystemReady; + private Intent mInitialBroadcast; + + private PowerManager.WakeLock mNetTransitionWakeLock; + private final PowerManager.WakeLock mPendingIntentWakeLock; + + // A helper object to track the current default HTTP proxy. ConnectivityService needs to tell + // the world when it changes. + @VisibleForTesting + protected final ProxyTracker mProxyTracker; + + final private SettingsObserver mSettingsObserver; + + private UserManager mUserManager; + + // the set of network types that can only be enabled by system/sig apps + private List<Integer> mProtectedNetworks; + + private Set<String> mWolSupportedInterfaces; + + private final TelephonyManager mTelephonyManager; + private final AppOpsManager mAppOpsManager; + + private final LocationPermissionChecker mLocationPermissionChecker; + + private KeepaliveTracker mKeepaliveTracker; + private QosCallbackTracker mQosCallbackTracker; + private NetworkNotificationManager mNotifier; + private LingerMonitor mLingerMonitor; + + // sequence number of NetworkRequests + private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID; + + // Sequence number for NetworkProvider IDs. + private final AtomicInteger mNextNetworkProviderId = new AtomicInteger( + NetworkProvider.FIRST_PROVIDER_ID); + + // NetworkRequest activity String log entries. + private static final int MAX_NETWORK_REQUEST_LOGS = 20; + private final LocalLog mNetworkRequestInfoLogs = new LocalLog(MAX_NETWORK_REQUEST_LOGS); + + // NetworkInfo blocked and unblocked String log entries + private static final int MAX_NETWORK_INFO_LOGS = 40; + private final LocalLog mNetworkInfoBlockingLogs = new LocalLog(MAX_NETWORK_INFO_LOGS); + + private static final int MAX_WAKELOCK_LOGS = 20; + private final LocalLog mWakelockLogs = new LocalLog(MAX_WAKELOCK_LOGS); + private int mTotalWakelockAcquisitions = 0; + private int mTotalWakelockReleases = 0; + private long mTotalWakelockDurationMs = 0; + private long mMaxWakelockDurationMs = 0; + private long mLastWakeLockAcquireTimestamp = 0; + + private final IpConnectivityLog mMetricsLog; + + @GuardedBy("mBandwidthRequests") + private final SparseArray<Integer> mBandwidthRequests = new SparseArray(10); + + @VisibleForTesting + final MultinetworkPolicyTracker mMultinetworkPolicyTracker; + + @VisibleForTesting + final Map<IBinder, ConnectivityDiagnosticsCallbackInfo> mConnectivityDiagnosticsCallbacks = + new HashMap<>(); + + /** + * Implements support for the legacy "one network per network type" model. + * + * We used to have a static array of NetworkStateTrackers, one for each + * network type, but that doesn't work any more now that we can have, + * for example, more that one wifi network. This class stores all the + * NetworkAgentInfo objects that support a given type, but the legacy + * API will only see the first one. + * + * It serves two main purposes: + * + * 1. Provide information about "the network for a given type" (since this + * API only supports one). + * 2. Send legacy connectivity change broadcasts. Broadcasts are sent if + * the first network for a given type changes, or if the default network + * changes. + */ + @VisibleForTesting + static class LegacyTypeTracker { + + private static final boolean DBG = true; + private static final boolean VDBG = false; + + /** + * Array of lists, one per legacy network type (e.g., TYPE_MOBILE_MMS). + * Each list holds references to all NetworkAgentInfos that are used to + * satisfy requests for that network type. + * + * This array is built out at startup such that an unsupported network + * doesn't get an ArrayList instance, making this a tristate: + * unsupported, supported but not active and active. + * + * The actual lists are populated when we scan the network types that + * are supported on this device. + * + * Threading model: + * - addSupportedType() is only called in the constructor + * - add(), update(), remove() are only called from the ConnectivityService handler thread. + * They are therefore not thread-safe with respect to each other. + * - getNetworkForType() can be called at any time on binder threads. It is synchronized + * on mTypeLists to be thread-safe with respect to a concurrent remove call. + * - getRestoreTimerForType(type) is also synchronized on mTypeLists. + * - dump is thread-safe with respect to concurrent add and remove calls. + */ + private final ArrayList<NetworkAgentInfo> mTypeLists[]; + @NonNull + private final ConnectivityService mService; + + // Restore timers for requestNetworkForFeature (network type -> timer in ms). Types without + // an entry have no timer (equivalent to -1). Lazily loaded. + @NonNull + private ArrayMap<Integer, Integer> mRestoreTimers = new ArrayMap<>(); + + LegacyTypeTracker(@NonNull ConnectivityService service) { + mService = service; + mTypeLists = new ArrayList[ConnectivityManager.MAX_NETWORK_TYPE + 1]; + } + + public void loadSupportedTypes(@NonNull Context ctx, @NonNull TelephonyManager tm) { + final PackageManager pm = ctx.getPackageManager(); + if (pm.hasSystemFeature(FEATURE_WIFI)) { + addSupportedType(TYPE_WIFI); + } + if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) { + addSupportedType(TYPE_WIFI_P2P); + } + if (tm.isDataCapable()) { + // Telephony does not have granular support for these types: they are either all + // supported, or none is supported + addSupportedType(TYPE_MOBILE); + addSupportedType(TYPE_MOBILE_MMS); + addSupportedType(TYPE_MOBILE_SUPL); + addSupportedType(TYPE_MOBILE_DUN); + addSupportedType(TYPE_MOBILE_HIPRI); + addSupportedType(TYPE_MOBILE_FOTA); + addSupportedType(TYPE_MOBILE_IMS); + addSupportedType(TYPE_MOBILE_CBS); + addSupportedType(TYPE_MOBILE_IA); + addSupportedType(TYPE_MOBILE_EMERGENCY); + } + if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) { + addSupportedType(TYPE_BLUETOOTH); + } + if (pm.hasSystemFeature(FEATURE_WATCH)) { + // TYPE_PROXY is only used on Wear + addSupportedType(TYPE_PROXY); + } + // Ethernet is often not specified in the configs, although many devices can use it via + // USB host adapters. Add it as long as the ethernet service is here. + if (ctx.getSystemService(Context.ETHERNET_SERVICE) != null) { + addSupportedType(TYPE_ETHERNET); + } + + // Always add TYPE_VPN as a supported type + addSupportedType(TYPE_VPN); + } + + private void addSupportedType(int type) { + if (mTypeLists[type] != null) { + throw new IllegalStateException( + "legacy list for type " + type + "already initialized"); + } + mTypeLists[type] = new ArrayList<>(); + } + + public boolean isTypeSupported(int type) { + return isNetworkTypeValid(type) && mTypeLists[type] != null; + } + + public NetworkAgentInfo getNetworkForType(int type) { + synchronized (mTypeLists) { + if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) { + return mTypeLists[type].get(0); + } + } + return null; + } + + public int getRestoreTimerForType(int type) { + synchronized (mTypeLists) { + if (mRestoreTimers == null) { + mRestoreTimers = loadRestoreTimers(); + } + return mRestoreTimers.getOrDefault(type, -1); + } + } + + private ArrayMap<Integer, Integer> loadRestoreTimers() { + final String[] configs = mService.mResources.get().getStringArray( + R.array.config_legacy_networktype_restore_timers); + final ArrayMap<Integer, Integer> ret = new ArrayMap<>(configs.length); + for (final String config : configs) { + final String[] splits = TextUtils.split(config, ","); + if (splits.length != 2) { + logwtf("Invalid restore timer token count: " + config); + continue; + } + try { + ret.put(Integer.parseInt(splits[0]), Integer.parseInt(splits[1])); + } catch (NumberFormatException e) { + logwtf("Invalid restore timer number format: " + config, e); + } + } + return ret; + } + + private void maybeLogBroadcast(NetworkAgentInfo nai, DetailedState state, int type, + boolean isDefaultNetwork) { + if (DBG) { + log("Sending " + state + + " broadcast for type " + type + " " + nai.toShortString() + + " isDefaultNetwork=" + isDefaultNetwork); + } + } + + // When a lockdown VPN connects, send another CONNECTED broadcast for the underlying + // network type, to preserve previous behaviour. + private void maybeSendLegacyLockdownBroadcast(@NonNull NetworkAgentInfo vpnNai) { + if (vpnNai != mService.getLegacyLockdownNai()) return; + + if (vpnNai.declaredUnderlyingNetworks == null + || vpnNai.declaredUnderlyingNetworks.length != 1) { + Log.wtf(TAG, "Legacy lockdown VPN must have exactly one underlying network: " + + Arrays.toString(vpnNai.declaredUnderlyingNetworks)); + return; + } + final NetworkAgentInfo underlyingNai = mService.getNetworkAgentInfoForNetwork( + vpnNai.declaredUnderlyingNetworks[0]); + if (underlyingNai == null) return; + + final int type = underlyingNai.networkInfo.getType(); + final DetailedState state = DetailedState.CONNECTED; + maybeLogBroadcast(underlyingNai, state, type, true /* isDefaultNetwork */); + mService.sendLegacyNetworkBroadcast(underlyingNai, state, type); + } + + /** Adds the given network to the specified legacy type list. */ + public void add(int type, NetworkAgentInfo nai) { + if (!isTypeSupported(type)) { + return; // Invalid network type. + } + if (VDBG) log("Adding agent " + nai + " for legacy network type " + type); + + ArrayList<NetworkAgentInfo> list = mTypeLists[type]; + if (list.contains(nai)) { + return; + } + synchronized (mTypeLists) { + list.add(nai); + } + + // Send a broadcast if this is the first network of its type or if it's the default. + final boolean isDefaultNetwork = mService.isDefaultNetwork(nai); + + // If a legacy lockdown VPN is active, override the NetworkInfo state in all broadcasts + // to preserve previous behaviour. + final DetailedState state = mService.getLegacyLockdownState(DetailedState.CONNECTED); + if ((list.size() == 1) || isDefaultNetwork) { + maybeLogBroadcast(nai, state, type, isDefaultNetwork); + mService.sendLegacyNetworkBroadcast(nai, state, type); + } + + if (type == TYPE_VPN && state == DetailedState.CONNECTED) { + maybeSendLegacyLockdownBroadcast(nai); + } + } + + /** Removes the given network from the specified legacy type list. */ + public void remove(int type, NetworkAgentInfo nai, boolean wasDefault) { + ArrayList<NetworkAgentInfo> list = mTypeLists[type]; + if (list == null || list.isEmpty()) { + return; + } + final boolean wasFirstNetwork = list.get(0).equals(nai); + + synchronized (mTypeLists) { + if (!list.remove(nai)) { + return; + } + } + + if (wasFirstNetwork || wasDefault) { + maybeLogBroadcast(nai, DetailedState.DISCONNECTED, type, wasDefault); + mService.sendLegacyNetworkBroadcast(nai, DetailedState.DISCONNECTED, type); + } + + if (!list.isEmpty() && wasFirstNetwork) { + if (DBG) log("Other network available for type " + type + + ", sending connected broadcast"); + final NetworkAgentInfo replacement = list.get(0); + maybeLogBroadcast(replacement, DetailedState.CONNECTED, type, + mService.isDefaultNetwork(replacement)); + mService.sendLegacyNetworkBroadcast(replacement, DetailedState.CONNECTED, type); + } + } + + /** Removes the given network from all legacy type lists. */ + public void remove(NetworkAgentInfo nai, boolean wasDefault) { + if (VDBG) log("Removing agent " + nai + " wasDefault=" + wasDefault); + for (int type = 0; type < mTypeLists.length; type++) { + remove(type, nai, wasDefault); + } + } + + // send out another legacy broadcast - currently only used for suspend/unsuspend + // toggle + public void update(NetworkAgentInfo nai) { + final boolean isDefault = mService.isDefaultNetwork(nai); + final DetailedState state = nai.networkInfo.getDetailedState(); + for (int type = 0; type < mTypeLists.length; type++) { + final ArrayList<NetworkAgentInfo> list = mTypeLists[type]; + final boolean contains = (list != null && list.contains(nai)); + final boolean isFirst = contains && (nai == list.get(0)); + if (isFirst || contains && isDefault) { + maybeLogBroadcast(nai, state, type, isDefault); + mService.sendLegacyNetworkBroadcast(nai, state, type); + } + } + } + + public void dump(IndentingPrintWriter pw) { + pw.println("mLegacyTypeTracker:"); + pw.increaseIndent(); + pw.print("Supported types:"); + for (int type = 0; type < mTypeLists.length; type++) { + if (mTypeLists[type] != null) pw.print(" " + type); + } + pw.println(); + pw.println("Current state:"); + pw.increaseIndent(); + synchronized (mTypeLists) { + for (int type = 0; type < mTypeLists.length; type++) { + if (mTypeLists[type] == null || mTypeLists[type].isEmpty()) continue; + for (NetworkAgentInfo nai : mTypeLists[type]) { + pw.println(type + " " + nai.toShortString()); + } + } + } + pw.decreaseIndent(); + pw.decreaseIndent(); + pw.println(); + } + } + private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this); + + final LocalPriorityDump mPriorityDumper = new LocalPriorityDump(); + /** + * Helper class which parses out priority arguments and dumps sections according to their + * priority. If priority arguments are omitted, function calls the legacy dump command. + */ + private class LocalPriorityDump { + private static final String PRIORITY_ARG = "--dump-priority"; + private static final String PRIORITY_ARG_HIGH = "HIGH"; + private static final String PRIORITY_ARG_NORMAL = "NORMAL"; + + LocalPriorityDump() {} + + private void dumpHigh(FileDescriptor fd, PrintWriter pw) { + doDump(fd, pw, new String[] {DIAG_ARG}); + doDump(fd, pw, new String[] {SHORT_ARG}); + } + + private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) { + doDump(fd, pw, args); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (args == null) { + dumpNormal(fd, pw, args); + return; + } + + String priority = null; + for (int argIndex = 0; argIndex < args.length; argIndex++) { + if (args[argIndex].equals(PRIORITY_ARG) && argIndex + 1 < args.length) { + argIndex++; + priority = args[argIndex]; + } + } + + if (PRIORITY_ARG_HIGH.equals(priority)) { + dumpHigh(fd, pw); + } else if (PRIORITY_ARG_NORMAL.equals(priority)) { + dumpNormal(fd, pw, args); + } else { + // ConnectivityService publishes binder service using publishBinderService() with + // no priority assigned will be treated as NORMAL priority. Dumpsys does not send + // "--dump-priority" arguments to the service. Thus, dump NORMAL only to align the + // legacy output for dumpsys connectivity. + // TODO: Integrate into signal dump. + dumpNormal(fd, pw, args); + } + } + } + + /** + * Keeps track of the number of requests made under different uids. + */ + public static class PerUidCounter { + private final int mMaxCountPerUid; + + // Map from UID to number of NetworkRequests that UID has filed. + @VisibleForTesting + @GuardedBy("mUidToNetworkRequestCount") + final SparseIntArray mUidToNetworkRequestCount = new SparseIntArray(); + + /** + * Constructor + * + * @param maxCountPerUid the maximum count per uid allowed + */ + public PerUidCounter(final int maxCountPerUid) { + mMaxCountPerUid = maxCountPerUid; + } + + /** + * Increments the request count of the given uid. Throws an exception if the number + * of open requests for the uid exceeds the value of maxCounterPerUid which is the value + * passed into the constructor. see: {@link #PerUidCounter(int)}. + * + * @throws ServiceSpecificException with + * {@link ConnectivityManager.Errors.TOO_MANY_REQUESTS} if the number of requests for + * the uid exceed the allowed number. + * + * @param uid the uid that the request was made under + */ + public void incrementCountOrThrow(final int uid) { + synchronized (mUidToNetworkRequestCount) { + incrementCountOrThrow(uid, 1 /* numToIncrement */); + } + } + + private void incrementCountOrThrow(final int uid, final int numToIncrement) { + final int newRequestCount = + mUidToNetworkRequestCount.get(uid, 0) + numToIncrement; + if (newRequestCount >= mMaxCountPerUid) { + throw new ServiceSpecificException( + ConnectivityManager.Errors.TOO_MANY_REQUESTS); + } + mUidToNetworkRequestCount.put(uid, newRequestCount); + } + + /** + * Decrements the request count of the given uid. + * + * @param uid the uid that the request was made under + */ + public void decrementCount(final int uid) { + synchronized (mUidToNetworkRequestCount) { + decrementCount(uid, 1 /* numToDecrement */); + } + } + + private void decrementCount(final int uid, final int numToDecrement) { + final int newRequestCount = + mUidToNetworkRequestCount.get(uid, 0) - numToDecrement; + if (newRequestCount < 0) { + logwtf("BUG: too small request count " + newRequestCount + " for UID " + uid); + } else if (newRequestCount == 0) { + mUidToNetworkRequestCount.delete(uid); + } else { + mUidToNetworkRequestCount.put(uid, newRequestCount); + } + } + + /** + * Used to adjust the request counter for the per-app API flows. Directly adjusting the + * counter is not ideal however in the per-app flows, the nris can't be removed until they + * are used to create the new nris upon set. Therefore the request count limit can be + * artificially hit. This method is used as a workaround for this particular case so that + * the request counts are accounted for correctly. + * @param uid the uid to adjust counts for + * @param numOfNewRequests the new request count to account for + * @param r the runnable to execute + */ + public void transact(final int uid, final int numOfNewRequests, @NonNull final Runnable r) { + // This should only be used on the handler thread as per all current and foreseen + // use-cases. ensureRunningOnConnectivityServiceThread() can't be used because there is + // no ref to the outer ConnectivityService. + synchronized (mUidToNetworkRequestCount) { + final int reqCountOverage = getCallingUidRequestCountOverage(uid, numOfNewRequests); + decrementCount(uid, reqCountOverage); + r.run(); + incrementCountOrThrow(uid, reqCountOverage); + } + } + + private int getCallingUidRequestCountOverage(final int uid, final int numOfNewRequests) { + final int newUidRequestCount = mUidToNetworkRequestCount.get(uid, 0) + + numOfNewRequests; + return newUidRequestCount >= MAX_NETWORK_REQUESTS_PER_SYSTEM_UID + ? newUidRequestCount - (MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1) : 0; + } + } + + /** + * Dependencies of ConnectivityService, for injection in tests. + */ + @VisibleForTesting + public static class Dependencies { + public int getCallingUid() { + return Binder.getCallingUid(); + } + + /** + * Get system properties to use in ConnectivityService. + */ + public MockableSystemProperties getSystemProperties() { + return new MockableSystemProperties(); + } + + /** + * Get the {@link ConnectivityResources} to use in ConnectivityService. + */ + public ConnectivityResources getResources(@NonNull Context ctx) { + return new ConnectivityResources(ctx); + } + + /** + * Create a HandlerThread to use in ConnectivityService. + */ + public HandlerThread makeHandlerThread() { + return new HandlerThread("ConnectivityServiceThread"); + } + + /** + * Get a reference to the ModuleNetworkStackClient. + */ + public NetworkStackClientBase getNetworkStack() { + return ModuleNetworkStackClient.getInstance(null); + } + + /** + * @see ProxyTracker + */ + public ProxyTracker makeProxyTracker(@NonNull Context context, + @NonNull Handler connServiceHandler) { + return new ProxyTracker(context, connServiceHandler, EVENT_PROXY_HAS_CHANGED); + } + + /** + * @see NetIdManager + */ + public NetIdManager makeNetIdManager() { + return new NetIdManager(); + } + + /** + * @see NetworkUtils#queryUserAccess(int, int) + */ + public boolean queryUserAccess(int uid, Network network, ConnectivityService cs) { + return cs.queryUserAccess(uid, network); + } + + /** + * Gets the UID that owns a socket connection. Needed because opening SOCK_DIAG sockets + * requires CAP_NET_ADMIN, which the unit tests do not have. + */ + public int getConnectionOwnerUid(int protocol, InetSocketAddress local, + InetSocketAddress remote) { + return InetDiagMessage.getConnectionOwnerUid(protocol, local, remote); + } + + /** + * @see MultinetworkPolicyTracker + */ + public MultinetworkPolicyTracker makeMultinetworkPolicyTracker( + @NonNull Context c, @NonNull Handler h, @NonNull Runnable r) { + return new MultinetworkPolicyTracker(c, h, r); + } + + /** + * @see BatteryStatsManager + */ + public void reportNetworkInterfaceForTransports(Context context, String iface, + int[] transportTypes) { + final BatteryStatsManager batteryStats = + context.getSystemService(BatteryStatsManager.class); + batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes); + } + + public boolean getCellular464XlatEnabled() { + return NetworkProperties.isCellular464XlatEnabled().orElse(true); + } + } + + public ConnectivityService(Context context) { + this(context, getDnsResolver(context), new IpConnectivityLog(), + INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)), + new Dependencies()); + } + + @VisibleForTesting + protected ConnectivityService(Context context, IDnsResolver dnsresolver, + IpConnectivityLog logger, INetd netd, Dependencies deps) { + if (DBG) log("ConnectivityService starting up"); + + mDeps = Objects.requireNonNull(deps, "missing Dependencies"); + mSystemProperties = mDeps.getSystemProperties(); + mNetIdManager = mDeps.makeNetIdManager(); + mContext = Objects.requireNonNull(context, "missing Context"); + mResources = deps.getResources(mContext); + mNetworkRequestCounter = new PerUidCounter(MAX_NETWORK_REQUESTS_PER_UID); + mSystemNetworkRequestCounter = new PerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID); + + mMetricsLog = logger; + mNetworkRanker = new NetworkRanker(); + final NetworkRequest defaultInternetRequest = createDefaultRequest(); + mDefaultRequest = new NetworkRequestInfo( + Process.myUid(), defaultInternetRequest, null, + new Binder(), NetworkCallback.FLAG_INCLUDE_LOCATION_INFO, + null /* attributionTags */); + mNetworkRequests.put(defaultInternetRequest, mDefaultRequest); + mDefaultNetworkRequests.add(mDefaultRequest); + mNetworkRequestInfoLogs.log("REGISTER " + mDefaultRequest); + + mDefaultMobileDataRequest = createDefaultInternetRequestForTransport( + NetworkCapabilities.TRANSPORT_CELLULAR, NetworkRequest.Type.BACKGROUND_REQUEST); + + // The default WiFi request is a background request so that apps using WiFi are + // migrated to a better network (typically ethernet) when one comes up, instead + // of staying on WiFi forever. + mDefaultWifiRequest = createDefaultInternetRequestForTransport( + NetworkCapabilities.TRANSPORT_WIFI, NetworkRequest.Type.BACKGROUND_REQUEST); + + mDefaultVehicleRequest = createAlwaysOnRequestForCapability( + NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL, + NetworkRequest.Type.BACKGROUND_REQUEST); + + mHandlerThread = mDeps.makeHandlerThread(); + mHandlerThread.start(); + mHandler = new InternalHandler(mHandlerThread.getLooper()); + mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper()); + mConnectivityDiagnosticsHandler = + new ConnectivityDiagnosticsHandler(mHandlerThread.getLooper()); + + mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(), + ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000); + + mLingerDelayMs = mSystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS); + // TODO: Consider making the timer customizable. + mNascentDelayMs = DEFAULT_NASCENT_DELAY_MS; + + mStatsManager = mContext.getSystemService(NetworkStatsManager.class); + mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); + mDnsResolver = Objects.requireNonNull(dnsresolver, "missing IDnsResolver"); + mProxyTracker = mDeps.makeProxyTracker(mContext, mHandler); + + mNetd = netd; + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); + mLocationPermissionChecker = new LocationPermissionChecker(mContext); + + // To ensure uid state is synchronized with Network Policy, register for + // NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService + // reading existing policy from disk. + mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback); + + final PowerManager powerManager = (PowerManager) context.getSystemService( + Context.POWER_SERVICE); + mNetTransitionWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mPendingIntentWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + + mLegacyTypeTracker.loadSupportedTypes(mContext, mTelephonyManager); + mProtectedNetworks = new ArrayList<>(); + int[] protectedNetworks = mResources.get().getIntArray(R.array.config_protectedNetworks); + for (int p : protectedNetworks) { + if (mLegacyTypeTracker.isTypeSupported(p) && !mProtectedNetworks.contains(p)) { + mProtectedNetworks.add(p); + } else { + if (DBG) loge("Ignoring protectedNetwork " + p); + } + } + + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + + mPermissionMonitor = new PermissionMonitor(mContext, mNetd); + + mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); + // Listen for user add/removes to inform PermissionMonitor. + // Should run on mHandler to avoid any races. + final IntentFilter userIntentFilter = new IntentFilter(); + userIntentFilter.addAction(Intent.ACTION_USER_ADDED); + userIntentFilter.addAction(Intent.ACTION_USER_REMOVED); + mUserAllContext.registerReceiver(mUserIntentReceiver, userIntentFilter, + null /* broadcastPermission */, mHandler); + + // Listen to package add/removes for netd + final IntentFilter packageIntentFilter = new IntentFilter(); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + packageIntentFilter.addDataScheme("package"); + mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter, + null /* broadcastPermission */, mHandler); + + mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mHandler, mNetd); + + mNetdCallback = new NetdCallback(); + try { + mNetd.registerUnsolicitedEventListener(mNetdCallback); + } catch (RemoteException | ServiceSpecificException e) { + loge("Error registering event listener :" + e); + } + + mSettingsObserver = new SettingsObserver(mContext, mHandler); + registerSettingsCallbacks(); + + mKeepaliveTracker = new KeepaliveTracker(mContext, mHandler); + mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager); + mQosCallbackTracker = new QosCallbackTracker(mHandler, mNetworkRequestCounter); + + final int dailyLimit = Settings.Global.getInt(mContext.getContentResolver(), + ConnectivitySettingsManager.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT, + LingerMonitor.DEFAULT_NOTIFICATION_DAILY_LIMIT); + final long rateLimit = Settings.Global.getLong(mContext.getContentResolver(), + ConnectivitySettingsManager.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS, + LingerMonitor.DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS); + mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit); + + mMultinetworkPolicyTracker = mDeps.makeMultinetworkPolicyTracker( + mContext, mHandler, () -> updateAvoidBadWifi()); + mMultinetworkPolicyTracker.start(); + + mDnsManager = new DnsManager(mContext, mDnsResolver); + registerPrivateDnsSettingsCallbacks(); + + // This NAI is a sentinel used to offer no service to apps that are on a multi-layer + // request that doesn't allow fallback to the default network. It should never be visible + // to apps. As such, it's not in the list of NAIs and doesn't need many of the normal + // arguments like the handler or the DnsResolver. + // TODO : remove this ; it is probably better handled with a sentinel request. + mNoServiceNetwork = new NetworkAgentInfo(null, + new Network(INetd.UNREACHABLE_NET_ID), + new NetworkInfo(TYPE_NONE, 0, "", ""), + new LinkProperties(), new NetworkCapabilities(), + new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null, + new NetworkAgentConfig(), this, null, null, 0, INVALID_UID, + mLingerDelayMs, mQosCallbackTracker, mDeps); + } + + private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) { + return createDefaultNetworkCapabilitiesForUidRange(new UidRange(uid, uid)); + } + + private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRange( + @NonNull final UidRange uids) { + final NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + netCap.removeCapability(NET_CAPABILITY_NOT_VPN); + netCap.setUids(UidRange.toIntRanges(Collections.singleton(uids))); + return netCap; + } + + private NetworkRequest createDefaultRequest() { + return createDefaultInternetRequestForTransport( + TYPE_NONE, NetworkRequest.Type.REQUEST); + } + + private NetworkRequest createDefaultInternetRequestForTransport( + int transportType, NetworkRequest.Type type) { + final NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName()); + if (transportType > TYPE_NONE) { + netCap.addTransportType(transportType); + } + return createNetworkRequest(type, netCap); + } + + private NetworkRequest createNetworkRequest( + NetworkRequest.Type type, NetworkCapabilities netCap) { + return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type); + } + + private NetworkRequest createAlwaysOnRequestForCapability(int capability, + NetworkRequest.Type type) { + final NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.clearAll(); + netCap.addCapability(capability); + netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName()); + return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type); + } + + // Used only for testing. + // TODO: Delete this and either: + // 1. Give FakeSettingsProvider the ability to send settings change notifications (requires + // changing ContentResolver to make registerContentObserver non-final). + // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it + // by subclassing SettingsObserver. + @VisibleForTesting + void updateAlwaysOnNetworks() { + mHandler.sendEmptyMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS); + } + + // See FakeSettingsProvider comment above. + @VisibleForTesting + void updatePrivateDnsSettings() { + mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED); + } + + private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, int id) { + final boolean enable = mContext.getResources().getBoolean(id); + handleAlwaysOnNetworkRequest(networkRequest, enable); + } + + private void handleAlwaysOnNetworkRequest( + NetworkRequest networkRequest, String settingName, boolean defaultValue) { + final boolean enable = toBool(Settings.Global.getInt( + mContext.getContentResolver(), settingName, encodeBool(defaultValue))); + handleAlwaysOnNetworkRequest(networkRequest, enable); + } + + private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, boolean enable) { + final boolean isEnabled = (mNetworkRequests.get(networkRequest) != null); + if (enable == isEnabled) { + return; // Nothing to do. + } + + if (enable) { + handleRegisterNetworkRequest(new NetworkRequestInfo( + Process.myUid(), networkRequest, null, new Binder(), + NetworkCallback.FLAG_INCLUDE_LOCATION_INFO, + null /* attributionTags */)); + } else { + handleReleaseNetworkRequest(networkRequest, Process.SYSTEM_UID, + /* callOnUnavailable */ false); + } + } + + private void handleConfigureAlwaysOnNetworks() { + handleAlwaysOnNetworkRequest(mDefaultMobileDataRequest, + ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON, true /* defaultValue */); + handleAlwaysOnNetworkRequest(mDefaultWifiRequest, + ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED, false /* defaultValue */); + final boolean vehicleAlwaysRequested = mResources.get().getBoolean( + R.bool.config_vehicleInternalNetworkAlwaysRequested); + handleAlwaysOnNetworkRequest(mDefaultVehicleRequest, vehicleAlwaysRequested); + } + + private void registerSettingsCallbacks() { + // Watch for global HTTP proxy changes. + mSettingsObserver.observe( + Settings.Global.getUriFor(Settings.Global.HTTP_PROXY), + EVENT_APPLY_GLOBAL_HTTP_PROXY); + + // Watch for whether or not to keep mobile data always on. + mSettingsObserver.observe( + Settings.Global.getUriFor(ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON), + EVENT_CONFIGURE_ALWAYS_ON_NETWORKS); + + // Watch for whether or not to keep wifi always on. + mSettingsObserver.observe( + Settings.Global.getUriFor(ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED), + EVENT_CONFIGURE_ALWAYS_ON_NETWORKS); + } + + private void registerPrivateDnsSettingsCallbacks() { + for (Uri uri : DnsManager.getPrivateDnsSettingsUris()) { + mSettingsObserver.observe(uri, EVENT_PRIVATE_DNS_SETTINGS_CHANGED); + } + } + + private synchronized int nextNetworkRequestId() { + // TODO: Consider handle wrapping and exclude {@link NetworkRequest#REQUEST_ID_NONE} if + // doing that. + return mNextNetworkRequestId++; + } + + @VisibleForTesting + protected NetworkAgentInfo getNetworkAgentInfoForNetwork(Network network) { + if (network == null) { + return null; + } + return getNetworkAgentInfoForNetId(network.getNetId()); + } + + private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) { + synchronized (mNetworkForNetId) { + return mNetworkForNetId.get(netId); + } + } + + // TODO: determine what to do when more than one VPN applies to |uid|. + private NetworkAgentInfo getVpnForUid(int uid) { + synchronized (mNetworkForNetId) { + for (int i = 0; i < mNetworkForNetId.size(); i++) { + final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i); + if (nai.isVPN() && nai.everConnected && nai.networkCapabilities.appliesToUid(uid)) { + return nai; + } + } + } + return null; + } + + private Network[] getVpnUnderlyingNetworks(int uid) { + if (mLockdownEnabled) return null; + final NetworkAgentInfo nai = getVpnForUid(uid); + if (nai != null) return nai.declaredUnderlyingNetworks; + return null; + } + + private NetworkAgentInfo getNetworkAgentInfoForUid(int uid) { + NetworkAgentInfo nai = getDefaultNetworkForUid(uid); + + final Network[] networks = getVpnUnderlyingNetworks(uid); + if (networks != null) { + // getUnderlyingNetworks() returns: + // null => there was no VPN, or the VPN didn't specify anything, so we use the default. + // empty array => the VPN explicitly said "no default network". + // non-empty array => the VPN specified one or more default networks; we use the + // first one. + if (networks.length > 0) { + nai = getNetworkAgentInfoForNetwork(networks[0]); + } else { + nai = null; + } + } + return nai; + } + + /** + * Check if UID should be blocked from using the specified network. + */ + private boolean isNetworkWithCapabilitiesBlocked(@Nullable final NetworkCapabilities nc, + final int uid, final boolean ignoreBlocked) { + // Networks aren't blocked when ignoring blocked status + if (ignoreBlocked) { + return false; + } + if (isUidBlockedByVpn(uid, mVpnBlockedUidRanges)) return true; + final long ident = Binder.clearCallingIdentity(); + try { + final boolean metered = nc == null ? true : nc.isMetered(); + return mPolicyManager.isUidNetworkingBlocked(uid, metered); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void maybeLogBlockedNetworkInfo(NetworkInfo ni, int uid) { + if (ni == null || !LOGD_BLOCKED_NETWORKINFO) { + return; + } + final boolean blocked; + synchronized (mBlockedAppUids) { + if (ni.getDetailedState() == DetailedState.BLOCKED && mBlockedAppUids.add(uid)) { + blocked = true; + } else if (ni.isConnected() && mBlockedAppUids.remove(uid)) { + blocked = false; + } else { + return; + } + } + String action = blocked ? "BLOCKED" : "UNBLOCKED"; + log(String.format("Returning %s NetworkInfo to uid=%d", action, uid)); + mNetworkInfoBlockingLogs.log(action + " " + uid); + } + + private void maybeLogBlockedStatusChanged(NetworkRequestInfo nri, Network net, int blocked) { + if (nri == null || net == null || !LOGD_BLOCKED_NETWORKINFO) { + return; + } + final String action = (blocked != 0) ? "BLOCKED" : "UNBLOCKED"; + final int requestId = nri.getActiveRequest() != null + ? nri.getActiveRequest().requestId : nri.mRequests.get(0).requestId; + mNetworkInfoBlockingLogs.log(String.format( + "%s %d(%d) on netId %d: %s", action, nri.mAsUid, requestId, net.getNetId(), + Integer.toHexString(blocked))); + } + + /** + * Apply any relevant filters to the specified {@link NetworkInfo} for the given UID. For + * example, this may mark the network as {@link DetailedState#BLOCKED} based + * on {@link #isNetworkWithCapabilitiesBlocked}. + */ + @NonNull + private NetworkInfo filterNetworkInfo(@NonNull NetworkInfo networkInfo, int type, + @NonNull NetworkCapabilities nc, int uid, boolean ignoreBlocked) { + final NetworkInfo filtered = new NetworkInfo(networkInfo); + // Many legacy types (e.g,. TYPE_MOBILE_HIPRI) are not actually a property of the network + // but only exists if an app asks about them or requests them. Ensure the requesting app + // gets the type it asks for. + filtered.setType(type); + if (isNetworkWithCapabilitiesBlocked(nc, uid, ignoreBlocked)) { + filtered.setDetailedState(DetailedState.BLOCKED, null /* reason */, + null /* extraInfo */); + } + filterForLegacyLockdown(filtered); + return filtered; + } + + private NetworkInfo getFilteredNetworkInfo(NetworkAgentInfo nai, int uid, + boolean ignoreBlocked) { + return filterNetworkInfo(nai.networkInfo, nai.networkInfo.getType(), + nai.networkCapabilities, uid, ignoreBlocked); + } + + /** + * Return NetworkInfo for the active (i.e., connected) network interface. + * It is assumed that at most one network is active at a time. If more + * than one is active, it is indeterminate which will be returned. + * @return the info for the active network, or {@code null} if none is + * active + */ + @Override + public NetworkInfo getActiveNetworkInfo() { + enforceAccessPermission(); + final int uid = mDeps.getCallingUid(); + final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid); + if (nai == null) return null; + final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false); + maybeLogBlockedNetworkInfo(networkInfo, uid); + return networkInfo; + } + + @Override + public Network getActiveNetwork() { + enforceAccessPermission(); + return getActiveNetworkForUidInternal(mDeps.getCallingUid(), false); + } + + @Override + public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) { + PermissionUtils.enforceNetworkStackPermission(mContext); + return getActiveNetworkForUidInternal(uid, ignoreBlocked); + } + + private Network getActiveNetworkForUidInternal(final int uid, boolean ignoreBlocked) { + final NetworkAgentInfo vpnNai = getVpnForUid(uid); + if (vpnNai != null) { + final NetworkCapabilities requiredCaps = createDefaultNetworkCapabilitiesForUid(uid); + if (requiredCaps.satisfiedByNetworkCapabilities(vpnNai.networkCapabilities)) { + return vpnNai.network; + } + } + + NetworkAgentInfo nai = getDefaultNetworkForUid(uid); + if (nai == null || isNetworkWithCapabilitiesBlocked(nai.networkCapabilities, uid, + ignoreBlocked)) { + return null; + } + return nai.network; + } + + @Override + public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) { + PermissionUtils.enforceNetworkStackPermission(mContext); + final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid); + if (nai == null) return null; + return getFilteredNetworkInfo(nai, uid, ignoreBlocked); + } + + /** Returns a NetworkInfo object for a network that doesn't exist. */ + private NetworkInfo makeFakeNetworkInfo(int networkType, int uid) { + final NetworkInfo info = new NetworkInfo(networkType, 0 /* subtype */, + getNetworkTypeName(networkType), "" /* subtypeName */); + info.setIsAvailable(true); + // For compatibility with legacy code, return BLOCKED instead of DISCONNECTED when + // background data is restricted. + final NetworkCapabilities nc = new NetworkCapabilities(); // Metered. + final DetailedState state = isNetworkWithCapabilitiesBlocked(nc, uid, false) + ? DetailedState.BLOCKED + : DetailedState.DISCONNECTED; + info.setDetailedState(state, null /* reason */, null /* extraInfo */); + filterForLegacyLockdown(info); + return info; + } + + private NetworkInfo getFilteredNetworkInfoForType(int networkType, int uid) { + if (!mLegacyTypeTracker.isTypeSupported(networkType)) { + return null; + } + final NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); + if (nai == null) { + return makeFakeNetworkInfo(networkType, uid); + } + return filterNetworkInfo(nai.networkInfo, networkType, nai.networkCapabilities, uid, + false); + } + + @Override + public NetworkInfo getNetworkInfo(int networkType) { + enforceAccessPermission(); + final int uid = mDeps.getCallingUid(); + if (getVpnUnderlyingNetworks(uid) != null) { + // A VPN is active, so we may need to return one of its underlying networks. This + // information is not available in LegacyTypeTracker, so we have to get it from + // getNetworkAgentInfoForUid. + final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid); + if (nai == null) return null; + final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false); + if (networkInfo.getType() == networkType) { + return networkInfo; + } + } + return getFilteredNetworkInfoForType(networkType, uid); + } + + @Override + public NetworkInfo getNetworkInfoForUid(Network network, int uid, boolean ignoreBlocked) { + enforceAccessPermission(); + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) return null; + return getFilteredNetworkInfo(nai, uid, ignoreBlocked); + } + + @Override + public NetworkInfo[] getAllNetworkInfo() { + enforceAccessPermission(); + final ArrayList<NetworkInfo> result = new ArrayList<>(); + for (int networkType = 0; networkType <= ConnectivityManager.MAX_NETWORK_TYPE; + networkType++) { + NetworkInfo info = getNetworkInfo(networkType); + if (info != null) { + result.add(info); + } + } + return result.toArray(new NetworkInfo[result.size()]); + } + + @Override + public Network getNetworkForType(int networkType) { + enforceAccessPermission(); + if (!mLegacyTypeTracker.isTypeSupported(networkType)) { + return null; + } + final NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); + if (nai == null) { + return null; + } + final int uid = mDeps.getCallingUid(); + if (isNetworkWithCapabilitiesBlocked(nai.networkCapabilities, uid, false)) { + return null; + } + return nai.network; + } + + @Override + public Network[] getAllNetworks() { + enforceAccessPermission(); + synchronized (mNetworkForNetId) { + final Network[] result = new Network[mNetworkForNetId.size()]; + for (int i = 0; i < mNetworkForNetId.size(); i++) { + result[i] = mNetworkForNetId.valueAt(i).network; + } + return result; + } + } + + @Override + public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser( + int userId, String callingPackageName, @Nullable String callingAttributionTag) { + // The basic principle is: if an app's traffic could possibly go over a + // network, without the app doing anything multinetwork-specific, + // (hence, by "default"), then include that network's capabilities in + // the array. + // + // In the normal case, app traffic only goes over the system's default + // network connection, so that's the only network returned. + // + // With a VPN in force, some app traffic may go into the VPN, and thus + // over whatever underlying networks the VPN specifies, while other app + // traffic may go over the system default network (e.g.: a split-tunnel + // VPN, or an app disallowed by the VPN), so the set of networks + // returned includes the VPN's underlying networks and the system + // default. + enforceAccessPermission(); + + HashMap<Network, NetworkCapabilities> result = new HashMap<>(); + + for (final NetworkRequestInfo nri : mDefaultNetworkRequests) { + if (!nri.isBeingSatisfied()) { + continue; + } + final NetworkAgentInfo nai = nri.getSatisfier(); + final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai); + if (null != nc + && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) + && !result.containsKey(nai.network)) { + result.put( + nai.network, + createWithLocationInfoSanitizedIfNecessaryWhenParceled( + nc, false /* includeLocationSensitiveInfo */, + getCallingPid(), mDeps.getCallingUid(), callingPackageName, + callingAttributionTag)); + } + } + + // No need to check mLockdownEnabled. If it's true, getVpnUnderlyingNetworks returns null. + final Network[] networks = getVpnUnderlyingNetworks(mDeps.getCallingUid()); + if (null != networks) { + for (final Network network : networks) { + final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network); + if (null != nc) { + result.put( + network, + createWithLocationInfoSanitizedIfNecessaryWhenParceled( + nc, + false /* includeLocationSensitiveInfo */, + getCallingPid(), mDeps.getCallingUid(), callingPackageName, + callingAttributionTag)); + } + } + } + + NetworkCapabilities[] out = new NetworkCapabilities[result.size()]; + out = result.values().toArray(out); + return out; + } + + @Override + public boolean isNetworkSupported(int networkType) { + enforceAccessPermission(); + return mLegacyTypeTracker.isTypeSupported(networkType); + } + + /** + * Return LinkProperties for the active (i.e., connected) default + * network interface for the calling uid. + * @return the ip properties for the active network, or {@code null} if + * none is active + */ + @Override + public LinkProperties getActiveLinkProperties() { + enforceAccessPermission(); + final int uid = mDeps.getCallingUid(); + NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid); + if (nai == null) return null; + return linkPropertiesRestrictedForCallerPermissions(nai.linkProperties, + Binder.getCallingPid(), uid); + } + + @Override + public LinkProperties getLinkPropertiesForType(int networkType) { + enforceAccessPermission(); + NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); + final LinkProperties lp = getLinkProperties(nai); + if (lp == null) return null; + return linkPropertiesRestrictedForCallerPermissions( + lp, Binder.getCallingPid(), mDeps.getCallingUid()); + } + + // TODO - this should be ALL networks + @Override + public LinkProperties getLinkProperties(Network network) { + enforceAccessPermission(); + final LinkProperties lp = getLinkProperties(getNetworkAgentInfoForNetwork(network)); + if (lp == null) return null; + return linkPropertiesRestrictedForCallerPermissions( + lp, Binder.getCallingPid(), mDeps.getCallingUid()); + } + + @Nullable + private LinkProperties getLinkProperties(@Nullable NetworkAgentInfo nai) { + if (nai == null) { + return null; + } + synchronized (nai) { + return nai.linkProperties; + } + } + + private NetworkCapabilities getNetworkCapabilitiesInternal(Network network) { + return getNetworkCapabilitiesInternal(getNetworkAgentInfoForNetwork(network)); + } + + private NetworkCapabilities getNetworkCapabilitiesInternal(NetworkAgentInfo nai) { + if (nai == null) return null; + synchronized (nai) { + return networkCapabilitiesRestrictedForCallerPermissions( + nai.networkCapabilities, Binder.getCallingPid(), mDeps.getCallingUid()); + } + } + + @Override + public NetworkCapabilities getNetworkCapabilities(Network network, String callingPackageName, + @Nullable String callingAttributionTag) { + mAppOpsManager.checkPackage(mDeps.getCallingUid(), callingPackageName); + enforceAccessPermission(); + return createWithLocationInfoSanitizedIfNecessaryWhenParceled( + getNetworkCapabilitiesInternal(network), + false /* includeLocationSensitiveInfo */, + getCallingPid(), mDeps.getCallingUid(), callingPackageName, callingAttributionTag); + } + + @VisibleForTesting + NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions( + NetworkCapabilities nc, int callerPid, int callerUid) { + final NetworkCapabilities newNc = new NetworkCapabilities(nc); + if (!checkSettingsPermission(callerPid, callerUid)) { + newNc.setUids(null); + newNc.setSSID(null); + } + if (newNc.getNetworkSpecifier() != null) { + newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact()); + } + newNc.setAdministratorUids(new int[0]); + if (!checkAnyPermissionOf( + callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) { + newNc.setSubscriptionIds(Collections.emptySet()); + } + + return newNc; + } + + /** + * Wrapper used to cache the permission check results performed for the corresponding + * app. This avoid performing multiple permission checks for different fields in + * NetworkCapabilities. + * Note: This wrapper does not support any sort of invalidation and thus must not be + * persistent or long-lived. It may only be used for the time necessary to + * compute the redactions required by one particular NetworkCallback or + * synchronous call. + */ + private class RedactionPermissionChecker { + private final int mCallingPid; + private final int mCallingUid; + @NonNull private final String mCallingPackageName; + @Nullable private final String mCallingAttributionTag; + + private Boolean mHasLocationPermission = null; + private Boolean mHasLocalMacAddressPermission = null; + private Boolean mHasSettingsPermission = null; + + RedactionPermissionChecker(int callingPid, int callingUid, + @NonNull String callingPackageName, @Nullable String callingAttributionTag) { + mCallingPid = callingPid; + mCallingUid = callingUid; + mCallingPackageName = callingPackageName; + mCallingAttributionTag = callingAttributionTag; + } + + private boolean hasLocationPermissionInternal() { + final long token = Binder.clearCallingIdentity(); + try { + return mLocationPermissionChecker.checkLocationPermission( + mCallingPackageName, mCallingAttributionTag, mCallingUid, + null /* message */); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Returns whether the app holds location permission or not (might return cached result + * if the permission was already checked before). + */ + public boolean hasLocationPermission() { + if (mHasLocationPermission == null) { + // If there is no cached result, perform the check now. + mHasLocationPermission = hasLocationPermissionInternal(); + } + return mHasLocationPermission; + } + + /** + * Returns whether the app holds local mac address permission or not (might return cached + * result if the permission was already checked before). + */ + public boolean hasLocalMacAddressPermission() { + if (mHasLocalMacAddressPermission == null) { + // If there is no cached result, perform the check now. + mHasLocalMacAddressPermission = + checkLocalMacAddressPermission(mCallingPid, mCallingUid); + } + return mHasLocalMacAddressPermission; + } + + /** + * Returns whether the app holds settings permission or not (might return cached + * result if the permission was already checked before). + */ + public boolean hasSettingsPermission() { + if (mHasSettingsPermission == null) { + // If there is no cached result, perform the check now. + mHasSettingsPermission = checkSettingsPermission(mCallingPid, mCallingUid); + } + return mHasSettingsPermission; + } + } + + private static boolean shouldRedact(@NetworkCapabilities.RedactionType long redactions, + @NetworkCapabilities.NetCapability long redaction) { + return (redactions & redaction) != 0; + } + + /** + * Use the provided |applicableRedactions| to check the receiving app's + * permissions and clear/set the corresponding bit in the returned bitmask. The bitmask + * returned will be used to ensure the necessary redactions are performed by NetworkCapabilities + * before being sent to the corresponding app. + */ + private @NetworkCapabilities.RedactionType long retrieveRequiredRedactions( + @NetworkCapabilities.RedactionType long applicableRedactions, + @NonNull RedactionPermissionChecker redactionPermissionChecker, + boolean includeLocationSensitiveInfo) { + long redactions = applicableRedactions; + if (shouldRedact(redactions, REDACT_FOR_ACCESS_FINE_LOCATION)) { + if (includeLocationSensitiveInfo + && redactionPermissionChecker.hasLocationPermission()) { + redactions &= ~REDACT_FOR_ACCESS_FINE_LOCATION; + } + } + if (shouldRedact(redactions, REDACT_FOR_LOCAL_MAC_ADDRESS)) { + if (redactionPermissionChecker.hasLocalMacAddressPermission()) { + redactions &= ~REDACT_FOR_LOCAL_MAC_ADDRESS; + } + } + if (shouldRedact(redactions, REDACT_FOR_NETWORK_SETTINGS)) { + if (redactionPermissionChecker.hasSettingsPermission()) { + redactions &= ~REDACT_FOR_NETWORK_SETTINGS; + } + } + return redactions; + } + + @VisibleForTesting + @Nullable + NetworkCapabilities createWithLocationInfoSanitizedIfNecessaryWhenParceled( + @Nullable NetworkCapabilities nc, boolean includeLocationSensitiveInfo, + int callingPid, int callingUid, @NonNull String callingPkgName, + @Nullable String callingAttributionTag) { + if (nc == null) { + return null; + } + // Avoid doing location permission check if the transport info has no location sensitive + // data. + final RedactionPermissionChecker redactionPermissionChecker = + new RedactionPermissionChecker(callingPid, callingUid, callingPkgName, + callingAttributionTag); + final long redactions = retrieveRequiredRedactions( + nc.getApplicableRedactions(), redactionPermissionChecker, + includeLocationSensitiveInfo); + final NetworkCapabilities newNc = new NetworkCapabilities(nc, redactions); + // Reset owner uid if not destined for the owner app. + if (callingUid != nc.getOwnerUid()) { + newNc.setOwnerUid(INVALID_UID); + return newNc; + } + // Allow VPNs to see ownership of their own VPN networks - not location sensitive. + if (nc.hasTransport(TRANSPORT_VPN)) { + // Owner UIDs already checked above. No need to re-check. + return newNc; + } + // If the calling does not want location sensitive data & target SDK >= S, then mask info. + // Else include the owner UID iff the calling has location permission to provide backwards + // compatibility for older apps. + if (!includeLocationSensitiveInfo + && isTargetSdkAtleast( + Build.VERSION_CODES.S, callingUid, callingPkgName)) { + newNc.setOwnerUid(INVALID_UID); + return newNc; + } + // Reset owner uid if the app has no location permission. + if (!redactionPermissionChecker.hasLocationPermission()) { + newNc.setOwnerUid(INVALID_UID); + } + return newNc; + } + + private LinkProperties linkPropertiesRestrictedForCallerPermissions( + LinkProperties lp, int callerPid, int callerUid) { + if (lp == null) return new LinkProperties(); + + // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls. + final boolean needsSanitization = + (lp.getCaptivePortalApiUrl() != null || lp.getCaptivePortalData() != null); + if (!needsSanitization) { + return new LinkProperties(lp); + } + + if (checkSettingsPermission(callerPid, callerUid)) { + return new LinkProperties(lp, true /* parcelSensitiveFields */); + } + + final LinkProperties newLp = new LinkProperties(lp); + // Sensitive fields would not be parceled anyway, but sanitize for consistency before the + // object gets parceled. + newLp.setCaptivePortalApiUrl(null); + newLp.setCaptivePortalData(null); + return newLp; + } + + private void restrictRequestUidsForCallerAndSetRequestorInfo(NetworkCapabilities nc, + int callerUid, String callerPackageName) { + // There is no need to track the effective UID of the request here. If the caller + // lacks the settings permission, the effective UID is the same as the calling ID. + if (!checkSettingsPermission()) { + // Unprivileged apps can only pass in null or their own UID. + if (nc.getUids() == null) { + // If the caller passes in null, the callback will also match networks that do not + // apply to its UID, similarly to what it would see if it called getAllNetworks. + // In this case, redact everything in the request immediately. This ensures that the + // app is not able to get any redacted information by filing an unredacted request + // and observing whether the request matches something. + if (nc.getNetworkSpecifier() != null) { + nc.setNetworkSpecifier(nc.getNetworkSpecifier().redact()); + } + } else { + nc.setSingleUid(callerUid); + } + } + nc.setRequestorUidAndPackageName(callerUid, callerPackageName); + nc.setAdministratorUids(new int[0]); + + // Clear owner UID; this can never come from an app. + nc.setOwnerUid(INVALID_UID); + } + + private void restrictBackgroundRequestForCaller(NetworkCapabilities nc) { + if (!mPermissionMonitor.hasUseBackgroundNetworksPermission(mDeps.getCallingUid())) { + nc.addCapability(NET_CAPABILITY_FOREGROUND); + } + } + + @Override + public @RestrictBackgroundStatus int getRestrictBackgroundStatusByCaller() { + enforceAccessPermission(); + final int callerUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + return mPolicyManager.getRestrictBackgroundStatus(callerUid); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // TODO: Consider delete this function or turn it into a no-op method. + @Override + public NetworkState[] getAllNetworkState() { + // This contains IMSI details, so make sure the caller is privileged. + PermissionUtils.enforceNetworkStackPermission(mContext); + + final ArrayList<NetworkState> result = new ArrayList<>(); + for (NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) { + // NetworkStateSnapshot doesn't contain NetworkInfo, so need to fetch it from the + // NetworkAgentInfo. + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork()); + if (nai != null && nai.networkInfo.isConnected()) { + result.add(new NetworkState(new NetworkInfo(nai.networkInfo), + snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(), + snapshot.getNetwork(), snapshot.getSubscriberId())); + } + } + return result.toArray(new NetworkState[result.size()]); + } + + @Override + @NonNull + public List<NetworkStateSnapshot> getAllNetworkStateSnapshots() { + // This contains IMSI details, so make sure the caller is privileged. + enforceNetworkStackOrSettingsPermission(); + + final ArrayList<NetworkStateSnapshot> result = new ArrayList<>(); + for (Network network : getAllNetworks()) { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + // TODO: Consider include SUSPENDED networks, which should be considered as + // temporary shortage of connectivity of a connected network. + if (nai != null && nai.networkInfo.isConnected()) { + // TODO (b/73321673) : NetworkStateSnapshot contains a copy of the + // NetworkCapabilities, which may contain UIDs of apps to which the + // network applies. Should the UIDs be cleared so as not to leak or + // interfere ? + result.add(nai.getNetworkStateSnapshot()); + } + } + return result; + } + + @Override + public boolean isActiveNetworkMetered() { + enforceAccessPermission(); + + final NetworkCapabilities caps = getNetworkCapabilitiesInternal(getActiveNetwork()); + if (caps != null) { + return !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + } else { + // Always return the most conservative value + return true; + } + } + + /** + * Ensures that the system cannot call a particular method. + */ + private boolean disallowedBecauseSystemCaller() { + // TODO: start throwing a SecurityException when GnssLocationProvider stops calling + // requestRouteToHost. In Q, GnssLocationProvider is changed to not call requestRouteToHost + // for devices launched with Q and above. However, existing devices upgrading to Q and + // above must continued to be supported for few more releases. + if (isSystem(mDeps.getCallingUid()) && SystemProperties.getInt( + "ro.product.first_api_level", 0) > Build.VERSION_CODES.P) { + log("This method exists only for app backwards compatibility" + + " and must not be called by system services."); + return true; + } + return false; + } + + /** + * Ensure that a network route exists to deliver traffic to the specified + * host via the specified network interface. + * @param networkType the type of the network over which traffic to the + * specified host is to be routed + * @param hostAddress the IP address of the host to which the route is + * desired + * @return {@code true} on success, {@code false} on failure + */ + @Override + public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress, + String callingPackageName, String callingAttributionTag) { + if (disallowedBecauseSystemCaller()) { + return false; + } + enforceChangePermission(callingPackageName, callingAttributionTag); + if (mProtectedNetworks.contains(networkType)) { + enforceConnectivityRestrictedNetworksPermission(); + } + + InetAddress addr; + try { + addr = InetAddress.getByAddress(hostAddress); + } catch (UnknownHostException e) { + if (DBG) log("requestRouteToHostAddress got " + e.toString()); + return false; + } + + if (!ConnectivityManager.isNetworkTypeValid(networkType)) { + if (DBG) log("requestRouteToHostAddress on invalid network: " + networkType); + return false; + } + + NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); + if (nai == null) { + if (mLegacyTypeTracker.isTypeSupported(networkType) == false) { + if (DBG) log("requestRouteToHostAddress on unsupported network: " + networkType); + } else { + if (DBG) log("requestRouteToHostAddress on down network: " + networkType); + } + return false; + } + + DetailedState netState; + synchronized (nai) { + netState = nai.networkInfo.getDetailedState(); + } + + if (netState != DetailedState.CONNECTED && netState != DetailedState.CAPTIVE_PORTAL_CHECK) { + if (VDBG) { + log("requestRouteToHostAddress on down network " + + "(" + networkType + ") - dropped" + + " netState=" + netState); + } + return false; + } + + final int uid = mDeps.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + LinkProperties lp; + int netId; + synchronized (nai) { + lp = nai.linkProperties; + netId = nai.network.getNetId(); + } + boolean ok = addLegacyRouteToHost(lp, addr, netId, uid); + if (DBG) { + log("requestRouteToHostAddress " + addr + nai.toShortString() + " ok=" + ok); + } + return ok; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private boolean addLegacyRouteToHost(LinkProperties lp, InetAddress addr, int netId, int uid) { + RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), addr); + if (bestRoute == null) { + bestRoute = RouteInfo.makeHostRoute(addr, lp.getInterfaceName()); + } else { + String iface = bestRoute.getInterface(); + if (bestRoute.getGateway().equals(addr)) { + // if there is no better route, add the implied hostroute for our gateway + bestRoute = RouteInfo.makeHostRoute(addr, iface); + } else { + // if we will connect to this through another route, add a direct route + // to it's gateway + bestRoute = RouteInfo.makeHostRoute(addr, bestRoute.getGateway(), iface); + } + } + if (DBG) log("Adding legacy route " + bestRoute + + " for UID/PID " + uid + "/" + Binder.getCallingPid()); + + final String dst = bestRoute.getDestinationLinkAddress().toString(); + final String nextHop = bestRoute.hasGateway() + ? bestRoute.getGateway().getHostAddress() : ""; + try { + mNetd.networkAddLegacyRoute(netId, bestRoute.getInterface(), dst, nextHop , uid); + } catch (RemoteException | ServiceSpecificException e) { + if (DBG) loge("Exception trying to add a route: " + e); + return false; + } + return true; + } + + class DnsResolverUnsolicitedEventCallback extends + IDnsResolverUnsolicitedEventListener.Stub { + @Override + public void onPrivateDnsValidationEvent(final PrivateDnsValidationEventParcel event) { + try { + mHandler.sendMessage(mHandler.obtainMessage( + EVENT_PRIVATE_DNS_VALIDATION_UPDATE, + new PrivateDnsValidationUpdate(event.netId, + InetAddresses.parseNumericAddress(event.ipAddress), + event.hostname, event.validation))); + } catch (IllegalArgumentException e) { + loge("Error parsing ip address in validation event"); + } + } + + @Override + public void onDnsHealthEvent(final DnsHealthEventParcel event) { + NetworkAgentInfo nai = getNetworkAgentInfoForNetId(event.netId); + // Netd event only allow registrants from system. Each NetworkMonitor thread is under + // the caller thread of registerNetworkAgent. Thus, it's not allowed to register netd + // event callback for certain nai. e.g. cellular. Register here to pass to + // NetworkMonitor instead. + // TODO: Move the Dns Event to NetworkMonitor. NetdEventListenerService only allow one + // callback from each caller type. Need to re-factor NetdEventListenerService to allow + // multiple NetworkMonitor registrants. + if (nai != null && nai.satisfies(mDefaultRequest.mRequests.get(0))) { + nai.networkMonitor().notifyDnsResponse(event.healthResult); + } + } + + @Override + public void onNat64PrefixEvent(final Nat64PrefixEventParcel event) { + mHandler.post(() -> handleNat64PrefixEvent(event.netId, event.prefixOperation, + event.prefixAddress, event.prefixLength)); + } + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() { + return this.HASH; + } + } + + @VisibleForTesting + protected final DnsResolverUnsolicitedEventCallback mResolverUnsolEventCallback = + new DnsResolverUnsolicitedEventCallback(); + + private void registerDnsResolverUnsolicitedEventListener() { + try { + mDnsResolver.registerUnsolicitedEventListener(mResolverUnsolEventCallback); + } catch (Exception e) { + loge("Error registering DnsResolver unsolicited event callback: " + e); + } + } + + private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() { + @Override + public void onUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_UID_BLOCKED_REASON_CHANGED, + uid, blockedReasons)); + } + }; + + private void handleUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) { + maybeNotifyNetworkBlockedForNewState(uid, blockedReasons); + setUidBlockedReasons(uid, blockedReasons); + } + + private boolean checkAnyPermissionOf(String... permissions) { + for (String permission : permissions) { + if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + return true; + } + } + return false; + } + + private boolean checkAnyPermissionOf(int pid, int uid, String... permissions) { + for (String permission : permissions) { + if (mContext.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) { + return true; + } + } + return false; + } + + private void enforceAnyPermissionOf(String... permissions) { + if (!checkAnyPermissionOf(permissions)) { + throw new SecurityException("Requires one of the following permissions: " + + String.join(", ", permissions) + "."); + } + } + + private void enforceInternetPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERNET, + "ConnectivityService"); + } + + private void enforceAccessPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_NETWORK_STATE, + "ConnectivityService"); + } + + /** + * Performs a strict and comprehensive check of whether a calling package is allowed to + * change the state of network, as the condition differs for pre-M, M+, and + * privileged/preinstalled apps. The caller is expected to have either the + * CHANGE_NETWORK_STATE or the WRITE_SETTINGS permission declared. Either of these + * permissions allow changing network state; WRITE_SETTINGS is a runtime permission and + * can be revoked, but (except in M, excluding M MRs), CHANGE_NETWORK_STATE is a normal + * permission and cannot be revoked. See http://b/23597341 + * + * Note: if the check succeeds because the application holds WRITE_SETTINGS, the operation + * of this app will be updated to the current time. + */ + private void enforceChangePermission(String callingPkg, String callingAttributionTag) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.CHANGE_NETWORK_STATE) + == PackageManager.PERMISSION_GRANTED) { + return; + } + + if (callingPkg == null) { + throw new SecurityException("Calling package name is null."); + } + + final AppOpsManager appOpsMgr = mContext.getSystemService(AppOpsManager.class); + final int uid = mDeps.getCallingUid(); + final int mode = appOpsMgr.noteOpNoThrow(AppOpsManager.OPSTR_WRITE_SETTINGS, uid, + callingPkg, callingAttributionTag, null /* message */); + + if (mode == AppOpsManager.MODE_ALLOWED) { + return; + } + + if ((mode == AppOpsManager.MODE_DEFAULT) && (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.WRITE_SETTINGS) == PackageManager.PERMISSION_GRANTED)) { + return; + } + + throw new SecurityException(callingPkg + " was not granted either of these permissions:" + + android.Manifest.permission.CHANGE_NETWORK_STATE + "," + + android.Manifest.permission.WRITE_SETTINGS + "."); + } + + private void enforceSettingsPermission() { + enforceAnyPermissionOf( + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private void enforceNetworkFactoryPermission() { + enforceAnyPermissionOf( + android.Manifest.permission.NETWORK_FACTORY, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private void enforceNetworkFactoryOrSettingsPermission() { + enforceAnyPermissionOf( + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_FACTORY, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private void enforceNetworkFactoryOrTestNetworksPermission() { + enforceAnyPermissionOf( + android.Manifest.permission.MANAGE_TEST_NETWORKS, + android.Manifest.permission.NETWORK_FACTORY, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private boolean checkSettingsPermission() { + return checkAnyPermissionOf( + android.Manifest.permission.NETWORK_SETTINGS, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private boolean checkSettingsPermission(int pid, int uid) { + return PERMISSION_GRANTED == mContext.checkPermission( + android.Manifest.permission.NETWORK_SETTINGS, pid, uid) + || PERMISSION_GRANTED == mContext.checkPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid); + } + + private void enforceNetworkStackOrSettingsPermission() { + enforceAnyPermissionOf( + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private void enforceNetworkStackSettingsOrSetup() { + enforceAnyPermissionOf( + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_SETUP_WIZARD, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private void enforceAirplaneModePermission() { + enforceAnyPermissionOf( + android.Manifest.permission.NETWORK_AIRPLANE_MODE, + android.Manifest.permission.NETWORK_SETTINGS, + android.Manifest.permission.NETWORK_SETUP_WIZARD, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private void enforceOemNetworkPreferencesPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE, + "ConnectivityService"); + } + + private boolean checkNetworkStackPermission() { + return checkAnyPermissionOf( + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private boolean checkNetworkStackPermission(int pid, int uid) { + return checkAnyPermissionOf(pid, uid, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + } + + private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) { + return checkAnyPermissionOf(pid, uid, + android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + android.Manifest.permission.NETWORK_SETTINGS); + } + + private void enforceConnectivityRestrictedNetworksPermission() { + try { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS, + "ConnectivityService"); + return; + } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ } + // TODO: Remove this fallback check after all apps have declared + // CONNECTIVITY_USE_RESTRICTED_NETWORKS. + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CONNECTIVITY_INTERNAL, + "ConnectivityService"); + } + + private void enforceKeepalivePermission() { + mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService"); + } + + private boolean checkLocalMacAddressPermission(int pid, int uid) { + return PERMISSION_GRANTED == mContext.checkPermission( + Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid); + } + + private void sendConnectedBroadcast(NetworkInfo info) { + sendGeneralBroadcast(info, CONNECTIVITY_ACTION); + } + + private void sendInetConditionBroadcast(NetworkInfo info) { + sendGeneralBroadcast(info, ConnectivityManager.INET_CONDITION_ACTION); + } + + private Intent makeGeneralIntent(NetworkInfo info, String bcastType) { + Intent intent = new Intent(bcastType); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info)); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType()); + if (info.isFailover()) { + intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true); + info.setFailover(false); + } + if (info.getReason() != null) { + intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason()); + } + if (info.getExtraInfo() != null) { + intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, + info.getExtraInfo()); + } + intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished); + return intent; + } + + private void sendGeneralBroadcast(NetworkInfo info, String bcastType) { + sendStickyBroadcast(makeGeneralIntent(info, bcastType)); + } + + private void sendStickyBroadcast(Intent intent) { + synchronized (this) { + if (!mSystemReady + && intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) { + mInitialBroadcast = new Intent(intent); + } + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + if (VDBG) { + log("sendStickyBroadcast: action=" + intent.getAction()); + } + + Bundle options = null; + final long ident = Binder.clearCallingIdentity(); + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + final NetworkInfo ni = intent.getParcelableExtra( + ConnectivityManager.EXTRA_NETWORK_INFO); + final BroadcastOptions opts = BroadcastOptions.makeBasic(); + opts.setMaxManifestReceiverApiLevel(Build.VERSION_CODES.M); + options = opts.toBundle(); + intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); + } + try { + mUserAllContext.sendStickyBroadcast(intent, options); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + /** + * Called by SystemServer through ConnectivityManager when the system is ready. + */ + @Override + public void systemReady() { + if (mDeps.getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Calling Uid is not system uid."); + } + systemReadyInternal(); + } + + /** + * Called when ConnectivityService can initialize remaining components. + */ + @VisibleForTesting + public void systemReadyInternal() { + // Since mApps in PermissionMonitor needs to be populated first to ensure that + // listening network request which is sent by MultipathPolicyTracker won't be added + // NET_CAPABILITY_FOREGROUND capability. Thus, MultipathPolicyTracker.start() must + // be called after PermissionMonitor#startMonitoring(). + // Calling PermissionMonitor#startMonitoring() in systemReadyInternal() and the + // MultipathPolicyTracker.start() is called in NetworkPolicyManagerService#systemReady() + // to ensure the tracking will be initialized correctly. + mPermissionMonitor.startMonitoring(); + mProxyTracker.loadGlobalProxy(); + registerDnsResolverUnsolicitedEventListener(); + + synchronized (this) { + mSystemReady = true; + if (mInitialBroadcast != null) { + mContext.sendStickyBroadcastAsUser(mInitialBroadcast, UserHandle.ALL); + mInitialBroadcast = null; + } + } + + // Create network requests for always-on networks. + mHandler.sendMessage(mHandler.obtainMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS)); + } + + /** + * Start listening for default data network activity state changes. + */ + @Override + public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) { + mNetworkActivityTracker.registerNetworkActivityListener(l); + } + + /** + * Stop listening for default data network activity state changes. + */ + @Override + public void unregisterNetworkActivityListener(@NonNull INetworkActivityListener l) { + mNetworkActivityTracker.unregisterNetworkActivityListener(l); + } + + /** + * Check whether the default network radio is currently active. + */ + @Override + public boolean isDefaultNetworkActive() { + return mNetworkActivityTracker.isDefaultNetworkActive(); + } + + /** + * Reads the network specific MTU size from resources. + * and set it on it's iface. + */ + private void updateMtu(LinkProperties newLp, LinkProperties oldLp) { + final String iface = newLp.getInterfaceName(); + final int mtu = newLp.getMtu(); + if (oldLp == null && mtu == 0) { + // Silently ignore unset MTU value. + return; + } + if (oldLp != null && newLp.isIdenticalMtu(oldLp)) { + if (VDBG) log("identical MTU - not setting"); + return; + } + if (!LinkProperties.isValidMtu(mtu, newLp.hasGlobalIpv6Address())) { + if (mtu != 0) loge("Unexpected mtu value: " + mtu + ", " + iface); + return; + } + + // Cannot set MTU without interface name + if (TextUtils.isEmpty(iface)) { + loge("Setting MTU size with null iface."); + return; + } + + try { + if (VDBG || DDBG) log("Setting MTU size: " + iface + ", " + mtu); + mNetd.interfaceSetMtu(iface, mtu); + } catch (RemoteException | ServiceSpecificException e) { + loge("exception in interfaceSetMtu()" + e); + } + } + + @VisibleForTesting + protected static final String DEFAULT_TCP_BUFFER_SIZES = "4096,87380,110208,4096,16384,110208"; + + private void updateTcpBufferSizes(String tcpBufferSizes) { + String[] values = null; + if (tcpBufferSizes != null) { + values = tcpBufferSizes.split(","); + } + + if (values == null || values.length != 6) { + if (DBG) log("Invalid tcpBufferSizes string: " + tcpBufferSizes +", using defaults"); + tcpBufferSizes = DEFAULT_TCP_BUFFER_SIZES; + values = tcpBufferSizes.split(","); + } + + if (tcpBufferSizes.equals(mCurrentTcpBufferSizes)) return; + + try { + if (VDBG || DDBG) log("Setting tx/rx TCP buffers to " + tcpBufferSizes); + + String rmemValues = String.join(" ", values[0], values[1], values[2]); + String wmemValues = String.join(" ", values[3], values[4], values[5]); + mNetd.setTcpRWmemorySize(rmemValues, wmemValues); + mCurrentTcpBufferSizes = tcpBufferSizes; + } catch (RemoteException | ServiceSpecificException e) { + loge("Can't set TCP buffer sizes:" + e); + } + } + + @Override + public int getRestoreDefaultNetworkDelay(int networkType) { + String restoreDefaultNetworkDelayStr = mSystemProperties.get( + NETWORK_RESTORE_DELAY_PROP_NAME); + if(restoreDefaultNetworkDelayStr != null && + restoreDefaultNetworkDelayStr.length() != 0) { + try { + return Integer.parseInt(restoreDefaultNetworkDelayStr); + } catch (NumberFormatException e) { + } + } + // if the system property isn't set, use the value for the apn type + int ret = RESTORE_DEFAULT_NETWORK_DELAY; + + if (mLegacyTypeTracker.isTypeSupported(networkType)) { + ret = mLegacyTypeTracker.getRestoreTimerForType(networkType); + } + return ret; + } + + private void dumpNetworkDiagnostics(IndentingPrintWriter pw) { + final List<NetworkDiagnostics> netDiags = new ArrayList<NetworkDiagnostics>(); + final long DIAG_TIME_MS = 5000; + for (NetworkAgentInfo nai : networksSortedById()) { + PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(nai.network); + // Start gathering diagnostic information. + netDiags.add(new NetworkDiagnostics( + nai.network, + new LinkProperties(nai.linkProperties), // Must be a copy. + privateDnsCfg, + DIAG_TIME_MS)); + } + + for (NetworkDiagnostics netDiag : netDiags) { + pw.println(); + netDiag.waitForMeasurements(); + netDiag.dump(pw); + } + } + + @Override + protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, + @Nullable String[] args) { + if (!checkDumpPermission(mContext, TAG, writer)) return; + + mPriorityDumper.dump(fd, writer, args); + } + + private boolean checkDumpPermission(Context context, String tag, PrintWriter pw) { + if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump " + tag + " from from pid=" + + Binder.getCallingPid() + ", uid=" + mDeps.getCallingUid() + + " due to missing android.permission.DUMP permission"); + return false; + } else { + return true; + } + } + + private void doDump(FileDescriptor fd, PrintWriter writer, String[] args) { + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + + if (CollectionUtils.contains(args, DIAG_ARG)) { + dumpNetworkDiagnostics(pw); + return; + } else if (CollectionUtils.contains(args, NETWORK_ARG)) { + dumpNetworks(pw); + return; + } else if (CollectionUtils.contains(args, REQUEST_ARG)) { + dumpNetworkRequests(pw); + return; + } + + pw.print("NetworkProviders for:"); + for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) { + pw.print(" " + npi.name); + } + pw.println(); + pw.println(); + + final NetworkAgentInfo defaultNai = getDefaultNetwork(); + pw.print("Active default network: "); + if (defaultNai == null) { + pw.println("none"); + } else { + pw.println(defaultNai.network.getNetId()); + } + pw.println(); + + pw.print("Current per-app default networks: "); + pw.increaseIndent(); + dumpPerAppNetworkPreferences(pw); + pw.decreaseIndent(); + pw.println(); + + pw.println("Current Networks:"); + pw.increaseIndent(); + dumpNetworks(pw); + pw.decreaseIndent(); + pw.println(); + + pw.println("Status for known UIDs:"); + pw.increaseIndent(); + final int size = mUidBlockedReasons.size(); + for (int i = 0; i < size; i++) { + // Don't crash if the array is modified while dumping in bugreports. + try { + final int uid = mUidBlockedReasons.keyAt(i); + final int blockedReasons = mUidBlockedReasons.valueAt(i); + pw.println("UID=" + uid + " blockedReasons=" + + Integer.toHexString(blockedReasons)); + } catch (ArrayIndexOutOfBoundsException e) { + pw.println(" ArrayIndexOutOfBoundsException"); + } catch (ConcurrentModificationException e) { + pw.println(" ConcurrentModificationException"); + } + } + pw.println(); + pw.decreaseIndent(); + + pw.println("Network Requests:"); + pw.increaseIndent(); + dumpNetworkRequests(pw); + pw.decreaseIndent(); + pw.println(); + + mLegacyTypeTracker.dump(pw); + + pw.println(); + mKeepaliveTracker.dump(pw); + + pw.println(); + dumpAvoidBadWifiSettings(pw); + + pw.println(); + + if (!CollectionUtils.contains(args, SHORT_ARG)) { + pw.println(); + pw.println("mNetworkRequestInfoLogs (most recent first):"); + pw.increaseIndent(); + mNetworkRequestInfoLogs.reverseDump(pw); + pw.decreaseIndent(); + + pw.println(); + pw.println("mNetworkInfoBlockingLogs (most recent first):"); + pw.increaseIndent(); + mNetworkInfoBlockingLogs.reverseDump(pw); + pw.decreaseIndent(); + + pw.println(); + pw.println("NetTransition WakeLock activity (most recent first):"); + pw.increaseIndent(); + pw.println("total acquisitions: " + mTotalWakelockAcquisitions); + pw.println("total releases: " + mTotalWakelockReleases); + pw.println("cumulative duration: " + (mTotalWakelockDurationMs / 1000) + "s"); + pw.println("longest duration: " + (mMaxWakelockDurationMs / 1000) + "s"); + if (mTotalWakelockAcquisitions > mTotalWakelockReleases) { + long duration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp; + pw.println("currently holding WakeLock for: " + (duration / 1000) + "s"); + } + mWakelockLogs.reverseDump(pw); + + pw.println(); + pw.println("bandwidth update requests (by uid):"); + pw.increaseIndent(); + synchronized (mBandwidthRequests) { + for (int i = 0; i < mBandwidthRequests.size(); i++) { + pw.println("[" + mBandwidthRequests.keyAt(i) + + "]: " + mBandwidthRequests.valueAt(i)); + } + } + pw.decreaseIndent(); + pw.decreaseIndent(); + + pw.println(); + pw.println("mOemNetworkPreferencesLogs (most recent first):"); + pw.increaseIndent(); + mOemNetworkPreferencesLogs.reverseDump(pw); + pw.decreaseIndent(); + } + + pw.println(); + + pw.println(); + pw.println("Permission Monitor:"); + pw.increaseIndent(); + mPermissionMonitor.dump(pw); + pw.decreaseIndent(); + + pw.println(); + pw.println("Legacy network activity:"); + pw.increaseIndent(); + mNetworkActivityTracker.dump(pw); + pw.decreaseIndent(); + } + + private void dumpNetworks(IndentingPrintWriter pw) { + for (NetworkAgentInfo nai : networksSortedById()) { + pw.println(nai.toString()); + pw.increaseIndent(); + pw.println(String.format( + "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d", + nai.numForegroundNetworkRequests(), + nai.numNetworkRequests() - nai.numRequestNetworkRequests(), + nai.numBackgroundNetworkRequests(), + nai.numNetworkRequests())); + pw.increaseIndent(); + for (int i = 0; i < nai.numNetworkRequests(); i++) { + pw.println(nai.requestAt(i).toString()); + } + pw.decreaseIndent(); + pw.println("Inactivity Timers:"); + pw.increaseIndent(); + nai.dumpInactivityTimers(pw); + pw.decreaseIndent(); + pw.decreaseIndent(); + } + } + + private void dumpPerAppNetworkPreferences(IndentingPrintWriter pw) { + pw.println("Per-App Network Preference:"); + pw.increaseIndent(); + if (0 == mOemNetworkPreferences.getNetworkPreferences().size()) { + pw.println("none"); + } else { + pw.println(mOemNetworkPreferences.toString()); + } + pw.decreaseIndent(); + + for (final NetworkRequestInfo defaultRequest : mDefaultNetworkRequests) { + if (mDefaultRequest == defaultRequest) { + continue; + } + + final boolean isActive = null != defaultRequest.getSatisfier(); + pw.println("Is per-app network active:"); + pw.increaseIndent(); + pw.println(isActive); + if (isActive) { + pw.println("Active network: " + defaultRequest.getSatisfier().network.netId); + } + pw.println("Tracked UIDs:"); + pw.increaseIndent(); + if (0 == defaultRequest.mRequests.size()) { + pw.println("none, this should never occur."); + } else { + pw.println(defaultRequest.mRequests.get(0).networkCapabilities.getUidRanges()); + } + pw.decreaseIndent(); + pw.decreaseIndent(); + } + } + + private void dumpNetworkRequests(IndentingPrintWriter pw) { + for (NetworkRequestInfo nri : requestsSortedById()) { + pw.println(nri.toString()); + } + } + + /** + * Return an array of all current NetworkAgentInfos sorted by network id. + */ + private NetworkAgentInfo[] networksSortedById() { + NetworkAgentInfo[] networks = new NetworkAgentInfo[0]; + networks = mNetworkAgentInfos.toArray(networks); + Arrays.sort(networks, Comparator.comparingInt(nai -> nai.network.getNetId())); + return networks; + } + + /** + * Return an array of all current NetworkRequest sorted by request id. + */ + @VisibleForTesting + NetworkRequestInfo[] requestsSortedById() { + NetworkRequestInfo[] requests = new NetworkRequestInfo[0]; + requests = getNrisFromGlobalRequests().toArray(requests); + // Sort the array based off the NRI containing the min requestId in its requests. + Arrays.sort(requests, + Comparator.comparingInt(nri -> Collections.min(nri.mRequests, + Comparator.comparingInt(req -> req.requestId)).requestId + ) + ); + return requests; + } + + private boolean isLiveNetworkAgent(NetworkAgentInfo nai, int what) { + final NetworkAgentInfo officialNai = getNetworkAgentInfoForNetwork(nai.network); + if (officialNai != null && officialNai.equals(nai)) return true; + if (officialNai != null || VDBG) { + loge(eventName(what) + " - isLiveNetworkAgent found mismatched netId: " + officialNai + + " - " + nai); + } + return false; + } + + // must be stateless - things change under us. + private class NetworkStateTrackerHandler extends Handler { + public NetworkStateTrackerHandler(Looper looper) { + super(looper); + } + + private void maybeHandleNetworkAgentMessage(Message msg) { + final Pair<NetworkAgentInfo, Object> arg = (Pair<NetworkAgentInfo, Object>) msg.obj; + final NetworkAgentInfo nai = arg.first; + if (!mNetworkAgentInfos.contains(nai)) { + if (VDBG) { + log(String.format("%s from unknown NetworkAgent", eventName(msg.what))); + } + return; + } + + switch (msg.what) { + case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: { + NetworkCapabilities networkCapabilities = (NetworkCapabilities) arg.second; + if (networkCapabilities.hasConnectivityManagedCapability()) { + Log.wtf(TAG, "BUG: " + nai + " has CS-managed capability."); + } + if (networkCapabilities.hasTransport(TRANSPORT_TEST)) { + // Make sure the original object is not mutated. NetworkAgent normally + // makes a copy of the capabilities when sending the message through + // the Messenger, but if this ever changes, not making a defensive copy + // here will give attack vectors to clients using this code path. + networkCapabilities = new NetworkCapabilities(networkCapabilities); + networkCapabilities.restrictCapabilitesForTestNetwork(nai.creatorUid); + } + processCapabilitiesFromAgent(nai, networkCapabilities); + updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities); + break; + } + case NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED: { + LinkProperties newLp = (LinkProperties) arg.second; + processLinkPropertiesFromAgent(nai, newLp); + handleUpdateLinkProperties(nai, newLp); + break; + } + case NetworkAgent.EVENT_NETWORK_INFO_CHANGED: { + NetworkInfo info = (NetworkInfo) arg.second; + updateNetworkInfo(nai, info); + break; + } + case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: { + updateNetworkScore(nai, (NetworkScore) arg.second); + break; + } + case NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED: { + if (nai.everConnected) { + loge("ERROR: cannot call explicitlySelected on already-connected network"); + // Note that if the NAI had been connected, this would affect the + // score, and therefore would require re-mixing the score and performing + // a rematch. + } + nai.networkAgentConfig.explicitlySelected = toBool(msg.arg1); + nai.networkAgentConfig.acceptUnvalidated = toBool(msg.arg1) && toBool(msg.arg2); + // Mark the network as temporarily accepting partial connectivity so that it + // will be validated (and possibly become default) even if it only provides + // partial internet access. Note that if user connects to partial connectivity + // and choose "don't ask again", then wifi disconnected by some reasons(maybe + // out of wifi coverage) and if the same wifi is available again, the device + // will auto connect to this wifi even though the wifi has "no internet". + // TODO: Evaluate using a separate setting in IpMemoryStore. + nai.networkAgentConfig.acceptPartialConnectivity = toBool(msg.arg2); + break; + } + case NetworkAgent.EVENT_SOCKET_KEEPALIVE: { + mKeepaliveTracker.handleEventSocketKeepalive(nai, msg.arg1, msg.arg2); + break; + } + case NetworkAgent.EVENT_UNDERLYING_NETWORKS_CHANGED: { + // TODO: prevent loops, e.g., if a network declares itself as underlying. + if (!nai.supportsUnderlyingNetworks()) { + Log.wtf(TAG, "Non-virtual networks cannot have underlying networks"); + break; + } + + final List<Network> underlying = (List<Network>) arg.second; + + if (isLegacyLockdownNai(nai) + && (underlying == null || underlying.size() != 1)) { + Log.wtf(TAG, "Legacy lockdown VPN " + nai.toShortString() + + " must have exactly one underlying network: " + underlying); + } + + final Network[] oldUnderlying = nai.declaredUnderlyingNetworks; + nai.declaredUnderlyingNetworks = (underlying != null) + ? underlying.toArray(new Network[0]) : null; + + if (!Arrays.equals(oldUnderlying, nai.declaredUnderlyingNetworks)) { + if (DBG) { + log(nai.toShortString() + " changed underlying networks to " + + Arrays.toString(nai.declaredUnderlyingNetworks)); + } + updateCapabilitiesForNetwork(nai); + notifyIfacesChangedForNetworkStats(); + } + break; + } + case NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED: { + if (msg.arg1 >= 0 && msg.arg1 <= NetworkAgent.MAX_TEARDOWN_DELAY_MS) { + nai.teardownDelayMs = msg.arg1; + } else { + logwtf(nai.toShortString() + " set invalid teardown delay " + msg.arg1); + } + break; + } + case NetworkAgent.EVENT_LINGER_DURATION_CHANGED: { + nai.setLingerDuration((int) arg.second); + break; + } + } + } + + private boolean maybeHandleNetworkMonitorMessage(Message msg) { + switch (msg.what) { + default: + return false; + case EVENT_PROBE_STATUS_CHANGED: { + final Integer netId = (Integer) msg.obj; + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId); + if (nai == null) { + break; + } + final boolean probePrivateDnsCompleted = + ((msg.arg1 & NETWORK_VALIDATION_PROBE_PRIVDNS) != 0); + final boolean privateDnsBroken = + ((msg.arg2 & NETWORK_VALIDATION_PROBE_PRIVDNS) == 0); + if (probePrivateDnsCompleted) { + if (nai.networkCapabilities.isPrivateDnsBroken() != privateDnsBroken) { + nai.networkCapabilities.setPrivateDnsBroken(privateDnsBroken); + updateCapabilitiesForNetwork(nai); + } + // Only show the notification when the private DNS is broken and the + // PRIVATE_DNS_BROKEN notification hasn't shown since last valid. + if (privateDnsBroken && !nai.networkAgentConfig.hasShownBroken) { + showNetworkNotification(nai, NotificationType.PRIVATE_DNS_BROKEN); + } + nai.networkAgentConfig.hasShownBroken = privateDnsBroken; + } else if (nai.networkCapabilities.isPrivateDnsBroken()) { + // If probePrivateDnsCompleted is false but nai.networkCapabilities says + // private DNS is broken, it means this network is being reevaluated. + // Either probing private DNS is not necessary any more or it hasn't been + // done yet. In either case, the networkCapabilities should be updated to + // reflect the new status. + nai.networkCapabilities.setPrivateDnsBroken(false); + updateCapabilitiesForNetwork(nai); + nai.networkAgentConfig.hasShownBroken = false; + } + break; + } + case EVENT_NETWORK_TESTED: { + final NetworkTestedResults results = (NetworkTestedResults) msg.obj; + + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(results.mNetId); + if (nai == null) break; + + handleNetworkTested(nai, results.mTestResult, + (results.mRedirectUrl == null) ? "" : results.mRedirectUrl); + break; + } + case EVENT_PROVISIONING_NOTIFICATION: { + final int netId = msg.arg2; + final boolean visible = toBool(msg.arg1); + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId); + // If captive portal status has changed, update capabilities or disconnect. + if (nai != null && (visible != nai.lastCaptivePortalDetected)) { + nai.lastCaptivePortalDetected = visible; + nai.everCaptivePortalDetected |= visible; + if (nai.lastCaptivePortalDetected && + ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID + == getCaptivePortalMode()) { + if (DBG) log("Avoiding captive portal network: " + nai.toShortString()); + nai.onPreventAutomaticReconnect(); + teardownUnneededNetwork(nai); + break; + } + updateCapabilitiesForNetwork(nai); + } + if (!visible) { + // Only clear SIGN_IN and NETWORK_SWITCH notifications here, or else other + // notifications belong to the same network may be cleared unexpectedly. + mNotifier.clearNotification(netId, NotificationType.SIGN_IN); + mNotifier.clearNotification(netId, NotificationType.NETWORK_SWITCH); + } else { + if (nai == null) { + loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor"); + break; + } + if (!nai.networkAgentConfig.provisioningNotificationDisabled) { + mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null, + (PendingIntent) msg.obj, + nai.networkAgentConfig.explicitlySelected); + } + } + break; + } + case EVENT_PRIVATE_DNS_CONFIG_RESOLVED: { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2); + if (nai == null) break; + + updatePrivateDns(nai, (PrivateDnsConfig) msg.obj); + break; + } + case EVENT_CAPPORT_DATA_CHANGED: { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2); + if (nai == null) break; + handleCapportApiDataUpdate(nai, (CaptivePortalData) msg.obj); + break; + } + } + return true; + } + + private void handleNetworkTested( + @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) { + final boolean wasPartial = nai.partialConnectivity; + nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0); + final boolean partialConnectivityChanged = + (wasPartial != nai.partialConnectivity); + + final boolean valid = ((testResult & NETWORK_VALIDATION_RESULT_VALID) != 0); + final boolean wasValidated = nai.lastValidated; + final boolean wasDefault = isDefaultNetwork(nai); + + if (DBG) { + final String logMsg = !TextUtils.isEmpty(redirectUrl) + ? " with redirect to " + redirectUrl + : ""; + log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg); + } + if (valid != nai.lastValidated) { + final int oldScore = nai.getCurrentScore(); + nai.lastValidated = valid; + nai.everValidated |= valid; + updateCapabilities(oldScore, nai, nai.networkCapabilities); + if (valid) { + handleFreshlyValidatedNetwork(nai); + // Clear NO_INTERNET, PRIVATE_DNS_BROKEN, PARTIAL_CONNECTIVITY and + // LOST_INTERNET notifications if network becomes valid. + mNotifier.clearNotification(nai.network.getNetId(), + NotificationType.NO_INTERNET); + mNotifier.clearNotification(nai.network.getNetId(), + NotificationType.LOST_INTERNET); + mNotifier.clearNotification(nai.network.getNetId(), + NotificationType.PARTIAL_CONNECTIVITY); + mNotifier.clearNotification(nai.network.getNetId(), + NotificationType.PRIVATE_DNS_BROKEN); + // If network becomes valid, the hasShownBroken should be reset for + // that network so that the notification will be fired when the private + // DNS is broken again. + nai.networkAgentConfig.hasShownBroken = false; + } + } else if (partialConnectivityChanged) { + updateCapabilitiesForNetwork(nai); + } + updateInetCondition(nai); + // Let the NetworkAgent know the state of its network + // TODO: Evaluate to update partial connectivity to status to NetworkAgent. + nai.onValidationStatusChanged( + valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK, + redirectUrl); + + // If NetworkMonitor detects partial connectivity before + // EVENT_PROMPT_UNVALIDATED arrives, show the partial connectivity notification + // immediately. Re-notify partial connectivity silently if no internet + // notification already there. + if (!wasPartial && nai.partialConnectivity) { + // Remove delayed message if there is a pending message. + mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network); + handlePromptUnvalidated(nai.network); + } + + if (wasValidated && !nai.lastValidated) { + handleNetworkUnvalidated(nai); + } + } + + private int getCaptivePortalMode() { + return Settings.Global.getInt(mContext.getContentResolver(), + ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE, + ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT); + } + + private boolean maybeHandleNetworkAgentInfoMessage(Message msg) { + switch (msg.what) { + default: + return false; + case NetworkAgentInfo.EVENT_NETWORK_LINGER_COMPLETE: { + NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj; + if (nai != null && isLiveNetworkAgent(nai, msg.what)) { + handleLingerComplete(nai); + } + break; + } + case NetworkAgentInfo.EVENT_AGENT_REGISTERED: { + handleNetworkAgentRegistered(msg); + break; + } + case NetworkAgentInfo.EVENT_AGENT_DISCONNECTED: { + handleNetworkAgentDisconnected(msg); + break; + } + } + return true; + } + + @Override + public void handleMessage(Message msg) { + if (!maybeHandleNetworkMonitorMessage(msg) + && !maybeHandleNetworkAgentInfoMessage(msg)) { + maybeHandleNetworkAgentMessage(msg); + } + } + } + + private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub { + private final int mNetId; + private final AutodestructReference<NetworkAgentInfo> mNai; + + private NetworkMonitorCallbacks(NetworkAgentInfo nai) { + mNetId = nai.network.getNetId(); + mNai = new AutodestructReference<>(nai); + } + + @Override + public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT, + new Pair<>(mNai.getAndDestroy(), networkMonitor))); + } + + @Override + public void notifyNetworkTested(int testResult, @Nullable String redirectUrl) { + // Legacy version of notifyNetworkTestedWithExtras. + // Would only be called if the system has a NetworkStack module older than the + // framework, which does not happen in practice. + Log.wtf(TAG, "Deprecated notifyNetworkTested called: no action taken"); + } + + @Override + public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) { + // Notify mTrackerHandler and mConnectivityDiagnosticsHandler of the event. Both use + // the same looper so messages will be processed in sequence. + final Message msg = mTrackerHandler.obtainMessage( + EVENT_NETWORK_TESTED, + new NetworkTestedResults( + mNetId, p.result, p.timestampMillis, p.redirectUrl)); + mTrackerHandler.sendMessage(msg); + + // Invoke ConnectivityReport generation for this Network test event. + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(mNetId); + if (nai == null) return; + + final PersistableBundle extras = new PersistableBundle(); + extras.putInt(KEY_NETWORK_VALIDATION_RESULT, p.result); + extras.putInt(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK, p.probesSucceeded); + extras.putInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK, p.probesAttempted); + + ConnectivityReportEvent reportEvent = + new ConnectivityReportEvent(p.timestampMillis, nai, extras); + final Message m = mConnectivityDiagnosticsHandler.obtainMessage( + ConnectivityDiagnosticsHandler.EVENT_NETWORK_TESTED, reportEvent); + mConnectivityDiagnosticsHandler.sendMessage(m); + } + + @Override + public void notifyPrivateDnsConfigResolved(PrivateDnsConfigParcel config) { + mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage( + EVENT_PRIVATE_DNS_CONFIG_RESOLVED, + 0, mNetId, PrivateDnsConfig.fromParcel(config))); + } + + @Override + public void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) { + mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage( + EVENT_PROBE_STATUS_CHANGED, + probesCompleted, probesSucceeded, new Integer(mNetId))); + } + + @Override + public void notifyCaptivePortalDataChanged(CaptivePortalData data) { + mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage( + EVENT_CAPPORT_DATA_CHANGED, + 0, mNetId, data)); + } + + @Override + public void showProvisioningNotification(String action, String packageName) { + final Intent intent = new Intent(action); + intent.setPackage(packageName); + + final PendingIntent pendingIntent; + // Only the system server can register notifications with package "android" + final long token = Binder.clearCallingIdentity(); + try { + pendingIntent = PendingIntent.getBroadcast( + mContext, + 0 /* requestCode */, + intent, + PendingIntent.FLAG_IMMUTABLE); + } finally { + Binder.restoreCallingIdentity(token); + } + mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage( + EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_SHOW, + mNetId, pendingIntent)); + } + + @Override + public void hideProvisioningNotification() { + mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage( + EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_HIDE, mNetId)); + } + + @Override + public void notifyDataStallSuspected(DataStallReportParcelable p) { + ConnectivityService.this.notifyDataStallSuspected(p, mNetId); + } + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() { + return this.HASH; + } + } + + private void notifyDataStallSuspected(DataStallReportParcelable p, int netId) { + log("Data stall detected with methods: " + p.detectionMethod); + + final PersistableBundle extras = new PersistableBundle(); + int detectionMethod = 0; + if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) { + extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, p.dnsConsecutiveTimeouts); + detectionMethod |= DETECTION_METHOD_DNS_EVENTS; + } + if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) { + extras.putInt(KEY_TCP_PACKET_FAIL_RATE, p.tcpPacketFailRate); + extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS, + p.tcpMetricsCollectionPeriodMillis); + detectionMethod |= DETECTION_METHOD_TCP_METRICS; + } + + final Message msg = mConnectivityDiagnosticsHandler.obtainMessage( + ConnectivityDiagnosticsHandler.EVENT_DATA_STALL_SUSPECTED, detectionMethod, netId, + new Pair<>(p.timestampMillis, extras)); + + // NetworkStateTrackerHandler currently doesn't take any actions based on data + // stalls so send the message directly to ConnectivityDiagnosticsHandler and avoid + // the cost of going through two handlers. + mConnectivityDiagnosticsHandler.sendMessage(msg); + } + + private boolean hasDataStallDetectionMethod(DataStallReportParcelable p, int detectionMethod) { + return (p.detectionMethod & detectionMethod) != 0; + } + + private boolean networkRequiresPrivateDnsValidation(NetworkAgentInfo nai) { + return isPrivateDnsValidationRequired(nai.networkCapabilities); + } + + private void handleFreshlyValidatedNetwork(NetworkAgentInfo nai) { + if (nai == null) return; + // If the Private DNS mode is opportunistic, reprogram the DNS servers + // in order to restart a validation pass from within netd. + final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig(); + if (cfg.useTls && TextUtils.isEmpty(cfg.hostname)) { + updateDnses(nai.linkProperties, null, nai.network.getNetId()); + } + } + + private void handlePrivateDnsSettingsChanged() { + final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig(); + + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + handlePerNetworkPrivateDnsConfig(nai, cfg); + if (networkRequiresPrivateDnsValidation(nai)) { + handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties)); + } + } + } + + private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) { + // Private DNS only ever applies to networks that might provide + // Internet access and therefore also require validation. + if (!networkRequiresPrivateDnsValidation(nai)) return; + + // Notify the NetworkAgentInfo/NetworkMonitor in case NetworkMonitor needs to cancel or + // schedule DNS resolutions. If a DNS resolution is required the + // result will be sent back to us. + nai.networkMonitor().notifyPrivateDnsChanged(cfg.toParcel()); + + // With Private DNS bypass support, we can proceed to update the + // Private DNS config immediately, even if we're in strict mode + // and have not yet resolved the provider name into a set of IPs. + updatePrivateDns(nai, cfg); + } + + private void updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) { + mDnsManager.updatePrivateDns(nai.network, newCfg); + updateDnses(nai.linkProperties, null, nai.network.getNetId()); + } + + private void handlePrivateDnsValidationUpdate(PrivateDnsValidationUpdate update) { + NetworkAgentInfo nai = getNetworkAgentInfoForNetId(update.netId); + if (nai == null) { + return; + } + mDnsManager.updatePrivateDnsValidation(update); + handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties)); + } + + private void handleNat64PrefixEvent(int netId, int operation, String prefixAddress, + int prefixLength) { + NetworkAgentInfo nai = mNetworkForNetId.get(netId); + if (nai == null) return; + + log(String.format("NAT64 prefix changed on netId %d: operation=%d, %s/%d", + netId, operation, prefixAddress, prefixLength)); + + IpPrefix prefix = null; + if (operation == IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED) { + try { + prefix = new IpPrefix(InetAddresses.parseNumericAddress(prefixAddress), + prefixLength); + } catch (IllegalArgumentException e) { + loge("Invalid NAT64 prefix " + prefixAddress + "/" + prefixLength); + return; + } + } + + nai.clatd.setNat64PrefixFromDns(prefix); + handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties)); + } + + private void handleCapportApiDataUpdate(@NonNull final NetworkAgentInfo nai, + @Nullable final CaptivePortalData data) { + nai.capportApiData = data; + // CaptivePortalData will be merged into LinkProperties from NetworkAgentInfo + handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties)); + } + + /** + * Updates the inactivity state from the network requests inside the NAI. + * @param nai the agent info to update + * @param now the timestamp of the event causing this update + * @return whether the network was inactive as a result of this update + */ + private boolean updateInactivityState(@NonNull final NetworkAgentInfo nai, final long now) { + // 1. Update the inactivity timer. If it's changed, reschedule or cancel the alarm. + // 2. If the network was inactive and there are now requests, unset inactive. + // 3. If this network is unneeded (which implies it is not lingering), and there is at least + // one lingered request, set inactive. + nai.updateInactivityTimer(); + if (nai.isInactive() && nai.numForegroundNetworkRequests() > 0) { + if (DBG) log("Unsetting inactive " + nai.toShortString()); + nai.unsetInactive(); + logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER); + } else if (unneeded(nai, UnneededFor.LINGER) && nai.getInactivityExpiry() > 0) { + if (DBG) { + final int lingerTime = (int) (nai.getInactivityExpiry() - now); + log("Setting inactive " + nai.toShortString() + " for " + lingerTime + "ms"); + } + nai.setInactive(); + logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER); + return true; + } + return false; + } + + private void handleNetworkAgentRegistered(Message msg) { + final NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj; + if (!mNetworkAgentInfos.contains(nai)) { + return; + } + + if (msg.arg1 == NetworkAgentInfo.ARG_AGENT_SUCCESS) { + if (VDBG) log("NetworkAgent registered"); + } else { + loge("Error connecting NetworkAgent"); + mNetworkAgentInfos.remove(nai); + if (nai != null) { + final boolean wasDefault = isDefaultNetwork(nai); + synchronized (mNetworkForNetId) { + mNetworkForNetId.remove(nai.network.getNetId()); + } + mNetIdManager.releaseNetId(nai.network.getNetId()); + // Just in case. + mLegacyTypeTracker.remove(nai, wasDefault); + } + } + } + + private void handleNetworkAgentDisconnected(Message msg) { + NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj; + if (mNetworkAgentInfos.contains(nai)) { + disconnectAndDestroyNetwork(nai); + } + } + + // Destroys a network, remove references to it from the internal state managed by + // ConnectivityService, free its interfaces and clean up. + // Must be called on the Handler thread. + private void disconnectAndDestroyNetwork(NetworkAgentInfo nai) { + ensureRunningOnConnectivityServiceThread(); + if (DBG) { + log(nai.toShortString() + " disconnected, was satisfying " + nai.numNetworkRequests()); + } + // Clear all notifications of this network. + mNotifier.clearNotification(nai.network.getNetId()); + // A network agent has disconnected. + // TODO - if we move the logic to the network agent (have them disconnect + // because they lost all their requests or because their score isn't good) + // then they would disconnect organically, report their new state and then + // disconnect the channel. + if (nai.networkInfo.isConnected()) { + nai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, + null, null); + } + final boolean wasDefault = isDefaultNetwork(nai); + if (wasDefault) { + mDefaultInetConditionPublished = 0; + } + notifyIfacesChangedForNetworkStats(); + // TODO - we shouldn't send CALLBACK_LOST to requests that can be satisfied + // by other networks that are already connected. Perhaps that can be done by + // sending all CALLBACK_LOST messages (for requests, not listens) at the end + // of rematchAllNetworksAndRequests + notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST); + mKeepaliveTracker.handleStopAllKeepalives(nai, SocketKeepalive.ERROR_INVALID_NETWORK); + + mQosCallbackTracker.handleNetworkReleased(nai.network); + for (String iface : nai.linkProperties.getAllInterfaceNames()) { + // Disable wakeup packet monitoring for each interface. + wakeupModifyInterface(iface, nai.networkCapabilities, false); + } + nai.networkMonitor().notifyNetworkDisconnected(); + mNetworkAgentInfos.remove(nai); + nai.clatd.update(); + synchronized (mNetworkForNetId) { + // Remove the NetworkAgent, but don't mark the netId as + // available until we've told netd to delete it below. + mNetworkForNetId.remove(nai.network.getNetId()); + } + propagateUnderlyingNetworkCapabilities(nai.network); + // Remove all previously satisfied requests. + for (int i = 0; i < nai.numNetworkRequests(); i++) { + final NetworkRequest request = nai.requestAt(i); + final NetworkRequestInfo nri = mNetworkRequests.get(request); + final NetworkAgentInfo currentNetwork = nri.getSatisfier(); + if (currentNetwork != null + && currentNetwork.network.getNetId() == nai.network.getNetId()) { + // uid rules for this network will be removed in destroyNativeNetwork(nai). + // TODO : setting the satisfier is in fact the job of the rematch. Teach the + // rematch not to keep disconnected agents instead of setting it here ; this + // will also allow removing updating the offers below. + nri.setSatisfier(null, null); + for (final NetworkOfferInfo noi : mNetworkOffers) { + informOffer(nri, noi.offer, mNetworkRanker); + } + + if (mDefaultRequest == nri) { + // TODO : make battery stats aware that since 2013 multiple interfaces may be + // active at the same time. For now keep calling this with the default + // network, because while incorrect this is the closest to the old (also + // incorrect) behavior. + mNetworkActivityTracker.updateDataActivityTracking( + null /* newNetwork */, nai); + ensureNetworkTransitionWakelock(nai.toShortString()); + } + } + } + nai.clearInactivityState(); + // TODO: mLegacyTypeTracker.remove seems redundant given there's a full rematch right after. + // Currently, deleting it breaks tests that check for the default network disconnecting. + // Find out why, fix the rematch code, and delete this. + mLegacyTypeTracker.remove(nai, wasDefault); + rematchAllNetworksAndRequests(); + mLingerMonitor.noteDisconnect(nai); + + // Immediate teardown. + if (nai.teardownDelayMs == 0) { + destroyNetwork(nai); + return; + } + + // Delayed teardown. + try { + mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM); + } catch (RemoteException e) { + Log.d(TAG, "Error marking network restricted during teardown: " + e); + } + mHandler.postDelayed(() -> destroyNetwork(nai), nai.teardownDelayMs); + } + + private void destroyNetwork(NetworkAgentInfo nai) { + if (nai.created) { + // Tell netd to clean up the configuration for this network + // (routing rules, DNS, etc). + // This may be slow as it requires a lot of netd shelling out to ip and + // ip[6]tables to flush routes and remove the incoming packet mark rule, so do it + // after we've rematched networks with requests (which might change the default + // network or service a new request from an app), so network traffic isn't interrupted + // for an unnecessarily long time. + destroyNativeNetwork(nai); + mDnsManager.removeNetwork(nai.network); + } + mNetIdManager.releaseNetId(nai.network.getNetId()); + nai.onNetworkDestroyed(); + } + + private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) { + try { + // This should never fail. Specifying an already in use NetID will cause failure. + final NativeNetworkConfig config; + if (nai.isVPN()) { + if (getVpnType(nai) == VpnManager.TYPE_VPN_NONE) { + Log.wtf(TAG, "Unable to get VPN type from network " + nai.toShortString()); + return false; + } + config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL, + INetd.PERMISSION_NONE, + (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass), + getVpnType(nai)); + } else { + config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL, + getNetworkPermission(nai.networkCapabilities), /*secure=*/ false, + VpnManager.TYPE_VPN_NONE); + } + mNetd.networkCreate(config); + mDnsResolver.createNetworkCache(nai.network.getNetId()); + mDnsManager.updateTransportsForNetwork(nai.network.getNetId(), + nai.networkCapabilities.getTransportTypes()); + return true; + } catch (RemoteException | ServiceSpecificException e) { + loge("Error creating network " + nai.toShortString() + ": " + e.getMessage()); + return false; + } + } + + private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) { + try { + mNetd.networkDestroy(nai.network.getNetId()); + } catch (RemoteException | ServiceSpecificException e) { + loge("Exception destroying network(networkDestroy): " + e); + } + try { + mDnsResolver.destroyNetworkCache(nai.network.getNetId()); + } catch (RemoteException | ServiceSpecificException e) { + loge("Exception destroying network: " + e); + } + } + + // If this method proves to be too slow then we can maintain a separate + // pendingIntent => NetworkRequestInfo map. + // This method assumes that every non-null PendingIntent maps to exactly 1 NetworkRequestInfo. + private NetworkRequestInfo findExistingNetworkRequestInfo(PendingIntent pendingIntent) { + for (Map.Entry<NetworkRequest, NetworkRequestInfo> entry : mNetworkRequests.entrySet()) { + PendingIntent existingPendingIntent = entry.getValue().mPendingIntent; + if (existingPendingIntent != null && + existingPendingIntent.intentFilterEquals(pendingIntent)) { + return entry.getValue(); + } + } + return null; + } + + private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) { + final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj); + // handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests. + ensureNotMultilayerRequest(nri, "handleRegisterNetworkRequestWithIntent"); + final NetworkRequestInfo existingRequest = + findExistingNetworkRequestInfo(nri.mPendingIntent); + if (existingRequest != null) { // remove the existing request. + if (DBG) { + log("Replacing " + existingRequest.mRequests.get(0) + " with " + + nri.mRequests.get(0) + " because their intents matched."); + } + handleReleaseNetworkRequest(existingRequest.mRequests.get(0), mDeps.getCallingUid(), + /* callOnUnavailable */ false); + } + handleRegisterNetworkRequest(nri); + } + + private void handleRegisterNetworkRequest(@NonNull final NetworkRequestInfo nri) { + handleRegisterNetworkRequests(Collections.singleton(nri)); + } + + private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) { + ensureRunningOnConnectivityServiceThread(); + for (final NetworkRequestInfo nri : nris) { + mNetworkRequestInfoLogs.log("REGISTER " + nri); + for (final NetworkRequest req : nri.mRequests) { + mNetworkRequests.put(req, nri); + // TODO: Consider update signal strength for other types. + if (req.isListen()) { + for (final NetworkAgentInfo network : mNetworkAgentInfos) { + if (req.networkCapabilities.hasSignalStrength() + && network.satisfiesImmutableCapabilitiesOf(req)) { + updateSignalStrengthThresholds(network, "REGISTER", req); + } + } + } + } + // If this NRI has a satisfier already, it is replacing an older request that + // has been removed. Track it. + final NetworkRequest activeRequest = nri.getActiveRequest(); + if (null != activeRequest) { + // If there is an active request, then for sure there is a satisfier. + nri.getSatisfier().addRequest(activeRequest); + } + } + + rematchAllNetworksAndRequests(); + + // Requests that have not been matched to a network will not have been sent to the + // providers, because the old satisfier and the new satisfier are the same (null in this + // case). Send these requests to the providers. + for (final NetworkRequestInfo nri : nris) { + for (final NetworkOfferInfo noi : mNetworkOffers) { + informOffer(nri, noi.offer, mNetworkRanker); + } + } + } + + private void handleReleaseNetworkRequestWithIntent(@NonNull final PendingIntent pendingIntent, + final int callingUid) { + final NetworkRequestInfo nri = findExistingNetworkRequestInfo(pendingIntent); + if (nri != null) { + // handleReleaseNetworkRequestWithIntent() paths don't apply to multilayer requests. + ensureNotMultilayerRequest(nri, "handleReleaseNetworkRequestWithIntent"); + handleReleaseNetworkRequest( + nri.mRequests.get(0), + callingUid, + /* callOnUnavailable */ false); + } + } + + // Determines whether the network is the best (or could become the best, if it validated), for + // none of a particular type of NetworkRequests. The type of NetworkRequests considered depends + // on the value of reason: + // + // - UnneededFor.TEARDOWN: non-listen NetworkRequests. If a network is unneeded for this reason, + // then it should be torn down. + // - UnneededFor.LINGER: foreground NetworkRequests. If a network is unneeded for this reason, + // then it should be lingered. + private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) { + ensureRunningOnConnectivityServiceThread(); + + if (!nai.everConnected || nai.isVPN() || nai.isInactive() + || nai.getScore().getKeepConnectedReason() != NetworkScore.KEEP_CONNECTED_NONE) { + return false; + } + + final int numRequests; + switch (reason) { + case TEARDOWN: + numRequests = nai.numRequestNetworkRequests(); + break; + case LINGER: + numRequests = nai.numForegroundNetworkRequests(); + break; + default: + Log.wtf(TAG, "Invalid reason. Cannot happen."); + return true; + } + + if (numRequests > 0) return false; + + for (NetworkRequestInfo nri : mNetworkRequests.values()) { + if (reason == UnneededFor.LINGER + && !nri.isMultilayerRequest() + && nri.mRequests.get(0).isBackgroundRequest()) { + // Background requests don't affect lingering. + continue; + } + + if (isNetworkPotentialSatisfier(nai, nri)) { + return false; + } + } + return true; + } + + private boolean isNetworkPotentialSatisfier( + @NonNull final NetworkAgentInfo candidate, @NonNull final NetworkRequestInfo nri) { + // listen requests won't keep up a network satisfying it. If this is not a multilayer + // request, return immediately. For multilayer requests, check to see if any of the + // multilayer requests may have a potential satisfier. + if (!nri.isMultilayerRequest() && (nri.mRequests.get(0).isListen() + || nri.mRequests.get(0).isListenForBest())) { + return false; + } + for (final NetworkRequest req : nri.mRequests) { + // This multilayer listen request is satisfied therefore no further requests need to be + // evaluated deeming this network not a potential satisfier. + if ((req.isListen() || req.isListenForBest()) && nri.getActiveRequest() == req) { + return false; + } + // As non-multilayer listen requests have already returned, the below would only happen + // for a multilayer request therefore continue to the next request if available. + if (req.isListen() || req.isListenForBest()) { + continue; + } + // If this Network is already the highest scoring Network for a request, or if + // there is hope for it to become one if it validated, then it is needed. + if (candidate.satisfies(req)) { + // As soon as a network is found that satisfies a request, return. Specifically for + // multilayer requests, returning as soon as a NetworkAgentInfo satisfies a request + // is important so as to not evaluate lower priority requests further in + // nri.mRequests. + final NetworkAgentInfo champion = req.equals(nri.getActiveRequest()) + ? nri.getSatisfier() : null; + // Note that this catches two important cases: + // 1. Unvalidated cellular will not be reaped when unvalidated WiFi + // is currently satisfying the request. This is desirable when + // cellular ends up validating but WiFi does not. + // 2. Unvalidated WiFi will not be reaped when validated cellular + // is currently satisfying the request. This is desirable when + // WiFi ends up validating and out scoring cellular. + return mNetworkRanker.mightBeat(req, champion, candidate.getValidatedScoreable()); + } + } + + return false; + } + + private NetworkRequestInfo getNriForAppRequest( + NetworkRequest request, int callingUid, String requestedOperation) { + // Looking up the app passed param request in mRequests isn't possible since it may return + // null for a request managed by a per-app default. Therefore use getNriForAppRequest() to + // do the lookup since that will also find per-app default managed requests. + // Additionally, this lookup needs to be relatively fast (hence the lookup optimization) + // to avoid potential race conditions when validating a package->uid mapping when sending + // the callback on the very low-chance that an application shuts down prior to the callback + // being sent. + final NetworkRequestInfo nri = mNetworkRequests.get(request) != null + ? mNetworkRequests.get(request) : getNriForAppRequest(request); + + if (nri != null) { + if (Process.SYSTEM_UID != callingUid && nri.mUid != callingUid) { + log(String.format("UID %d attempted to %s for unowned request %s", + callingUid, requestedOperation, nri)); + return null; + } + } + + return nri; + } + + private void ensureNotMultilayerRequest(@NonNull final NetworkRequestInfo nri, + final String callingMethod) { + if (nri.isMultilayerRequest()) { + throw new IllegalStateException( + callingMethod + " does not support multilayer requests."); + } + } + + private void handleTimedOutNetworkRequest(@NonNull final NetworkRequestInfo nri) { + ensureRunningOnConnectivityServiceThread(); + // handleTimedOutNetworkRequest() is part of the requestNetwork() flow which works off of a + // single NetworkRequest and thus does not apply to multilayer requests. + ensureNotMultilayerRequest(nri, "handleTimedOutNetworkRequest"); + if (mNetworkRequests.get(nri.mRequests.get(0)) == null) { + return; + } + if (nri.isBeingSatisfied()) { + return; + } + if (VDBG || (DBG && nri.mRequests.get(0).isRequest())) { + log("releasing " + nri.mRequests.get(0) + " (timeout)"); + } + handleRemoveNetworkRequest(nri); + callCallbackForRequest( + nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0); + } + + private void handleReleaseNetworkRequest(@NonNull final NetworkRequest request, + final int callingUid, + final boolean callOnUnavailable) { + final NetworkRequestInfo nri = + getNriForAppRequest(request, callingUid, "release NetworkRequest"); + if (nri == null) { + return; + } + if (VDBG || (DBG && request.isRequest())) { + log("releasing " + request + " (release request)"); + } + handleRemoveNetworkRequest(nri); + if (callOnUnavailable) { + callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0); + } + } + + private void handleRemoveNetworkRequest(@NonNull final NetworkRequestInfo nri) { + ensureRunningOnConnectivityServiceThread(); + nri.unlinkDeathRecipient(); + for (final NetworkRequest req : nri.mRequests) { + mNetworkRequests.remove(req); + if (req.isListen()) { + removeListenRequestFromNetworks(req); + } + } + if (mDefaultNetworkRequests.remove(nri)) { + // If this request was one of the defaults, then the UID rules need to be updated + // WARNING : if the app(s) for which this network request is the default are doing + // traffic, this will kill their connected sockets, even if an equivalent request + // is going to be reinstated right away ; unconnected traffic will go on the default + // until the new default is set, which will happen very soon. + // TODO : The only way out of this is to diff old defaults and new defaults, and only + // remove ranges for those requests that won't have a replacement + final NetworkAgentInfo satisfier = nri.getSatisfier(); + if (null != satisfier) { + try { + mNetd.networkRemoveUidRanges(satisfier.network.getNetId(), + toUidRangeStableParcels(nri.getUids())); + } catch (RemoteException e) { + loge("Exception setting network preference default network", e); + } + } + } + nri.decrementRequestCount(); + mNetworkRequestInfoLogs.log("RELEASE " + nri); + + if (null != nri.getActiveRequest()) { + if (!nri.getActiveRequest().isListen()) { + removeSatisfiedNetworkRequestFromNetwork(nri); + } else { + nri.setSatisfier(null, null); + } + } + + // For all outstanding offers, cancel any of the layers of this NRI that used to be + // needed for this offer. + for (final NetworkOfferInfo noi : mNetworkOffers) { + for (final NetworkRequest req : nri.mRequests) { + if (req.isRequest() && noi.offer.neededFor(req)) { + noi.offer.onNetworkUnneeded(req); + } + } + } + } + + private void handleRemoveNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) { + for (final NetworkRequestInfo nri : nris) { + if (mDefaultRequest == nri) { + // Make sure we never remove the default request. + continue; + } + handleRemoveNetworkRequest(nri); + } + } + + private void removeListenRequestFromNetworks(@NonNull final NetworkRequest req) { + // listens don't have a singular affected Network. Check all networks to see + // if this listen request applies and remove it. + for (final NetworkAgentInfo nai : mNetworkAgentInfos) { + nai.removeRequest(req.requestId); + if (req.networkCapabilities.hasSignalStrength() + && nai.satisfiesImmutableCapabilitiesOf(req)) { + updateSignalStrengthThresholds(nai, "RELEASE", req); + } + } + } + + /** + * Remove a NetworkRequestInfo's satisfied request from its 'satisfier' (NetworkAgentInfo) and + * manage the necessary upkeep (linger, teardown networks, etc.) when doing so. + * @param nri the NetworkRequestInfo to disassociate from its current NetworkAgentInfo + */ + private void removeSatisfiedNetworkRequestFromNetwork(@NonNull final NetworkRequestInfo nri) { + boolean wasKept = false; + final NetworkAgentInfo nai = nri.getSatisfier(); + if (nai != null) { + final int requestLegacyType = nri.getActiveRequest().legacyType; + final boolean wasBackgroundNetwork = nai.isBackgroundNetwork(); + nai.removeRequest(nri.getActiveRequest().requestId); + if (VDBG || DDBG) { + log(" Removing from current network " + nai.toShortString() + + ", leaving " + nai.numNetworkRequests() + " requests."); + } + // If there are still lingered requests on this network, don't tear it down, + // but resume lingering instead. + final long now = SystemClock.elapsedRealtime(); + if (updateInactivityState(nai, now)) { + notifyNetworkLosing(nai, now); + } + if (unneeded(nai, UnneededFor.TEARDOWN)) { + if (DBG) log("no live requests for " + nai.toShortString() + "; disconnecting"); + teardownUnneededNetwork(nai); + } else { + wasKept = true; + } + nri.setSatisfier(null, null); + if (!wasBackgroundNetwork && nai.isBackgroundNetwork()) { + // Went from foreground to background. + updateCapabilitiesForNetwork(nai); + } + + // Maintain the illusion. When this request arrived, we might have pretended + // that a network connected to serve it, even though the network was already + // connected. Now that this request has gone away, we might have to pretend + // that the network disconnected. LegacyTypeTracker will generate that + // phantom disconnect for this type. + if (requestLegacyType != TYPE_NONE) { + boolean doRemove = true; + if (wasKept) { + // check if any of the remaining requests for this network are for the + // same legacy type - if so, don't remove the nai + for (int i = 0; i < nai.numNetworkRequests(); i++) { + NetworkRequest otherRequest = nai.requestAt(i); + if (otherRequest.legacyType == requestLegacyType + && otherRequest.isRequest()) { + if (DBG) log(" still have other legacy request - leaving"); + doRemove = false; + } + } + } + + if (doRemove) { + mLegacyTypeTracker.remove(requestLegacyType, nai, false); + } + } + } + } + + private PerUidCounter getRequestCounter(NetworkRequestInfo nri) { + return checkAnyPermissionOf( + nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) + ? mSystemNetworkRequestCounter : mNetworkRequestCounter; + } + + @Override + public void setAcceptUnvalidated(Network network, boolean accept, boolean always) { + enforceNetworkStackSettingsOrSetup(); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_UNVALIDATED, + encodeBool(accept), encodeBool(always), network)); + } + + @Override + public void setAcceptPartialConnectivity(Network network, boolean accept, boolean always) { + enforceNetworkStackSettingsOrSetup(); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY, + encodeBool(accept), encodeBool(always), network)); + } + + @Override + public void setAvoidUnvalidated(Network network) { + enforceNetworkStackSettingsOrSetup(); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_AVOID_UNVALIDATED, network)); + } + + private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) { + if (DBG) log("handleSetAcceptUnvalidated network=" + network + + " accept=" + accept + " always=" + always); + + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) { + // Nothing to do. + return; + } + + if (nai.everValidated) { + // The network validated while the dialog box was up. Take no action. + return; + } + + if (!nai.networkAgentConfig.explicitlySelected) { + Log.wtf(TAG, "BUG: setAcceptUnvalidated non non-explicitly selected network"); + } + + if (accept != nai.networkAgentConfig.acceptUnvalidated) { + nai.networkAgentConfig.acceptUnvalidated = accept; + // If network becomes partial connectivity and user already accepted to use this + // network, we should respect the user's option and don't need to popup the + // PARTIAL_CONNECTIVITY notification to user again. + nai.networkAgentConfig.acceptPartialConnectivity = accept; + nai.updateScoreForNetworkAgentUpdate(); + rematchAllNetworksAndRequests(); + } + + if (always) { + nai.onSaveAcceptUnvalidated(accept); + } + + if (!accept) { + // Tell the NetworkAgent to not automatically reconnect to the network. + nai.onPreventAutomaticReconnect(); + // Teardown the network. + teardownUnneededNetwork(nai); + } + + } + + private void handleSetAcceptPartialConnectivity(Network network, boolean accept, + boolean always) { + if (DBG) { + log("handleSetAcceptPartialConnectivity network=" + network + " accept=" + accept + + " always=" + always); + } + + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) { + // Nothing to do. + return; + } + + if (nai.lastValidated) { + // The network validated while the dialog box was up. Take no action. + return; + } + + if (accept != nai.networkAgentConfig.acceptPartialConnectivity) { + nai.networkAgentConfig.acceptPartialConnectivity = accept; + } + + // TODO: Use the current design or save the user choice into IpMemoryStore. + if (always) { + nai.onSaveAcceptUnvalidated(accept); + } + + if (!accept) { + // Tell the NetworkAgent to not automatically reconnect to the network. + nai.onPreventAutomaticReconnect(); + // Tear down the network. + teardownUnneededNetwork(nai); + } else { + // Inform NetworkMonitor that partial connectivity is acceptable. This will likely + // result in a partial connectivity result which will be processed by + // maybeHandleNetworkMonitorMessage. + // + // TODO: NetworkMonitor does not refer to the "never ask again" bit. The bit is stored + // per network. Therefore, NetworkMonitor may still do https probe. + nai.networkMonitor().setAcceptPartialConnectivity(); + } + } + + private void handleSetAvoidUnvalidated(Network network) { + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null || nai.lastValidated) { + // Nothing to do. The network either disconnected or revalidated. + return; + } + if (!nai.avoidUnvalidated) { + nai.avoidUnvalidated = true; + nai.updateScoreForNetworkAgentUpdate(); + rematchAllNetworksAndRequests(); + } + } + + private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) { + if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network); + mHandler.sendMessageDelayed( + mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network), + PROMPT_UNVALIDATED_DELAY_MS); + } + + @Override + public void startCaptivePortalApp(Network network) { + enforceNetworkStackOrSettingsPermission(); + mHandler.post(() -> { + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) return; + if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return; + nai.networkMonitor().launchCaptivePortalApp(); + }); + } + + /** + * NetworkStack endpoint to start the captive portal app. The NetworkStack needs to use this + * endpoint as it does not have INTERACT_ACROSS_USERS_FULL itself. + * @param network Network on which the captive portal was detected. + * @param appExtras Bundle to use as intent extras for the captive portal application. + * Must be treated as opaque to avoid preventing the captive portal app to + * update its arguments. + */ + @Override + public void startCaptivePortalAppInternal(Network network, Bundle appExtras) { + mContext.enforceCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + "ConnectivityService"); + + final Intent appIntent = new Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN); + appIntent.putExtras(appExtras); + appIntent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL, + new CaptivePortal(new CaptivePortalImpl(network).asBinder())); + appIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); + + final long token = Binder.clearCallingIdentity(); + try { + mContext.startActivityAsUser(appIntent, UserHandle.CURRENT); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private class CaptivePortalImpl extends ICaptivePortal.Stub { + private final Network mNetwork; + + private CaptivePortalImpl(Network network) { + mNetwork = network; + } + + @Override + public void appResponse(final int response) { + if (response == CaptivePortal.APP_RETURN_WANTED_AS_IS) { + enforceSettingsPermission(); + } + + final NetworkMonitorManager nm = getNetworkMonitorManager(mNetwork); + if (nm == null) return; + nm.notifyCaptivePortalAppFinished(response); + } + + @Override + public void appRequest(final int request) { + final NetworkMonitorManager nm = getNetworkMonitorManager(mNetwork); + if (nm == null) return; + + if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) { + checkNetworkStackPermission(); + nm.forceReevaluation(mDeps.getCallingUid()); + } + } + + @Nullable + private NetworkMonitorManager getNetworkMonitorManager(final Network network) { + // getNetworkAgentInfoForNetwork is thread-safe + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) return null; + + // nai.networkMonitor() is thread-safe + return nai.networkMonitor(); + } + } + + public boolean avoidBadWifi() { + return mMultinetworkPolicyTracker.getAvoidBadWifi(); + } + + /** + * Return whether the device should maintain continuous, working connectivity by switching away + * from WiFi networks having no connectivity. + * @see MultinetworkPolicyTracker#getAvoidBadWifi() + */ + public boolean shouldAvoidBadWifi() { + if (!checkNetworkStackPermission()) { + throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission"); + } + return avoidBadWifi(); + } + + private void updateAvoidBadWifi() { + for (final NetworkAgentInfo nai : mNetworkAgentInfos) { + nai.updateScoreForNetworkAgentUpdate(); + } + rematchAllNetworksAndRequests(); + } + + // TODO: Evaluate whether this is of interest to other consumers of + // MultinetworkPolicyTracker and worth moving out of here. + private void dumpAvoidBadWifiSettings(IndentingPrintWriter pw) { + final boolean configRestrict = mMultinetworkPolicyTracker.configRestrictsAvoidBadWifi(); + if (!configRestrict) { + pw.println("Bad Wi-Fi avoidance: unrestricted"); + return; + } + + pw.println("Bad Wi-Fi avoidance: " + avoidBadWifi()); + pw.increaseIndent(); + pw.println("Config restrict: " + configRestrict); + + final String value = mMultinetworkPolicyTracker.getAvoidBadWifiSetting(); + String description; + // Can't use a switch statement because strings are legal case labels, but null is not. + if ("0".equals(value)) { + description = "get stuck"; + } else if (value == null) { + description = "prompt"; + } else if ("1".equals(value)) { + description = "avoid"; + } else { + description = value + " (?)"; + } + pw.println("User setting: " + description); + pw.println("Network overrides:"); + pw.increaseIndent(); + for (NetworkAgentInfo nai : networksSortedById()) { + if (nai.avoidUnvalidated) { + pw.println(nai.toShortString()); + } + } + pw.decreaseIndent(); + pw.decreaseIndent(); + } + + // TODO: This method is copied from TetheringNotificationUpdater. Should have a utility class to + // unify the method. + private static @NonNull 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"; + } + + private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) { + final String action; + final boolean highPriority; + switch (type) { + case NO_INTERNET: + action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED; + // High priority because it is only displayed for explicitly selected networks. + highPriority = true; + break; + case PRIVATE_DNS_BROKEN: + action = Settings.ACTION_WIRELESS_SETTINGS; + // High priority because we should let user know why there is no internet. + highPriority = true; + break; + case LOST_INTERNET: + action = ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION; + // High priority because it could help the user avoid unexpected data usage. + highPriority = true; + break; + case PARTIAL_CONNECTIVITY: + action = ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY; + // Don't bother the user with a high-priority notification if the network was not + // explicitly selected by the user. + highPriority = nai.networkAgentConfig.explicitlySelected; + break; + default: + Log.wtf(TAG, "Unknown notification type " + type); + return; + } + + Intent intent = new Intent(action); + if (type != NotificationType.PRIVATE_DNS_BROKEN) { + intent.putExtra(ConnectivityManager.EXTRA_NETWORK, nai.network); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Some OEMs have their own Settings package. Thus, need to get the current using + // Settings package name instead of just use default name "com.android.settings". + final String settingsPkgName = getSettingsPackageName(mContext.getPackageManager()); + intent.setClassName(settingsPkgName, + settingsPkgName + ".wifi.WifiNoInternetDialog"); + } + + PendingIntent pendingIntent = PendingIntent.getActivity( + mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + intent, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + mNotifier.showNotification( + nai.network.getNetId(), type, nai, null, pendingIntent, highPriority); + } + + private boolean shouldPromptUnvalidated(NetworkAgentInfo nai) { + // Don't prompt if the network is validated, and don't prompt on captive portals + // because we're already prompting the user to sign in. + if (nai.everValidated || nai.everCaptivePortalDetected) { + return false; + } + + // If a network has partial connectivity, always prompt unless the user has already accepted + // partial connectivity and selected don't ask again. This ensures that if the device + // automatically connects to a network that has partial Internet access, the user will + // always be able to use it, either because they've already chosen "don't ask again" or + // because we have prompt them. + if (nai.partialConnectivity && !nai.networkAgentConfig.acceptPartialConnectivity) { + return true; + } + + // If a network has no Internet access, only prompt if the network was explicitly selected + // and if the user has not already told us to use the network regardless of whether it + // validated or not. + if (nai.networkAgentConfig.explicitlySelected + && !nai.networkAgentConfig.acceptUnvalidated) { + return true; + } + + return false; + } + + private void handlePromptUnvalidated(Network network) { + if (VDBG || DDBG) log("handlePromptUnvalidated " + network); + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + + if (nai == null || !shouldPromptUnvalidated(nai)) { + return; + } + + // Stop automatically reconnecting to this network in the future. Automatically connecting + // to a network that provides no or limited connectivity is not useful, because the user + // cannot use that network except through the notification shown by this method, and the + // notification is only shown if the network is explicitly selected by the user. + nai.onPreventAutomaticReconnect(); + + // TODO: Evaluate if it's needed to wait 8 seconds for triggering notification when + // NetworkMonitor detects the network is partial connectivity. Need to change the design to + // popup the notification immediately when the network is partial connectivity. + if (nai.partialConnectivity) { + showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY); + } else { + showNetworkNotification(nai, NotificationType.NO_INTERNET); + } + } + + private void handleNetworkUnvalidated(NetworkAgentInfo nai) { + NetworkCapabilities nc = nai.networkCapabilities; + if (DBG) log("handleNetworkUnvalidated " + nai.toShortString() + " cap=" + nc); + + if (!nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return; + } + + if (mMultinetworkPolicyTracker.shouldNotifyWifiUnvalidated()) { + showNetworkNotification(nai, NotificationType.LOST_INTERNET); + } + } + + @Override + public int getMultipathPreference(Network network) { + enforceAccessPermission(); + + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai != null && nai.networkCapabilities + .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { + return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED; + } + + final NetworkPolicyManager netPolicyManager = + mContext.getSystemService(NetworkPolicyManager.class); + + final long token = Binder.clearCallingIdentity(); + final int networkPreference; + try { + networkPreference = netPolicyManager.getMultipathPreference(network); + } finally { + Binder.restoreCallingIdentity(token); + } + if (networkPreference != 0) { + return networkPreference; + } + return mMultinetworkPolicyTracker.getMeteredMultipathPreference(); + } + + @Override + public NetworkRequest getDefaultRequest() { + return mDefaultRequest.mRequests.get(0); + } + + private class InternalHandler extends Handler { + public InternalHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_EXPIRE_NET_TRANSITION_WAKELOCK: + case EVENT_CLEAR_NET_TRANSITION_WAKELOCK: { + handleReleaseNetworkTransitionWakelock(msg.what); + break; + } + case EVENT_APPLY_GLOBAL_HTTP_PROXY: { + mProxyTracker.loadDeprecatedGlobalHttpProxy(); + break; + } + case EVENT_PROXY_HAS_CHANGED: { + final Pair<Network, ProxyInfo> arg = (Pair<Network, ProxyInfo>) msg.obj; + handleApplyDefaultProxy(arg.second); + break; + } + case EVENT_REGISTER_NETWORK_PROVIDER: { + handleRegisterNetworkProvider((NetworkProviderInfo) msg.obj); + break; + } + case EVENT_UNREGISTER_NETWORK_PROVIDER: { + handleUnregisterNetworkProvider((Messenger) msg.obj); + break; + } + case EVENT_REGISTER_NETWORK_OFFER: { + handleRegisterNetworkOffer((NetworkOffer) msg.obj); + break; + } + case EVENT_UNREGISTER_NETWORK_OFFER: { + final NetworkOfferInfo offer = + findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj); + if (null != offer) { + handleUnregisterNetworkOffer(offer); + } + break; + } + case EVENT_REGISTER_NETWORK_AGENT: { + final Pair<NetworkAgentInfo, INetworkMonitor> arg = + (Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj; + handleRegisterNetworkAgent(arg.first, arg.second); + break; + } + case EVENT_REGISTER_NETWORK_REQUEST: + case EVENT_REGISTER_NETWORK_LISTENER: { + handleRegisterNetworkRequest((NetworkRequestInfo) msg.obj); + break; + } + case EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT: + case EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT: { + handleRegisterNetworkRequestWithIntent(msg); + break; + } + case EVENT_TIMEOUT_NETWORK_REQUEST: { + NetworkRequestInfo nri = (NetworkRequestInfo) msg.obj; + handleTimedOutNetworkRequest(nri); + break; + } + case EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT: { + handleReleaseNetworkRequestWithIntent((PendingIntent) msg.obj, msg.arg1); + break; + } + case EVENT_RELEASE_NETWORK_REQUEST: { + handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1, + /* callOnUnavailable */ false); + break; + } + case EVENT_SET_ACCEPT_UNVALIDATED: { + Network network = (Network) msg.obj; + handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2)); + break; + } + case EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY: { + Network network = (Network) msg.obj; + handleSetAcceptPartialConnectivity(network, toBool(msg.arg1), + toBool(msg.arg2)); + break; + } + case EVENT_SET_AVOID_UNVALIDATED: { + handleSetAvoidUnvalidated((Network) msg.obj); + break; + } + case EVENT_PROMPT_UNVALIDATED: { + handlePromptUnvalidated((Network) msg.obj); + break; + } + case EVENT_CONFIGURE_ALWAYS_ON_NETWORKS: { + handleConfigureAlwaysOnNetworks(); + break; + } + // Sent by KeepaliveTracker to process an app request on the state machine thread. + case NetworkAgent.CMD_START_SOCKET_KEEPALIVE: { + mKeepaliveTracker.handleStartKeepalive(msg); + break; + } + // Sent by KeepaliveTracker to process an app request on the state machine thread. + case NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE: { + NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj); + int slot = msg.arg1; + int reason = msg.arg2; + mKeepaliveTracker.handleStopKeepalive(nai, slot, reason); + break; + } + case EVENT_REVALIDATE_NETWORK: { + handleReportNetworkConnectivity((Network) msg.obj, msg.arg1, toBool(msg.arg2)); + break; + } + case EVENT_PRIVATE_DNS_SETTINGS_CHANGED: + handlePrivateDnsSettingsChanged(); + break; + case EVENT_PRIVATE_DNS_VALIDATION_UPDATE: + handlePrivateDnsValidationUpdate( + (PrivateDnsValidationUpdate) msg.obj); + break; + case EVENT_UID_BLOCKED_REASON_CHANGED: + handleUidBlockedReasonChanged(msg.arg1, msg.arg2); + break; + case EVENT_SET_REQUIRE_VPN_FOR_UIDS: + handleSetRequireVpnForUids(toBool(msg.arg1), (UidRange[]) msg.obj); + break; + case EVENT_SET_OEM_NETWORK_PREFERENCE: { + final Pair<OemNetworkPreferences, IOnCompleteListener> arg = + (Pair<OemNetworkPreferences, IOnCompleteListener>) msg.obj; + handleSetOemNetworkPreference(arg.first, arg.second); + break; + } + case EVENT_SET_PROFILE_NETWORK_PREFERENCE: { + final Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener> arg = + (Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener>) + msg.obj; + handleSetProfileNetworkPreference(arg.first, arg.second); + break; + } + case EVENT_REPORT_NETWORK_ACTIVITY: + mNetworkActivityTracker.handleReportNetworkActivity(); + break; + } + } + } + + @Override + @Deprecated + public int getLastTetherError(String iface) { + final TetheringManager tm = (TetheringManager) mContext.getSystemService( + Context.TETHERING_SERVICE); + return tm.getLastTetherError(iface); + } + + @Override + @Deprecated + public String[] getTetherableIfaces() { + final TetheringManager tm = (TetheringManager) mContext.getSystemService( + Context.TETHERING_SERVICE); + return tm.getTetherableIfaces(); + } + + @Override + @Deprecated + public String[] getTetheredIfaces() { + final TetheringManager tm = (TetheringManager) mContext.getSystemService( + Context.TETHERING_SERVICE); + return tm.getTetheredIfaces(); + } + + + @Override + @Deprecated + public String[] getTetheringErroredIfaces() { + final TetheringManager tm = (TetheringManager) mContext.getSystemService( + Context.TETHERING_SERVICE); + + return tm.getTetheringErroredIfaces(); + } + + @Override + @Deprecated + public String[] getTetherableUsbRegexs() { + final TetheringManager tm = (TetheringManager) mContext.getSystemService( + Context.TETHERING_SERVICE); + + return tm.getTetherableUsbRegexs(); + } + + @Override + @Deprecated + public String[] getTetherableWifiRegexs() { + final TetheringManager tm = (TetheringManager) mContext.getSystemService( + Context.TETHERING_SERVICE); + return tm.getTetherableWifiRegexs(); + } + + // Called when we lose the default network and have no replacement yet. + // This will automatically be cleared after X seconds or a new default network + // becomes CONNECTED, whichever happens first. The timer is started by the + // first caller and not restarted by subsequent callers. + private void ensureNetworkTransitionWakelock(String forWhom) { + synchronized (this) { + if (mNetTransitionWakeLock.isHeld()) { + return; + } + mNetTransitionWakeLock.acquire(); + mLastWakeLockAcquireTimestamp = SystemClock.elapsedRealtime(); + mTotalWakelockAcquisitions++; + } + mWakelockLogs.log("ACQUIRE for " + forWhom); + Message msg = mHandler.obtainMessage(EVENT_EXPIRE_NET_TRANSITION_WAKELOCK); + final int lockTimeout = mResources.get().getInteger( + R.integer.config_networkTransitionTimeout); + mHandler.sendMessageDelayed(msg, lockTimeout); + } + + // Called when we gain a new default network to release the network transition wakelock in a + // second, to allow a grace period for apps to reconnect over the new network. Pending expiry + // message is cancelled. + private void scheduleReleaseNetworkTransitionWakelock() { + synchronized (this) { + if (!mNetTransitionWakeLock.isHeld()) { + return; // expiry message released the lock first. + } + } + // Cancel self timeout on wakelock hold. + mHandler.removeMessages(EVENT_EXPIRE_NET_TRANSITION_WAKELOCK); + Message msg = mHandler.obtainMessage(EVENT_CLEAR_NET_TRANSITION_WAKELOCK); + mHandler.sendMessageDelayed(msg, 1000); + } + + // Called when either message of ensureNetworkTransitionWakelock or + // scheduleReleaseNetworkTransitionWakelock is processed. + private void handleReleaseNetworkTransitionWakelock(int eventId) { + String event = eventName(eventId); + synchronized (this) { + if (!mNetTransitionWakeLock.isHeld()) { + mWakelockLogs.log(String.format("RELEASE: already released (%s)", event)); + Log.w(TAG, "expected Net Transition WakeLock to be held"); + return; + } + mNetTransitionWakeLock.release(); + long lockDuration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp; + mTotalWakelockDurationMs += lockDuration; + mMaxWakelockDurationMs = Math.max(mMaxWakelockDurationMs, lockDuration); + mTotalWakelockReleases++; + } + mWakelockLogs.log(String.format("RELEASE (%s)", event)); + } + + // 100 percent is full good, 0 is full bad. + @Override + public void reportInetCondition(int networkType, int percentage) { + NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType); + if (nai == null) return; + reportNetworkConnectivity(nai.network, percentage > 50); + } + + @Override + public void reportNetworkConnectivity(Network network, boolean hasConnectivity) { + enforceAccessPermission(); + enforceInternetPermission(); + final int uid = mDeps.getCallingUid(); + final int connectivityInfo = encodeBool(hasConnectivity); + + // Handle ConnectivityDiagnostics event before attempting to revalidate the network. This + // forces an ordering of ConnectivityDiagnostics events in the case where hasConnectivity + // does not match the known connectivity of the network - this causes NetworkMonitor to + // revalidate the network and generate a ConnectivityDiagnostics ConnectivityReport event. + final NetworkAgentInfo nai; + if (network == null) { + nai = getDefaultNetwork(); + } else { + nai = getNetworkAgentInfoForNetwork(network); + } + if (nai != null) { + mConnectivityDiagnosticsHandler.sendMessage( + mConnectivityDiagnosticsHandler.obtainMessage( + ConnectivityDiagnosticsHandler.EVENT_NETWORK_CONNECTIVITY_REPORTED, + connectivityInfo, 0, nai)); + } + + mHandler.sendMessage( + mHandler.obtainMessage(EVENT_REVALIDATE_NETWORK, uid, connectivityInfo, network)); + } + + private void handleReportNetworkConnectivity( + Network network, int uid, boolean hasConnectivity) { + final NetworkAgentInfo nai; + if (network == null) { + nai = getDefaultNetwork(); + } else { + nai = getNetworkAgentInfoForNetwork(network); + } + if (nai == null || nai.networkInfo.getState() == NetworkInfo.State.DISCONNECTING || + nai.networkInfo.getState() == NetworkInfo.State.DISCONNECTED) { + return; + } + // Revalidate if the app report does not match our current validated state. + if (hasConnectivity == nai.lastValidated) { + return; + } + if (DBG) { + int netid = nai.network.getNetId(); + log("reportNetworkConnectivity(" + netid + ", " + hasConnectivity + ") by " + uid); + } + // Validating a network that has not yet connected could result in a call to + // rematchNetworkAndRequests() which is not meant to work on such networks. + if (!nai.everConnected) { + return; + } + final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai); + if (isNetworkWithCapabilitiesBlocked(nc, uid, false)) { + return; + } + nai.networkMonitor().forceReevaluation(uid); + } + + // TODO: call into netd. + private boolean queryUserAccess(int uid, Network network) { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) return false; + + // Any UID can use its default network. + if (nai == getDefaultNetworkForUid(uid)) return true; + + // Privileged apps can use any network. + if (mPermissionMonitor.hasRestrictedNetworksPermission(uid)) { + return true; + } + + // An unprivileged UID can use a VPN iff the VPN applies to it. + if (nai.isVPN()) { + return nai.networkCapabilities.appliesToUid(uid); + } + + // An unprivileged UID can bypass the VPN that applies to it only if it can protect its + // sockets, i.e., if it is the owner. + final NetworkAgentInfo vpn = getVpnForUid(uid); + if (vpn != null && !vpn.networkAgentConfig.allowBypass + && uid != vpn.networkCapabilities.getOwnerUid()) { + return false; + } + + // The UID's permission must be at least sufficient for the network. Since the restricted + // permission was already checked above, that just leaves background networks. + if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_FOREGROUND)) { + return mPermissionMonitor.hasUseBackgroundNetworksPermission(uid); + } + + // Unrestricted network. Anyone gets to use it. + return true; + } + + /** + * Returns information about the proxy a certain network is using. If given a null network, it + * it will return the proxy for the bound network for the caller app or the default proxy if + * none. + * + * @param network the network we want to get the proxy information for. + * @return Proxy information if a network has a proxy configured, or otherwise null. + */ + @Override + public ProxyInfo getProxyForNetwork(Network network) { + final ProxyInfo globalProxy = mProxyTracker.getGlobalProxy(); + if (globalProxy != null) return globalProxy; + if (network == null) { + // Get the network associated with the calling UID. + final Network activeNetwork = getActiveNetworkForUidInternal(mDeps.getCallingUid(), + true); + if (activeNetwork == null) { + return null; + } + return getLinkPropertiesProxyInfo(activeNetwork); + } else if (mDeps.queryUserAccess(mDeps.getCallingUid(), network, this)) { + // Don't call getLinkProperties() as it requires ACCESS_NETWORK_STATE permission, which + // caller may not have. + return getLinkPropertiesProxyInfo(network); + } + // No proxy info available if the calling UID does not have network access. + return null; + } + + + private ProxyInfo getLinkPropertiesProxyInfo(Network network) { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null) return null; + synchronized (nai) { + final ProxyInfo linkHttpProxy = nai.linkProperties.getHttpProxy(); + return linkHttpProxy == null ? null : new ProxyInfo(linkHttpProxy); + } + } + + @Override + public void setGlobalProxy(@Nullable final ProxyInfo proxyProperties) { + PermissionUtils.enforceNetworkStackPermission(mContext); + mProxyTracker.setGlobalProxy(proxyProperties); + } + + @Override + @Nullable + public ProxyInfo getGlobalProxy() { + return mProxyTracker.getGlobalProxy(); + } + + private void handleApplyDefaultProxy(ProxyInfo proxy) { + if (proxy != null && TextUtils.isEmpty(proxy.getHost()) + && Uri.EMPTY.equals(proxy.getPacFileUrl())) { + proxy = null; + } + mProxyTracker.setDefaultProxy(proxy); + } + + // If the proxy has changed from oldLp to newLp, resend proxy broadcast. This method gets called + // when any network changes proxy. + // TODO: Remove usage of broadcast extras as they are deprecated and not applicable in a + // multi-network world where an app might be bound to a non-default network. + private void updateProxy(LinkProperties newLp, LinkProperties oldLp) { + ProxyInfo newProxyInfo = newLp == null ? null : newLp.getHttpProxy(); + ProxyInfo oldProxyInfo = oldLp == null ? null : oldLp.getHttpProxy(); + + if (!ProxyTracker.proxyInfoEqual(newProxyInfo, oldProxyInfo)) { + mProxyTracker.sendProxyBroadcast(); + } + } + + private static class SettingsObserver extends ContentObserver { + final private HashMap<Uri, Integer> mUriEventMap; + final private Context mContext; + final private Handler mHandler; + + SettingsObserver(Context context, Handler handler) { + super(null); + mUriEventMap = new HashMap<>(); + mContext = context; + mHandler = handler; + } + + void observe(Uri uri, int what) { + mUriEventMap.put(uri, what); + final ContentResolver resolver = mContext.getContentResolver(); + resolver.registerContentObserver(uri, false, this); + } + + @Override + public void onChange(boolean selfChange) { + Log.wtf(TAG, "Should never be reached."); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + final Integer what = mUriEventMap.get(uri); + if (what != null) { + mHandler.obtainMessage(what).sendToTarget(); + } else { + loge("No matching event to send for URI=" + uri); + } + } + } + + private static void log(String s) { + Log.d(TAG, s); + } + + private static void logw(String s) { + Log.w(TAG, s); + } + + private static void logwtf(String s) { + Log.wtf(TAG, s); + } + + private static void logwtf(String s, Throwable t) { + Log.wtf(TAG, s, t); + } + + private static void loge(String s) { + Log.e(TAG, s); + } + + private static void loge(String s, Throwable t) { + Log.e(TAG, s, t); + } + + /** + * Return the information of all ongoing VPNs. + * + * <p>This method is used to update NetworkStatsService. + * + * <p>Must be called on the handler thread. + */ + private UnderlyingNetworkInfo[] getAllVpnInfo() { + ensureRunningOnConnectivityServiceThread(); + if (mLockdownEnabled) { + return new UnderlyingNetworkInfo[0]; + } + List<UnderlyingNetworkInfo> infoList = new ArrayList<>(); + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + UnderlyingNetworkInfo info = createVpnInfo(nai); + if (info != null) { + infoList.add(info); + } + } + return infoList.toArray(new UnderlyingNetworkInfo[infoList.size()]); + } + + /** + * @return VPN information for accounting, or null if we can't retrieve all required + * information, e.g underlying ifaces. + */ + private UnderlyingNetworkInfo createVpnInfo(NetworkAgentInfo nai) { + if (!nai.isVPN()) return null; + + Network[] underlyingNetworks = nai.declaredUnderlyingNetworks; + // see VpnService.setUnderlyingNetworks()'s javadoc about how to interpret + // the underlyingNetworks list. + if (underlyingNetworks == null) { + final NetworkAgentInfo defaultNai = getDefaultNetworkForUid( + nai.networkCapabilities.getOwnerUid()); + if (defaultNai != null) { + underlyingNetworks = new Network[] { defaultNai.network }; + } + } + + if (CollectionUtils.isEmpty(underlyingNetworks)) return null; + + List<String> interfaces = new ArrayList<>(); + for (Network network : underlyingNetworks) { + NetworkAgentInfo underlyingNai = getNetworkAgentInfoForNetwork(network); + if (underlyingNai == null) continue; + LinkProperties lp = underlyingNai.linkProperties; + for (String iface : lp.getAllInterfaceNames()) { + if (!TextUtils.isEmpty(iface)) { + interfaces.add(iface); + } + } + } + + if (interfaces.isEmpty()) return null; + + // Must be non-null or NetworkStatsService will crash. + // Cannot happen in production code because Vpn only registers the NetworkAgent after the + // tun or ipsec interface is created. + // TODO: Remove this check. + if (nai.linkProperties.getInterfaceName() == null) return null; + + return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(), + nai.linkProperties.getInterfaceName(), interfaces); + } + + // TODO This needs to be the default network that applies to the NAI. + private Network[] underlyingNetworksOrDefault(final int ownerUid, + Network[] underlyingNetworks) { + final Network defaultNetwork = getNetwork(getDefaultNetworkForUid(ownerUid)); + if (underlyingNetworks == null && defaultNetwork != null) { + // null underlying networks means to track the default. + underlyingNetworks = new Network[] { defaultNetwork }; + } + return underlyingNetworks; + } + + // Returns true iff |network| is an underlying network of |nai|. + private boolean hasUnderlyingNetwork(NetworkAgentInfo nai, Network network) { + // TODO: support more than one level of underlying networks, either via a fixed-depth search + // (e.g., 2 levels of underlying networks), or via loop detection, or.... + if (!nai.supportsUnderlyingNetworks()) return false; + final Network[] underlying = underlyingNetworksOrDefault( + nai.networkCapabilities.getOwnerUid(), nai.declaredUnderlyingNetworks); + return CollectionUtils.contains(underlying, network); + } + + /** + * Recompute the capabilities for any networks that had a specific network as underlying. + * + * When underlying networks change, such networks may have to update capabilities to reflect + * things like the metered bit, their transports, and so on. The capabilities are calculated + * immediately. This method runs on the ConnectivityService thread. + */ + private void propagateUnderlyingNetworkCapabilities(Network updatedNetwork) { + ensureRunningOnConnectivityServiceThread(); + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + if (updatedNetwork == null || hasUnderlyingNetwork(nai, updatedNetwork)) { + updateCapabilitiesForNetwork(nai); + } + } + } + + private boolean isUidBlockedByVpn(int uid, List<UidRange> blockedUidRanges) { + // Determine whether this UID is blocked because of always-on VPN lockdown. If a VPN applies + // to the UID, then the UID is not blocked because always-on VPN lockdown applies only when + // a VPN is not up. + final NetworkAgentInfo vpnNai = getVpnForUid(uid); + if (vpnNai != null && !vpnNai.networkAgentConfig.allowBypass) return false; + for (UidRange range : blockedUidRanges) { + if (range.contains(uid)) return true; + } + return false; + } + + @Override + public void setRequireVpnForUids(boolean requireVpn, UidRange[] ranges) { + enforceNetworkStackOrSettingsPermission(); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_REQUIRE_VPN_FOR_UIDS, + encodeBool(requireVpn), 0 /* arg2 */, ranges)); + } + + private void handleSetRequireVpnForUids(boolean requireVpn, UidRange[] ranges) { + if (DBG) { + Log.d(TAG, "Setting VPN " + (requireVpn ? "" : "not ") + "required for UIDs: " + + Arrays.toString(ranges)); + } + // Cannot use a Set since the list of UID ranges might contain duplicates. + final List<UidRange> newVpnBlockedUidRanges = new ArrayList(mVpnBlockedUidRanges); + for (int i = 0; i < ranges.length; i++) { + if (requireVpn) { + newVpnBlockedUidRanges.add(ranges[i]); + } else { + newVpnBlockedUidRanges.remove(ranges[i]); + } + } + + try { + mNetd.networkRejectNonSecureVpn(requireVpn, toUidRangeStableParcels(ranges)); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "setRequireVpnForUids(" + requireVpn + ", " + + Arrays.toString(ranges) + "): netd command failed: " + e); + } + + for (final NetworkAgentInfo nai : mNetworkAgentInfos) { + final boolean curMetered = nai.networkCapabilities.isMetered(); + maybeNotifyNetworkBlocked(nai, curMetered, curMetered, + mVpnBlockedUidRanges, newVpnBlockedUidRanges); + } + + mVpnBlockedUidRanges = newVpnBlockedUidRanges; + } + + @Override + public void setLegacyLockdownVpnEnabled(boolean enabled) { + enforceNetworkStackOrSettingsPermission(); + mHandler.post(() -> mLockdownEnabled = enabled); + } + + private boolean isLegacyLockdownNai(NetworkAgentInfo nai) { + return mLockdownEnabled + && getVpnType(nai) == VpnManager.TYPE_VPN_LEGACY + && nai.networkCapabilities.appliesToUid(Process.FIRST_APPLICATION_UID); + } + + private NetworkAgentInfo getLegacyLockdownNai() { + if (!mLockdownEnabled) { + return null; + } + // The legacy lockdown VPN always only applies to userId 0. + final NetworkAgentInfo nai = getVpnForUid(Process.FIRST_APPLICATION_UID); + if (nai == null || !isLegacyLockdownNai(nai)) return null; + + // The legacy lockdown VPN must always have exactly one underlying network. + // This code may run on any thread and declaredUnderlyingNetworks may change, so store it in + // a local variable. There is no need to make a copy because its contents cannot change. + final Network[] underlying = nai.declaredUnderlyingNetworks; + if (underlying == null || underlying.length != 1) { + return null; + } + + // The legacy lockdown VPN always uses the default network. + // If the VPN's underlying network is no longer the current default network, it means that + // the default network has just switched, and the VPN is about to disconnect. + // Report that the VPN is not connected, so the state of NetworkInfo objects overwritten + // by filterForLegacyLockdown will be set to CONNECTING and not CONNECTED. + final NetworkAgentInfo defaultNetwork = getDefaultNetwork(); + if (defaultNetwork == null || !defaultNetwork.network.equals(underlying[0])) { + return null; + } + + return nai; + }; + + // TODO: move all callers to filterForLegacyLockdown and delete this method. + // This likely requires making sendLegacyNetworkBroadcast take a NetworkInfo object instead of + // just a DetailedState object. + private DetailedState getLegacyLockdownState(DetailedState origState) { + if (origState != DetailedState.CONNECTED) { + return origState; + } + return (mLockdownEnabled && getLegacyLockdownNai() == null) + ? DetailedState.CONNECTING + : DetailedState.CONNECTED; + } + + private void filterForLegacyLockdown(NetworkInfo ni) { + if (!mLockdownEnabled || !ni.isConnected()) return; + // The legacy lockdown VPN replaces the state of every network in CONNECTED state with the + // state of its VPN. This is to ensure that when an underlying network connects, apps will + // not see a CONNECTIVITY_ACTION broadcast for a network in state CONNECTED until the VPN + // comes up, at which point there is a new CONNECTIVITY_ACTION broadcast for the underlying + // network, this time with a state of CONNECTED. + // + // Now that the legacy lockdown code lives in ConnectivityService, and no longer has access + // to the internal state of the Vpn object, always replace the state with CONNECTING. This + // is not too far off the truth, since an always-on VPN, when not connected, is always + // trying to reconnect. + if (getLegacyLockdownNai() == null) { + ni.setDetailedState(DetailedState.CONNECTING, "", null); + } + } + + @Override + public void setProvisioningNotificationVisible(boolean visible, int networkType, + String action) { + enforceSettingsPermission(); + if (!ConnectivityManager.isNetworkTypeValid(networkType)) { + return; + } + final long ident = Binder.clearCallingIdentity(); + try { + // Concatenate the range of types onto the range of NetIDs. + int id = NetIdManager.MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE); + mNotifier.setProvNotificationVisible(visible, id, action); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public void setAirplaneMode(boolean enable) { + enforceAirplaneModePermission(); + final long ident = Binder.clearCallingIdentity(); + try { + final ContentResolver cr = mContext.getContentResolver(); + Settings.Global.putInt(cr, Settings.Global.AIRPLANE_MODE_ON, encodeBool(enable)); + Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", enable); + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void onUserAdded(@NonNull final UserHandle user) { + mPermissionMonitor.onUserAdded(user); + if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) { + handleSetOemNetworkPreference(mOemNetworkPreferences, null); + } + } + + private void onUserRemoved(@NonNull final UserHandle user) { + mPermissionMonitor.onUserRemoved(user); + // If there was a network preference for this user, remove it. + handleSetProfileNetworkPreference(new ProfileNetworkPreferences.Preference(user, null), + null /* listener */); + if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) { + handleSetOemNetworkPreference(mOemNetworkPreferences, null); + } + } + + private void onPackageChanged(@NonNull final String packageName) { + // This is necessary in case a package is added or removed, but also when it's replaced to + // run as a new UID by its manifest rules. Also, if a separate package shares the same UID + // as one in the preferences, then it should follow the same routing as that other package, + // which means updating the rules is never to be needed in this case (whether it joins or + // leaves a UID with a preference). + if (isMappedInOemNetworkPreference(packageName)) { + handleSetOemNetworkPreference(mOemNetworkPreferences, null); + } + } + + private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ensureRunningOnConnectivityServiceThread(); + final String action = intent.getAction(); + final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); + + // User should be filled for below intents, check the existence. + if (user == null) { + Log.wtf(TAG, intent.getAction() + " broadcast without EXTRA_USER"); + return; + } + + if (Intent.ACTION_USER_ADDED.equals(action)) { + onUserAdded(user); + } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + onUserRemoved(user); + } else { + Log.wtf(TAG, "received unexpected intent: " + action); + } + } + }; + + private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + ensureRunningOnConnectivityServiceThread(); + switch (intent.getAction()) { + case Intent.ACTION_PACKAGE_ADDED: + case Intent.ACTION_PACKAGE_REMOVED: + case Intent.ACTION_PACKAGE_REPLACED: + onPackageChanged(intent.getData().getSchemeSpecificPart()); + break; + default: + Log.wtf(TAG, "received unexpected intent: " + intent.getAction()); + } + } + }; + + private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>(); + private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>(); + + private static class NetworkProviderInfo { + public final String name; + public final Messenger messenger; + private final IBinder.DeathRecipient mDeathRecipient; + public final int providerId; + + NetworkProviderInfo(String name, Messenger messenger, int providerId, + @NonNull IBinder.DeathRecipient deathRecipient) { + this.name = name; + this.messenger = messenger; + this.providerId = providerId; + mDeathRecipient = deathRecipient; + + if (mDeathRecipient == null) { + throw new AssertionError("Must pass a deathRecipient"); + } + } + + void connect(Context context, Handler handler) { + try { + messenger.getBinder().linkToDeath(mDeathRecipient, 0); + } catch (RemoteException e) { + mDeathRecipient.binderDied(); + } + } + } + + private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) { + for (int i = 0; i < requests.size(); i++) { + ensureNetworkRequestHasType(requests.get(i)); + } + } + + private void ensureNetworkRequestHasType(NetworkRequest request) { + if (request.type == NetworkRequest.Type.NONE) { + throw new IllegalArgumentException( + "All NetworkRequests in ConnectivityService must have a type"); + } + } + + /** + * Tracks info about the requester. + * Also used to notice when the calling process dies so as to self-expire + */ + @VisibleForTesting + protected class NetworkRequestInfo implements IBinder.DeathRecipient { + // The requests to be satisfied in priority order. Non-multilayer requests will only have a + // single NetworkRequest in mRequests. + final List<NetworkRequest> mRequests; + + // mSatisfier and mActiveRequest rely on one another therefore set them together. + void setSatisfier( + @Nullable final NetworkAgentInfo satisfier, + @Nullable final NetworkRequest activeRequest) { + mSatisfier = satisfier; + mActiveRequest = activeRequest; + } + + // The network currently satisfying this NRI. Only one request in an NRI can have a + // satisfier. For non-multilayer requests, only non-listen requests can have a satisfier. + @Nullable + private NetworkAgentInfo mSatisfier; + NetworkAgentInfo getSatisfier() { + return mSatisfier; + } + + // The request in mRequests assigned to a network agent. This is null if none of the + // requests in mRequests can be satisfied. This member has the constraint of only being + // accessible on the handler thread. + @Nullable + private NetworkRequest mActiveRequest; + NetworkRequest getActiveRequest() { + return mActiveRequest; + } + + final PendingIntent mPendingIntent; + boolean mPendingIntentSent; + @Nullable + final Messenger mMessenger; + + // Information about the caller that caused this object to be created. + @Nullable + private final IBinder mBinder; + final int mPid; + final int mUid; + final @NetworkCallback.Flag int mCallbackFlags; + @Nullable + final String mCallingAttributionTag; + + // Counter keeping track of this NRI. + final PerUidCounter mPerUidCounter; + + // Effective UID of this request. This is different from mUid when a privileged process + // files a request on behalf of another UID. This UID is used to determine blocked status, + // UID matching, and so on. mUid above is used for permission checks and to enforce the + // maximum limit of registered callbacks per UID. + final int mAsUid; + + // In order to preserve the mapping of NetworkRequest-to-callback when apps register + // callbacks using a returned NetworkRequest, the original NetworkRequest needs to be + // maintained for keying off of. This is only a concern when the original nri + // mNetworkRequests changes which happens currently for apps that register callbacks to + // track the default network. In those cases, the nri is updated to have mNetworkRequests + // that match the per-app default nri that currently tracks the calling app's uid so that + // callbacks are fired at the appropriate time. When the callbacks fire, + // mNetworkRequestForCallback will be used so as to preserve the caller's mapping. When + // callbacks are updated to key off of an nri vs NetworkRequest, this stops being an issue. + // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects. + @NonNull + private final NetworkRequest mNetworkRequestForCallback; + NetworkRequest getNetworkRequestForCallback() { + return mNetworkRequestForCallback; + } + + /** + * Get the list of UIDs this nri applies to. + */ + @NonNull + private Set<UidRange> getUids() { + // networkCapabilities.getUids() returns a defensive copy. + // multilayer requests will all have the same uids so return the first one. + final Set<UidRange> uids = mRequests.get(0).networkCapabilities.getUidRanges(); + return (null == uids) ? new ArraySet<>() : uids; + } + + NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, + @Nullable final PendingIntent pi, @Nullable String callingAttributionTag) { + this(asUid, Collections.singletonList(r), r, pi, callingAttributionTag); + } + + NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r, + @NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi, + @Nullable String callingAttributionTag) { + ensureAllNetworkRequestsHaveType(r); + mRequests = initializeRequests(r); + mNetworkRequestForCallback = requestForCallback; + mPendingIntent = pi; + mMessenger = null; + mBinder = null; + mPid = getCallingPid(); + mUid = mDeps.getCallingUid(); + mAsUid = asUid; + mPerUidCounter = getRequestCounter(this); + mPerUidCounter.incrementCountOrThrow(mUid); + /** + * Location sensitive data not included in pending intent. Only included in + * {@link NetworkCallback}. + */ + mCallbackFlags = NetworkCallback.FLAG_NONE; + mCallingAttributionTag = callingAttributionTag; + } + + NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m, + @Nullable final IBinder binder, + @NetworkCallback.Flag int callbackFlags, + @Nullable String callingAttributionTag) { + this(asUid, Collections.singletonList(r), r, m, binder, callbackFlags, + callingAttributionTag); + } + + NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r, + @NonNull final NetworkRequest requestForCallback, @Nullable final Messenger m, + @Nullable final IBinder binder, + @NetworkCallback.Flag int callbackFlags, + @Nullable String callingAttributionTag) { + super(); + ensureAllNetworkRequestsHaveType(r); + mRequests = initializeRequests(r); + mNetworkRequestForCallback = requestForCallback; + mMessenger = m; + mBinder = binder; + mPid = getCallingPid(); + mUid = mDeps.getCallingUid(); + mAsUid = asUid; + mPendingIntent = null; + mPerUidCounter = getRequestCounter(this); + mPerUidCounter.incrementCountOrThrow(mUid); + mCallbackFlags = callbackFlags; + mCallingAttributionTag = callingAttributionTag; + linkDeathRecipient(); + } + + NetworkRequestInfo(@NonNull final NetworkRequestInfo nri, + @NonNull final List<NetworkRequest> r) { + super(); + ensureAllNetworkRequestsHaveType(r); + mRequests = initializeRequests(r); + mNetworkRequestForCallback = nri.getNetworkRequestForCallback(); + final NetworkAgentInfo satisfier = nri.getSatisfier(); + if (null != satisfier) { + // If the old NRI was satisfied by an NAI, then it may have had an active request. + // The active request is necessary to figure out what callbacks to send, in + // particular then a network updates its capabilities. + // As this code creates a new NRI with a new set of requests, figure out which of + // the list of requests should be the active request. It is always the first + // request of the list that can be satisfied by the satisfier since the order of + // requests is a priority order. + // Note even in the presence of a satisfier there may not be an active request, + // when the satisfier is the no-service network. + NetworkRequest activeRequest = null; + for (final NetworkRequest candidate : r) { + if (candidate.canBeSatisfiedBy(satisfier.networkCapabilities)) { + activeRequest = candidate; + break; + } + } + setSatisfier(satisfier, activeRequest); + } + mMessenger = nri.mMessenger; + mBinder = nri.mBinder; + mPid = nri.mPid; + mUid = nri.mUid; + mAsUid = nri.mAsUid; + mPendingIntent = nri.mPendingIntent; + mPerUidCounter = getRequestCounter(this); + mPerUidCounter.incrementCountOrThrow(mUid); + mCallbackFlags = nri.mCallbackFlags; + mCallingAttributionTag = nri.mCallingAttributionTag; + linkDeathRecipient(); + } + + NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r) { + this(asUid, Collections.singletonList(r)); + } + + NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r) { + this(asUid, r, r.get(0), null /* pi */, null /* callingAttributionTag */); + } + + // True if this NRI is being satisfied. It also accounts for if the nri has its satisifer + // set to the mNoServiceNetwork in which case mActiveRequest will be null thus returning + // false. + boolean isBeingSatisfied() { + return (null != mSatisfier && null != mActiveRequest); + } + + boolean isMultilayerRequest() { + return mRequests.size() > 1; + } + + private List<NetworkRequest> initializeRequests(List<NetworkRequest> r) { + // Creating a defensive copy to prevent the sender from modifying the list being + // reflected in the return value of this method. + final List<NetworkRequest> tempRequests = new ArrayList<>(r); + return Collections.unmodifiableList(tempRequests); + } + + void decrementRequestCount() { + mPerUidCounter.decrementCount(mUid); + } + + void linkDeathRecipient() { + if (null != mBinder) { + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + } + } + } + + void unlinkDeathRecipient() { + if (null != mBinder) { + mBinder.unlinkToDeath(this, 0); + } + } + + @Override + public void binderDied() { + log("ConnectivityService NetworkRequestInfo binderDied(" + + mRequests + ", " + mBinder + ")"); + releaseNetworkRequests(mRequests); + } + + @Override + public String toString() { + final String asUidString = (mAsUid == mUid) ? "" : " asUid: " + mAsUid; + return "uid/pid:" + mUid + "/" + mPid + asUidString + " activeRequest: " + + (mActiveRequest == null ? null : mActiveRequest.requestId) + + " callbackRequest: " + + mNetworkRequestForCallback.requestId + + " " + mRequests + + (mPendingIntent == null ? "" : " to trigger " + mPendingIntent) + + " callback flags: " + mCallbackFlags; + } + } + + private void ensureRequestableCapabilities(NetworkCapabilities networkCapabilities) { + final String badCapability = networkCapabilities.describeFirstNonRequestableCapability(); + if (badCapability != null) { + throw new IllegalArgumentException("Cannot request network with " + badCapability); + } + } + + // This checks that the passed capabilities either do not request a + // specific SSID/SignalStrength, or the calling app has permission to do so. + private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc, + int callerPid, int callerUid, String callerPackageName) { + if (null != nc.getSsid() && !checkSettingsPermission(callerPid, callerUid)) { + throw new SecurityException("Insufficient permissions to request a specific SSID"); + } + + if (nc.hasSignalStrength() + && !checkNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) { + throw new SecurityException( + "Insufficient permissions to request a specific signal strength"); + } + mAppOpsManager.checkPackage(callerUid, callerPackageName); + + if (!nc.getSubscriptionIds().isEmpty()) { + enforceNetworkFactoryPermission(); + } + } + + private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) { + final SortedSet<Integer> thresholds = new TreeSet<>(); + synchronized (nai) { + // mNetworkRequests may contain the same value multiple times in case of + // multilayer requests. It won't matter in this case because the thresholds + // will then be the same and be deduplicated as they enter the `thresholds` set. + // TODO : have mNetworkRequests be a Set<NetworkRequestInfo> or the like. + for (final NetworkRequestInfo nri : mNetworkRequests.values()) { + for (final NetworkRequest req : nri.mRequests) { + if (req.networkCapabilities.hasSignalStrength() + && nai.satisfiesImmutableCapabilitiesOf(req)) { + thresholds.add(req.networkCapabilities.getSignalStrength()); + } + } + } + } + return CollectionUtils.toIntArray(new ArrayList<>(thresholds)); + } + + private void updateSignalStrengthThresholds( + NetworkAgentInfo nai, String reason, NetworkRequest request) { + final int[] thresholdsArray = getSignalStrengthThresholds(nai); + + if (VDBG || (DBG && !"CONNECT".equals(reason))) { + String detail; + if (request != null && request.networkCapabilities.hasSignalStrength()) { + detail = reason + " " + request.networkCapabilities.getSignalStrength(); + } else { + detail = reason; + } + log(String.format("updateSignalStrengthThresholds: %s, sending %s to %s", + detail, Arrays.toString(thresholdsArray), nai.toShortString())); + } + + nai.onSignalStrengthThresholdsUpdated(thresholdsArray); + } + + private void ensureValidNetworkSpecifier(NetworkCapabilities nc) { + if (nc == null) { + return; + } + NetworkSpecifier ns = nc.getNetworkSpecifier(); + if (ns == null) { + return; + } + if (ns instanceof MatchAllNetworkSpecifier) { + throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted"); + } + } + + private void ensureValid(NetworkCapabilities nc) { + ensureValidNetworkSpecifier(nc); + if (nc.isPrivateDnsBroken()) { + throw new IllegalArgumentException("Can't request broken private DNS"); + } + } + + private boolean isTargetSdkAtleast(int version, int callingUid, + @NonNull String callingPackageName) { + final UserHandle user = UserHandle.getUserHandleForUid(callingUid); + final PackageManager pm = + mContext.createContextAsUser(user, 0 /* flags */).getPackageManager(); + try { + final int callingVersion = pm.getTargetSdkVersion(callingPackageName); + if (callingVersion < version) return false; + } catch (PackageManager.NameNotFoundException e) { } + return true; + } + + @Override + public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities, + int reqTypeInt, Messenger messenger, int timeoutMs, IBinder binder, + int legacyType, int callbackFlags, @NonNull String callingPackageName, + @Nullable String callingAttributionTag) { + if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) { + if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(), + callingPackageName)) { + throw new SecurityException("Insufficient permissions to specify legacy type"); + } + } + final NetworkCapabilities defaultNc = mDefaultRequest.mRequests.get(0).networkCapabilities; + final int callingUid = mDeps.getCallingUid(); + // Privileged callers can track the default network of another UID by passing in a UID. + if (asUid != Process.INVALID_UID) { + enforceSettingsPermission(); + } else { + asUid = callingUid; + } + final NetworkRequest.Type reqType; + try { + reqType = NetworkRequest.Type.values()[reqTypeInt]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Unsupported request type " + reqTypeInt); + } + switch (reqType) { + case TRACK_DEFAULT: + // If the request type is TRACK_DEFAULT, the passed {@code networkCapabilities} + // is unused and will be replaced by ones appropriate for the UID (usually, the + // calling app). This allows callers to keep track of the default network. + networkCapabilities = copyDefaultNetworkCapabilitiesForUid( + defaultNc, asUid, callingUid, callingPackageName); + enforceAccessPermission(); + break; + case TRACK_SYSTEM_DEFAULT: + enforceSettingsPermission(); + networkCapabilities = new NetworkCapabilities(defaultNc); + break; + case BACKGROUND_REQUEST: + enforceNetworkStackOrSettingsPermission(); + // Fall-through since other checks are the same with normal requests. + case REQUEST: + networkCapabilities = new NetworkCapabilities(networkCapabilities); + enforceNetworkRequestPermissions(networkCapabilities, callingPackageName, + callingAttributionTag); + // TODO: this is incorrect. We mark the request as metered or not depending on + // the state of the app when the request is filed, but we never change the + // request if the app changes network state. http://b/29964605 + enforceMeteredApnPolicy(networkCapabilities); + break; + case LISTEN_FOR_BEST: + enforceAccessPermission(); + networkCapabilities = new NetworkCapabilities(networkCapabilities); + break; + default: + throw new IllegalArgumentException("Unsupported request type " + reqType); + } + ensureRequestableCapabilities(networkCapabilities); + ensureSufficientPermissionsForRequest(networkCapabilities, + Binder.getCallingPid(), callingUid, callingPackageName); + + // Enforce FOREGROUND if the caller does not have permission to use background network. + if (reqType == LISTEN_FOR_BEST) { + restrictBackgroundRequestForCaller(networkCapabilities); + } + + // Set the UID range for this request to the single UID of the requester, unless the + // requester has the permission to specify other UIDs. + // This will overwrite any allowed UIDs in the requested capabilities. Though there + // are no visible methods to set the UIDs, an app could use reflection to try and get + // networks for other apps so it's essential that the UIDs are overwritten. + // Also set the requester UID and package name in the request. + restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities, + callingUid, callingPackageName); + + if (timeoutMs < 0) { + throw new IllegalArgumentException("Bad timeout specified"); + } + ensureValid(networkCapabilities); + + final NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType, + nextNetworkRequestId(), reqType); + final NetworkRequestInfo nri = getNriToRegister( + asUid, networkRequest, messenger, binder, callbackFlags, + callingAttributionTag); + if (DBG) log("requestNetwork for " + nri); + + // For TRACK_SYSTEM_DEFAULT callbacks, the capabilities have been modified since they were + // copied from the default request above. (This is necessary to ensure, for example, that + // the callback does not leak sensitive information to unprivileged apps.) Check that the + // changes don't alter request matching. + if (reqType == NetworkRequest.Type.TRACK_SYSTEM_DEFAULT && + (!networkCapabilities.equalRequestableCapabilities(defaultNc))) { + throw new IllegalStateException( + "TRACK_SYSTEM_DEFAULT capabilities don't match default request: " + + networkCapabilities + " vs. " + defaultNc); + } + + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri)); + if (timeoutMs > 0) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_REQUEST, + nri), timeoutMs); + } + return networkRequest; + } + + /** + * Return the nri to be used when registering a network request. Specifically, this is used with + * requests registered to track the default request. If there is currently a per-app default + * tracking the app requestor, then we need to create a version of this nri that mirrors that of + * the tracking per-app default so that callbacks are sent to the app requestor appropriately. + * @param asUid the uid on behalf of which to file the request. Different from requestorUid + * when a privileged caller is tracking the default network for another uid. + * @param nr the network request for the nri. + * @param msgr the messenger for the nri. + * @param binder the binder for the nri. + * @param callingAttributionTag the calling attribution tag for the nri. + * @return the nri to register. + */ + private NetworkRequestInfo getNriToRegister(final int asUid, @NonNull final NetworkRequest nr, + @Nullable final Messenger msgr, @Nullable final IBinder binder, + @NetworkCallback.Flag int callbackFlags, + @Nullable String callingAttributionTag) { + final List<NetworkRequest> requests; + if (NetworkRequest.Type.TRACK_DEFAULT == nr.type) { + requests = copyDefaultNetworkRequestsForUid( + asUid, nr.getRequestorUid(), nr.getRequestorPackageName()); + } else { + requests = Collections.singletonList(nr); + } + return new NetworkRequestInfo( + asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag); + } + + private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities, + String callingPackageName, String callingAttributionTag) { + if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) { + enforceConnectivityRestrictedNetworksPermission(); + } else { + enforceChangePermission(callingPackageName, callingAttributionTag); + } + } + + @Override + public boolean requestBandwidthUpdate(Network network) { + enforceAccessPermission(); + NetworkAgentInfo nai = null; + if (network == null) { + return false; + } + synchronized (mNetworkForNetId) { + nai = mNetworkForNetId.get(network.getNetId()); + } + if (nai != null) { + nai.onBandwidthUpdateRequested(); + synchronized (mBandwidthRequests) { + final int uid = mDeps.getCallingUid(); + Integer uidReqs = mBandwidthRequests.get(uid); + if (uidReqs == null) { + uidReqs = 0; + } + mBandwidthRequests.put(uid, ++uidReqs); + } + return true; + } + return false; + } + + private boolean isSystem(int uid) { + return uid < Process.FIRST_APPLICATION_UID; + } + + private void enforceMeteredApnPolicy(NetworkCapabilities networkCapabilities) { + final int uid = mDeps.getCallingUid(); + if (isSystem(uid)) { + // Exemption for system uid. + return; + } + if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) { + // Policy already enforced. + return; + } + final long ident = Binder.clearCallingIdentity(); + try { + if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) { + // If UID is restricted, don't allow them to bring up metered APNs. + networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public NetworkRequest pendingRequestForNetwork(NetworkCapabilities networkCapabilities, + PendingIntent operation, @NonNull String callingPackageName, + @Nullable String callingAttributionTag) { + Objects.requireNonNull(operation, "PendingIntent cannot be null."); + final int callingUid = mDeps.getCallingUid(); + networkCapabilities = new NetworkCapabilities(networkCapabilities); + enforceNetworkRequestPermissions(networkCapabilities, callingPackageName, + callingAttributionTag); + enforceMeteredApnPolicy(networkCapabilities); + ensureRequestableCapabilities(networkCapabilities); + ensureSufficientPermissionsForRequest(networkCapabilities, + Binder.getCallingPid(), callingUid, callingPackageName); + ensureValidNetworkSpecifier(networkCapabilities); + restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities, + callingUid, callingPackageName); + + NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE, + nextNetworkRequestId(), NetworkRequest.Type.REQUEST); + NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation, + callingAttributionTag); + if (DBG) log("pendingRequest for " + nri); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT, + nri)); + return networkRequest; + } + + private void releasePendingNetworkRequestWithDelay(PendingIntent operation) { + mHandler.sendMessageDelayed( + mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT, + mDeps.getCallingUid(), 0, operation), mReleasePendingIntentDelayMs); + } + + @Override + public void releasePendingNetworkRequest(PendingIntent operation) { + Objects.requireNonNull(operation, "PendingIntent cannot be null."); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT, + mDeps.getCallingUid(), 0, operation)); + } + + // In order to implement the compatibility measure for pre-M apps that call + // WifiManager.enableNetwork(..., true) without also binding to that network explicitly, + // WifiManager registers a network listen for the purpose of calling setProcessDefaultNetwork. + // This ensures it has permission to do so. + private boolean hasWifiNetworkListenPermission(NetworkCapabilities nc) { + if (nc == null) { + return false; + } + int[] transportTypes = nc.getTransportTypes(); + if (transportTypes.length != 1 || transportTypes[0] != NetworkCapabilities.TRANSPORT_WIFI) { + return false; + } + try { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.ACCESS_WIFI_STATE, + "ConnectivityService"); + } catch (SecurityException e) { + return false; + } + return true; + } + + @Override + public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities, + Messenger messenger, IBinder binder, + @NetworkCallback.Flag int callbackFlags, + @NonNull String callingPackageName, @NonNull String callingAttributionTag) { + final int callingUid = mDeps.getCallingUid(); + if (!hasWifiNetworkListenPermission(networkCapabilities)) { + enforceAccessPermission(); + } + + NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities); + ensureSufficientPermissionsForRequest(networkCapabilities, + Binder.getCallingPid(), callingUid, callingPackageName); + restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName); + // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so + // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get + // onLost and onAvailable callbacks when networks move in and out of the background. + // There is no need to do this for requests because an app without CHANGE_NETWORK_STATE + // can't request networks. + restrictBackgroundRequestForCaller(nc); + ensureValid(nc); + + NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(), + NetworkRequest.Type.LISTEN); + NetworkRequestInfo nri = + new NetworkRequestInfo(callingUid, networkRequest, messenger, binder, callbackFlags, + callingAttributionTag); + if (VDBG) log("listenForNetwork for " + nri); + + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri)); + return networkRequest; + } + + @Override + public void pendingListenForNetwork(NetworkCapabilities networkCapabilities, + PendingIntent operation, @NonNull String callingPackageName, + @Nullable String callingAttributionTag) { + Objects.requireNonNull(operation, "PendingIntent cannot be null."); + final int callingUid = mDeps.getCallingUid(); + if (!hasWifiNetworkListenPermission(networkCapabilities)) { + enforceAccessPermission(); + } + ensureValid(networkCapabilities); + ensureSufficientPermissionsForRequest(networkCapabilities, + Binder.getCallingPid(), callingUid, callingPackageName); + final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities); + restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName); + + NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(), + NetworkRequest.Type.LISTEN); + NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation, + callingAttributionTag); + if (VDBG) log("pendingListenForNetwork for " + nri); + + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri)); + } + + /** Returns the next Network provider ID. */ + public final int nextNetworkProviderId() { + return mNextNetworkProviderId.getAndIncrement(); + } + + private void releaseNetworkRequests(List<NetworkRequest> networkRequests) { + for (int i = 0; i < networkRequests.size(); i++) { + releaseNetworkRequest(networkRequests.get(i)); + } + } + + @Override + public void releaseNetworkRequest(NetworkRequest networkRequest) { + ensureNetworkRequestHasType(networkRequest); + mHandler.sendMessage(mHandler.obtainMessage( + EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest)); + } + + private void handleRegisterNetworkProvider(NetworkProviderInfo npi) { + if (mNetworkProviderInfos.containsKey(npi.messenger)) { + // Avoid creating duplicates. even if an app makes a direct AIDL call. + // This will never happen if an app calls ConnectivityManager#registerNetworkProvider, + // as that will throw if a duplicate provider is registered. + loge("Attempt to register existing NetworkProviderInfo " + + mNetworkProviderInfos.get(npi.messenger).name); + return; + } + + if (DBG) log("Got NetworkProvider Messenger for " + npi.name); + mNetworkProviderInfos.put(npi.messenger, npi); + npi.connect(mContext, mTrackerHandler); + } + + @Override + public int registerNetworkProvider(Messenger messenger, String name) { + enforceNetworkFactoryOrSettingsPermission(); + Objects.requireNonNull(messenger, "messenger must be non-null"); + NetworkProviderInfo npi = new NetworkProviderInfo(name, messenger, + nextNetworkProviderId(), () -> unregisterNetworkProvider(messenger)); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_PROVIDER, npi)); + return npi.providerId; + } + + @Override + public void unregisterNetworkProvider(Messenger messenger) { + enforceNetworkFactoryOrSettingsPermission(); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_PROVIDER, messenger)); + } + + @Override + public void offerNetwork(final int providerId, + @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps, + @NonNull final INetworkOfferCallback callback) { + Objects.requireNonNull(score); + Objects.requireNonNull(caps); + Objects.requireNonNull(callback); + final NetworkOffer offer = new NetworkOffer( + FullScore.makeProspectiveScore(score, caps), caps, callback, providerId); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_OFFER, offer)); + } + + @Override + public void unofferNetwork(@NonNull final INetworkOfferCallback callback) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_OFFER, callback)); + } + + private void handleUnregisterNetworkProvider(Messenger messenger) { + NetworkProviderInfo npi = mNetworkProviderInfos.remove(messenger); + if (npi == null) { + loge("Failed to find Messenger in unregisterNetworkProvider"); + return; + } + // Unregister all the offers from this provider + final ArrayList<NetworkOfferInfo> toRemove = new ArrayList<>(); + for (final NetworkOfferInfo noi : mNetworkOffers) { + if (noi.offer.providerId == npi.providerId) { + // Can't call handleUnregisterNetworkOffer here because iteration is in progress + toRemove.add(noi); + } + } + for (final NetworkOfferInfo noi : toRemove) { + handleUnregisterNetworkOffer(noi); + } + if (DBG) log("unregisterNetworkProvider for " + npi.name); + } + + @Override + public void declareNetworkRequestUnfulfillable(@NonNull final NetworkRequest request) { + if (request.hasTransport(TRANSPORT_TEST)) { + enforceNetworkFactoryOrTestNetworksPermission(); + } else { + enforceNetworkFactoryPermission(); + } + final NetworkRequestInfo nri = mNetworkRequests.get(request); + if (nri != null) { + // declareNetworkRequestUnfulfillable() paths don't apply to multilayer requests. + ensureNotMultilayerRequest(nri, "declareNetworkRequestUnfulfillable"); + mHandler.post(() -> handleReleaseNetworkRequest( + nri.mRequests.get(0), mDeps.getCallingUid(), true)); + } + } + + // NOTE: Accessed on multiple threads, must be synchronized on itself. + @GuardedBy("mNetworkForNetId") + private final SparseArray<NetworkAgentInfo> mNetworkForNetId = new SparseArray<>(); + // NOTE: Accessed on multiple threads, synchronized with mNetworkForNetId. + // An entry is first reserved with NetIdManager, prior to being added to mNetworkForNetId, so + // there may not be a strict 1:1 correlation between the two. + private final NetIdManager mNetIdManager; + + // NetworkAgentInfo keyed off its connecting messenger + // TODO - eval if we can reduce the number of lists/hashmaps/sparsearrays + // NOTE: Only should be accessed on ConnectivityServiceThread, except dump(). + private final ArraySet<NetworkAgentInfo> mNetworkAgentInfos = new ArraySet<>(); + + // UID ranges for users that are currently blocked by VPNs. + // This array is accessed and iterated on multiple threads without holding locks, so its + // contents must never be mutated. When the ranges change, the array is replaced with a new one + // (on the handler thread). + private volatile List<UidRange> mVpnBlockedUidRanges = new ArrayList<>(); + + // Must only be accessed on the handler thread + @NonNull + private final ArrayList<NetworkOfferInfo> mNetworkOffers = new ArrayList<>(); + + @GuardedBy("mBlockedAppUids") + private final HashSet<Integer> mBlockedAppUids = new HashSet<>(); + + // Current OEM network preferences. This object must only be written to on the handler thread. + // Since it is immutable and always non-null, other threads may read it if they only care + // about seeing a consistent object but not that it is current. + @NonNull + private OemNetworkPreferences mOemNetworkPreferences = + new OemNetworkPreferences.Builder().build(); + // Current per-profile network preferences. This object follows the same threading rules as + // the OEM network preferences above. + @NonNull + private ProfileNetworkPreferences mProfileNetworkPreferences = new ProfileNetworkPreferences(); + + // OemNetworkPreferences activity String log entries. + private static final int MAX_OEM_NETWORK_PREFERENCE_LOGS = 20; + @NonNull + private final LocalLog mOemNetworkPreferencesLogs = + new LocalLog(MAX_OEM_NETWORK_PREFERENCE_LOGS); + + /** + * Determine whether a given package has a mapping in the current OemNetworkPreferences. + * @param packageName the package name to check existence of a mapping for. + * @return true if a mapping exists, false otherwise + */ + private boolean isMappedInOemNetworkPreference(@NonNull final String packageName) { + return mOemNetworkPreferences.getNetworkPreferences().containsKey(packageName); + } + + // The always-on request for an Internet-capable network that apps without a specific default + // fall back to. + @VisibleForTesting + @NonNull + final NetworkRequestInfo mDefaultRequest; + // Collection of NetworkRequestInfo's used for default networks. + @VisibleForTesting + @NonNull + final ArraySet<NetworkRequestInfo> mDefaultNetworkRequests = new ArraySet<>(); + + private boolean isPerAppDefaultRequest(@NonNull final NetworkRequestInfo nri) { + return (mDefaultNetworkRequests.contains(nri) && mDefaultRequest != nri); + } + + /** + * Return the default network request currently tracking the given uid. + * @param uid the uid to check. + * @return the NetworkRequestInfo tracking the given uid. + */ + @NonNull + private NetworkRequestInfo getDefaultRequestTrackingUid(final int uid) { + for (final NetworkRequestInfo nri : mDefaultNetworkRequests) { + if (nri == mDefaultRequest) { + continue; + } + // Checking the first request is sufficient as only multilayer requests will have more + // than one request and for multilayer, all requests will track the same uids. + if (nri.mRequests.get(0).networkCapabilities.appliesToUid(uid)) { + return nri; + } + } + return mDefaultRequest; + } + + /** + * Get a copy of the network requests of the default request that is currently tracking the + * given uid. + * @param asUid the uid on behalf of which to file the request. Different from requestorUid + * when a privileged caller is tracking the default network for another uid. + * @param requestorUid the uid to check the default for. + * @param requestorPackageName the requestor's package name. + * @return a copy of the default's NetworkRequest that is tracking the given uid. + */ + @NonNull + private List<NetworkRequest> copyDefaultNetworkRequestsForUid( + final int asUid, final int requestorUid, @NonNull final String requestorPackageName) { + return copyNetworkRequestsForUid( + getDefaultRequestTrackingUid(asUid).mRequests, + asUid, requestorUid, requestorPackageName); + } + + /** + * Copy the given nri's NetworkRequest collection. + * @param requestsToCopy the NetworkRequest collection to be copied. + * @param asUid the uid on behalf of which to file the request. Different from requestorUid + * when a privileged caller is tracking the default network for another uid. + * @param requestorUid the uid to set on the copied collection. + * @param requestorPackageName the package name to set on the copied collection. + * @return the copied NetworkRequest collection. + */ + @NonNull + private List<NetworkRequest> copyNetworkRequestsForUid( + @NonNull final List<NetworkRequest> requestsToCopy, final int asUid, + final int requestorUid, @NonNull final String requestorPackageName) { + final List<NetworkRequest> requests = new ArrayList<>(); + for (final NetworkRequest nr : requestsToCopy) { + requests.add(new NetworkRequest(copyDefaultNetworkCapabilitiesForUid( + nr.networkCapabilities, asUid, requestorUid, requestorPackageName), + nr.legacyType, nextNetworkRequestId(), nr.type)); + } + return requests; + } + + @NonNull + private NetworkCapabilities copyDefaultNetworkCapabilitiesForUid( + @NonNull final NetworkCapabilities netCapToCopy, final int asUid, + final int requestorUid, @NonNull final String requestorPackageName) { + // These capabilities are for a TRACK_DEFAULT callback, so: + // 1. Remove NET_CAPABILITY_VPN, because it's (currently!) the only difference between + // mDefaultRequest and a per-UID default request. + // TODO: stop depending on the fact that these two unrelated things happen to be the same + // 2. Always set the UIDs to asUid. restrictRequestUidsForCallerAndSetRequestorInfo will + // not do this in the case of a privileged application. + final NetworkCapabilities netCap = new NetworkCapabilities(netCapToCopy); + netCap.removeCapability(NET_CAPABILITY_NOT_VPN); + netCap.setSingleUid(asUid); + restrictRequestUidsForCallerAndSetRequestorInfo( + netCap, requestorUid, requestorPackageName); + return netCap; + } + + /** + * Get the nri that is currently being tracked for callbacks by per-app defaults. + * @param nr the network request to check for equality against. + * @return the nri if one exists, null otherwise. + */ + @Nullable + private NetworkRequestInfo getNriForAppRequest(@NonNull final NetworkRequest nr) { + for (final NetworkRequestInfo nri : mNetworkRequests.values()) { + if (nri.getNetworkRequestForCallback().equals(nr)) { + return nri; + } + } + return null; + } + + /** + * Check if an nri is currently being managed by per-app default networking. + * @param nri the nri to check. + * @return true if this nri is currently being managed by per-app default networking. + */ + private boolean isPerAppTrackedNri(@NonNull final NetworkRequestInfo nri) { + // nri.mRequests.get(0) is only different from the original request filed in + // nri.getNetworkRequestForCallback() if nri.mRequests was changed by per-app default + // functionality therefore if these two don't match, it means this particular nri is + // currently being managed by a per-app default. + return nri.getNetworkRequestForCallback() != nri.mRequests.get(0); + } + + /** + * Determine if an nri is a managed default request that disallows default networking. + * @param nri the request to evaluate + * @return true if device-default networking is disallowed + */ + private boolean isDefaultBlocked(@NonNull final NetworkRequestInfo nri) { + // Check if this nri is a managed default that supports the default network at its + // lowest priority request. + final NetworkRequest defaultNetworkRequest = mDefaultRequest.mRequests.get(0); + final NetworkCapabilities lowestPriorityNetCap = + nri.mRequests.get(nri.mRequests.size() - 1).networkCapabilities; + return isPerAppDefaultRequest(nri) + && !(defaultNetworkRequest.networkCapabilities.equalRequestableCapabilities( + lowestPriorityNetCap)); + } + + // Request used to optionally keep mobile data active even when higher + // priority networks like Wi-Fi are active. + private final NetworkRequest mDefaultMobileDataRequest; + + // Request used to optionally keep wifi data active even when higher + // priority networks like ethernet are active. + private final NetworkRequest mDefaultWifiRequest; + + // Request used to optionally keep vehicle internal network always active + private final NetworkRequest mDefaultVehicleRequest; + + // Sentinel NAI used to direct apps with default networks that should have no connectivity to a + // network with no service. This NAI should never be matched against, nor should any public API + // ever return the associated network. For this reason, this NAI is not in the list of available + // NAIs. It is used in computeNetworkReassignment() to be set as the satisfier for non-device + // default requests that don't support using the device default network which will ultimately + // allow ConnectivityService to use this no-service network when calling makeDefaultForApps(). + @VisibleForTesting + final NetworkAgentInfo mNoServiceNetwork; + + // The NetworkAgentInfo currently satisfying the default request, if any. + private NetworkAgentInfo getDefaultNetwork() { + return mDefaultRequest.mSatisfier; + } + + private NetworkAgentInfo getDefaultNetworkForUid(final int uid) { + for (final NetworkRequestInfo nri : mDefaultNetworkRequests) { + // Currently, all network requests will have the same uids therefore checking the first + // one is sufficient. If/when uids are tracked at the nri level, this can change. + final Set<UidRange> uids = nri.mRequests.get(0).networkCapabilities.getUidRanges(); + if (null == uids) { + continue; + } + for (final UidRange range : uids) { + if (range.contains(uid)) { + return nri.getSatisfier(); + } + } + } + return getDefaultNetwork(); + } + + @Nullable + private Network getNetwork(@Nullable NetworkAgentInfo nai) { + return nai != null ? nai.network : null; + } + + private void ensureRunningOnConnectivityServiceThread() { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException( + "Not running on ConnectivityService thread: " + + Thread.currentThread().getName()); + } + } + + @VisibleForTesting + protected boolean isDefaultNetwork(NetworkAgentInfo nai) { + return nai == getDefaultNetwork(); + } + + /** + * Register a new agent with ConnectivityService to handle a network. + * + * @param na a reference for ConnectivityService to contact the agent asynchronously. + * @param networkInfo the initial info associated with this network. It can be updated later : + * see {@link #updateNetworkInfo}. + * @param linkProperties the initial link properties of this network. They can be updated + * later : see {@link #updateLinkProperties}. + * @param networkCapabilities the initial capabilites of this network. They can be updated + * later : see {@link #updateCapabilities}. + * @param initialScore the initial score of the network. See + * {@link NetworkAgentInfo#getCurrentScore}. + * @param networkAgentConfig metadata about the network. This is never updated. + * @param providerId the ID of the provider owning this NetworkAgent. + * @return the network created for this agent. + */ + public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo, + LinkProperties linkProperties, NetworkCapabilities networkCapabilities, + @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig, + int providerId) { + Objects.requireNonNull(networkInfo, "networkInfo must not be null"); + Objects.requireNonNull(linkProperties, "linkProperties must not be null"); + Objects.requireNonNull(networkCapabilities, "networkCapabilities must not be null"); + Objects.requireNonNull(initialScore, "initialScore must not be null"); + Objects.requireNonNull(networkAgentConfig, "networkAgentConfig must not be null"); + if (networkCapabilities.hasTransport(TRANSPORT_TEST)) { + enforceAnyPermissionOf(Manifest.permission.MANAGE_TEST_NETWORKS); + } else { + enforceNetworkFactoryPermission(); + } + + final int uid = mDeps.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + return registerNetworkAgentInternal(na, networkInfo, linkProperties, + networkCapabilities, initialScore, networkAgentConfig, providerId, uid); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo, + LinkProperties linkProperties, NetworkCapabilities networkCapabilities, + NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId, + int uid) { + if (networkCapabilities.hasTransport(TRANSPORT_TEST)) { + // Strictly, sanitizing here is unnecessary as the capabilities will be sanitized in + // the call to mixInCapabilities below anyway, but sanitizing here means the NAI never + // sees capabilities that may be malicious, which might prevent mistakes in the future. + networkCapabilities = new NetworkCapabilities(networkCapabilities); + networkCapabilities.restrictCapabilitesForTestNetwork(uid); + } + + LinkProperties lp = new LinkProperties(linkProperties); + + final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities); + final NetworkAgentInfo nai = new NetworkAgentInfo(na, + new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), lp, nc, + currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig), + this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs, + mQosCallbackTracker, mDeps); + + // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says. + processCapabilitiesFromAgent(nai, nc); + nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc)); + processLinkPropertiesFromAgent(nai, nai.linkProperties); + + final String extraInfo = networkInfo.getExtraInfo(); + final String name = TextUtils.isEmpty(extraInfo) + ? nai.networkCapabilities.getSsid() : extraInfo; + if (DBG) log("registerNetworkAgent " + nai); + mDeps.getNetworkStack().makeNetworkMonitor( + nai.network, name, new NetworkMonitorCallbacks(nai)); + // NetworkAgentInfo registration will finish when the NetworkMonitor is created. + // If the network disconnects or sends any other event before that, messages are deferred by + // NetworkAgent until nai.connect(), which will be called when finalizing the + // registration. + return nai.network; + } + + private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) { + nai.onNetworkMonitorCreated(networkMonitor); + if (VDBG) log("Got NetworkAgent Messenger"); + mNetworkAgentInfos.add(nai); + synchronized (mNetworkForNetId) { + mNetworkForNetId.put(nai.network.getNetId(), nai); + } + + try { + networkMonitor.start(); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + nai.notifyRegistered(); + NetworkInfo networkInfo = nai.networkInfo; + updateNetworkInfo(nai, networkInfo); + updateUids(nai, null, nai.networkCapabilities); + } + + private class NetworkOfferInfo implements IBinder.DeathRecipient { + @NonNull public final NetworkOffer offer; + + NetworkOfferInfo(@NonNull final NetworkOffer offer) { + this.offer = offer; + } + + @Override + public void binderDied() { + mHandler.post(() -> handleUnregisterNetworkOffer(this)); + } + } + + private boolean isNetworkProviderWithIdRegistered(final int providerId) { + for (final NetworkProviderInfo npi : mNetworkProviderInfos.values()) { + if (npi.providerId == providerId) return true; + } + return false; + } + + /** + * Register or update a network offer. + * @param newOffer The new offer. If the callback member is the same as an existing + * offer, it is an update of that offer. + */ + private void handleRegisterNetworkOffer(@NonNull final NetworkOffer newOffer) { + ensureRunningOnConnectivityServiceThread(); + if (!isNetworkProviderWithIdRegistered(newOffer.providerId)) { + // This may actually happen if a provider updates its score or registers and then + // immediately unregisters. The offer would still be in the handler queue, but the + // provider would have been removed. + if (DBG) log("Received offer from an unregistered provider"); + return; + } + final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback); + if (null != existingOffer) { + handleUnregisterNetworkOffer(existingOffer); + newOffer.migrateFrom(existingOffer.offer); + } + final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer); + try { + noi.offer.callback.asBinder().linkToDeath(noi, 0 /* flags */); + } catch (RemoteException e) { + noi.binderDied(); + return; + } + mNetworkOffers.add(noi); + issueNetworkNeeds(noi); + } + + private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) { + ensureRunningOnConnectivityServiceThread(); + mNetworkOffers.remove(noi); + noi.offer.callback.asBinder().unlinkToDeath(noi, 0 /* flags */); + } + + @Nullable private NetworkOfferInfo findNetworkOfferInfoByCallback( + @NonNull final INetworkOfferCallback callback) { + ensureRunningOnConnectivityServiceThread(); + for (final NetworkOfferInfo noi : mNetworkOffers) { + if (noi.offer.callback.asBinder().equals(callback.asBinder())) return noi; + } + return null; + } + + /** + * Called when receiving LinkProperties directly from a NetworkAgent. + * Stores into |nai| any data coming from the agent that might also be written to the network's + * LinkProperties by ConnectivityService itself. This ensures that the data provided by the + * agent is not lost when updateLinkProperties is called. + * This method should never alter the agent's LinkProperties, only store data in |nai|. + */ + private void processLinkPropertiesFromAgent(NetworkAgentInfo nai, LinkProperties lp) { + lp.ensureDirectlyConnectedRoutes(); + nai.clatd.setNat64PrefixFromRa(lp.getNat64Prefix()); + nai.networkAgentPortalData = lp.getCaptivePortalData(); + } + + private void updateLinkProperties(NetworkAgentInfo networkAgent, @NonNull LinkProperties newLp, + @NonNull LinkProperties oldLp) { + int netId = networkAgent.network.getNetId(); + + // The NetworkAgent does not know whether clatd is running on its network or not, or whether + // a NAT64 prefix was discovered by the DNS resolver. Before we do anything else, make sure + // the LinkProperties for the network are accurate. + networkAgent.clatd.fixupLinkProperties(oldLp, newLp); + + updateInterfaces(newLp, oldLp, netId, networkAgent.networkCapabilities); + + // update filtering rules, need to happen after the interface update so netd knows about the + // new interface (the interface name -> index map becomes initialized) + updateVpnFiltering(newLp, oldLp, networkAgent); + + updateMtu(newLp, oldLp); + // TODO - figure out what to do for clat +// for (LinkProperties lp : newLp.getStackedLinks()) { +// updateMtu(lp, null); +// } + if (isDefaultNetwork(networkAgent)) { + updateTcpBufferSizes(newLp.getTcpBufferSizes()); + } + + updateRoutes(newLp, oldLp, netId); + updateDnses(newLp, oldLp, netId); + // Make sure LinkProperties represents the latest private DNS status. + // This does not need to be done before updateDnses because the + // LinkProperties are not the source of the private DNS configuration. + // updateDnses will fetch the private DNS configuration from DnsManager. + mDnsManager.updatePrivateDnsStatus(netId, newLp); + + if (isDefaultNetwork(networkAgent)) { + handleApplyDefaultProxy(newLp.getHttpProxy()); + } else { + updateProxy(newLp, oldLp); + } + + updateWakeOnLan(newLp); + + // Captive portal data is obtained from NetworkMonitor and stored in NetworkAgentInfo. + // It is not always contained in the LinkProperties sent from NetworkAgents, and if it + // does, it needs to be merged here. + newLp.setCaptivePortalData(mergeCaptivePortalData(networkAgent.networkAgentPortalData, + networkAgent.capportApiData)); + + // TODO - move this check to cover the whole function + if (!Objects.equals(newLp, oldLp)) { + synchronized (networkAgent) { + networkAgent.linkProperties = newLp; + } + // Start or stop DNS64 detection and 464xlat according to network state. + networkAgent.clatd.update(); + notifyIfacesChangedForNetworkStats(); + networkAgent.networkMonitor().notifyLinkPropertiesChanged( + new LinkProperties(newLp, true /* parcelSensitiveFields */)); + if (networkAgent.everConnected) { + notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED); + } + } + + mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent); + } + + /** + * @param naData captive portal data from NetworkAgent + * @param apiData captive portal data from capport API + */ + @Nullable + private CaptivePortalData mergeCaptivePortalData(CaptivePortalData naData, + CaptivePortalData apiData) { + if (naData == null || apiData == null) { + return naData == null ? apiData : naData; + } + final CaptivePortalData.Builder captivePortalBuilder = + new CaptivePortalData.Builder(naData); + + if (apiData.isCaptive()) { + captivePortalBuilder.setCaptive(true); + } + if (apiData.isSessionExtendable()) { + captivePortalBuilder.setSessionExtendable(true); + } + if (apiData.getExpiryTimeMillis() >= 0 || apiData.getByteLimit() >= 0) { + // Expiry time, bytes remaining, refresh time all need to come from the same source, + // otherwise data would be inconsistent. Prefer the capport API info if present, + // as it can generally be refreshed more often. + captivePortalBuilder.setExpiryTime(apiData.getExpiryTimeMillis()); + captivePortalBuilder.setBytesRemaining(apiData.getByteLimit()); + captivePortalBuilder.setRefreshTime(apiData.getRefreshTimeMillis()); + } else if (naData.getExpiryTimeMillis() < 0 && naData.getByteLimit() < 0) { + // No source has time / bytes remaining information: surface the newest refresh time + // for other fields + captivePortalBuilder.setRefreshTime( + Math.max(naData.getRefreshTimeMillis(), apiData.getRefreshTimeMillis())); + } + + // Prioritize the user portal URL from the network agent if the source is authenticated. + if (apiData.getUserPortalUrl() != null && naData.getUserPortalUrlSource() + != CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) { + captivePortalBuilder.setUserPortalUrl(apiData.getUserPortalUrl(), + apiData.getUserPortalUrlSource()); + } + // Prioritize the venue information URL from the network agent if the source is + // authenticated. + if (apiData.getVenueInfoUrl() != null && naData.getVenueInfoUrlSource() + != CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) { + captivePortalBuilder.setVenueInfoUrl(apiData.getVenueInfoUrl(), + apiData.getVenueInfoUrlSource()); + } + return captivePortalBuilder.build(); + } + + private void wakeupModifyInterface(String iface, NetworkCapabilities caps, boolean add) { + // Marks are only available on WiFi interfaces. Checking for + // marks on unsupported interfaces is harmless. + if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return; + } + + int mark = mResources.get().getInteger(R.integer.config_networkWakeupPacketMark); + int mask = mResources.get().getInteger(R.integer.config_networkWakeupPacketMask); + + // Mask/mark of zero will not detect anything interesting. + // Don't install rules unless both values are nonzero. + if (mark == 0 || mask == 0) { + return; + } + + final String prefix = "iface:" + iface; + try { + if (add) { + mNetd.wakeupAddInterface(iface, prefix, mark, mask); + } else { + mNetd.wakeupDelInterface(iface, prefix, mark, mask); + } + } catch (Exception e) { + loge("Exception modifying wakeup packet monitoring: " + e); + } + + } + + private void updateInterfaces(final @Nullable LinkProperties newLp, + final @Nullable LinkProperties oldLp, final int netId, + final @NonNull NetworkCapabilities caps) { + final CompareResult<String> interfaceDiff = new CompareResult<>( + oldLp != null ? oldLp.getAllInterfaceNames() : null, + newLp != null ? newLp.getAllInterfaceNames() : null); + if (!interfaceDiff.added.isEmpty()) { + for (final String iface : interfaceDiff.added) { + try { + if (DBG) log("Adding iface " + iface + " to network " + netId); + mNetd.networkAddInterface(netId, iface); + wakeupModifyInterface(iface, caps, true); + mDeps.reportNetworkInterfaceForTransports(mContext, iface, + caps.getTransportTypes()); + } catch (Exception e) { + logw("Exception adding interface: " + e); + } + } + } + for (final String iface : interfaceDiff.removed) { + try { + if (DBG) log("Removing iface " + iface + " from network " + netId); + wakeupModifyInterface(iface, caps, false); + mNetd.networkRemoveInterface(netId, iface); + } catch (Exception e) { + loge("Exception removing interface: " + e); + } + } + } + + // TODO: move to frameworks/libs/net. + private RouteInfoParcel convertRouteInfo(RouteInfo route) { + final String nextHop; + + switch (route.getType()) { + case RouteInfo.RTN_UNICAST: + if (route.hasGateway()) { + nextHop = route.getGateway().getHostAddress(); + } else { + nextHop = INetd.NEXTHOP_NONE; + } + break; + case RouteInfo.RTN_UNREACHABLE: + nextHop = INetd.NEXTHOP_UNREACHABLE; + break; + case RouteInfo.RTN_THROW: + nextHop = INetd.NEXTHOP_THROW; + break; + default: + nextHop = INetd.NEXTHOP_NONE; + break; + } + + final RouteInfoParcel rip = new RouteInfoParcel(); + rip.ifName = route.getInterface(); + rip.destination = route.getDestination().toString(); + rip.nextHop = nextHop; + rip.mtu = route.getMtu(); + + return rip; + } + + /** + * Have netd update routes from oldLp to newLp. + * @return true if routes changed between oldLp and newLp + */ + private boolean updateRoutes(LinkProperties newLp, LinkProperties oldLp, int netId) { + // compare the route diff to determine which routes have been updated + final CompareOrUpdateResult<RouteInfo.RouteKey, RouteInfo> routeDiff = + new CompareOrUpdateResult<>( + oldLp != null ? oldLp.getAllRoutes() : null, + newLp != null ? newLp.getAllRoutes() : null, + (r) -> r.getRouteKey()); + + // add routes before removing old in case it helps with continuous connectivity + + // do this twice, adding non-next-hop routes first, then routes they are dependent on + for (RouteInfo route : routeDiff.added) { + if (route.hasGateway()) continue; + if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId); + try { + mNetd.networkAddRouteParcel(netId, convertRouteInfo(route)); + } catch (Exception e) { + if ((route.getDestination().getAddress() instanceof Inet4Address) || VDBG) { + loge("Exception in networkAddRouteParcel for non-gateway: " + e); + } + } + } + for (RouteInfo route : routeDiff.added) { + if (!route.hasGateway()) continue; + if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId); + try { + mNetd.networkAddRouteParcel(netId, convertRouteInfo(route)); + } catch (Exception e) { + if ((route.getGateway() instanceof Inet4Address) || VDBG) { + loge("Exception in networkAddRouteParcel for gateway: " + e); + } + } + } + + for (RouteInfo route : routeDiff.removed) { + if (VDBG || DDBG) log("Removing Route [" + route + "] from network " + netId); + try { + mNetd.networkRemoveRouteParcel(netId, convertRouteInfo(route)); + } catch (Exception e) { + loge("Exception in networkRemoveRouteParcel: " + e); + } + } + + for (RouteInfo route : routeDiff.updated) { + if (VDBG || DDBG) log("Updating Route [" + route + "] from network " + netId); + try { + mNetd.networkUpdateRouteParcel(netId, convertRouteInfo(route)); + } catch (Exception e) { + loge("Exception in networkUpdateRouteParcel: " + e); + } + } + return !routeDiff.added.isEmpty() || !routeDiff.removed.isEmpty() + || !routeDiff.updated.isEmpty(); + } + + private void updateDnses(LinkProperties newLp, LinkProperties oldLp, int netId) { + if (oldLp != null && newLp.isIdenticalDnses(oldLp)) { + return; // no updating necessary + } + + if (DBG) { + final Collection<InetAddress> dnses = newLp.getDnsServers(); + log("Setting DNS servers for network " + netId + " to " + dnses); + } + try { + mDnsManager.noteDnsServersForNetwork(netId, newLp); + mDnsManager.flushVmDnsCache(); + } catch (Exception e) { + loge("Exception in setDnsConfigurationForNetwork: " + e); + } + } + + private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp, + NetworkAgentInfo nai) { + final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null; + final String newIface = newLp != null ? newLp.getInterfaceName() : null; + final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp); + final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp); + + if (!wasFiltering && !needsFiltering) { + // Nothing to do. + return; + } + + if (Objects.equals(oldIface, newIface) && (wasFiltering == needsFiltering)) { + // Nothing changed. + return; + } + + final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges(); + final int vpnAppUid = nai.networkCapabilities.getOwnerUid(); + // TODO: this create a window of opportunity for apps to receive traffic between the time + // when the old rules are removed and the time when new rules are added. To fix this, + // make eBPF support two allowlisted interfaces so here new rules can be added before the + // old rules are being removed. + if (wasFiltering) { + mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid); + } + if (needsFiltering) { + mPermissionMonitor.onVpnUidRangesAdded(newIface, ranges, vpnAppUid); + } + } + + private void updateWakeOnLan(@NonNull LinkProperties lp) { + if (mWolSupportedInterfaces == null) { + mWolSupportedInterfaces = new ArraySet<>(mResources.get().getStringArray( + R.array.config_wakeonlan_supported_interfaces)); + } + lp.setWakeOnLanSupported(mWolSupportedInterfaces.contains(lp.getInterfaceName())); + } + + private int getNetworkPermission(NetworkCapabilities nc) { + if (!nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) { + return INetd.PERMISSION_SYSTEM; + } + if (!nc.hasCapability(NET_CAPABILITY_FOREGROUND)) { + return INetd.PERMISSION_NETWORK; + } + return INetd.PERMISSION_NONE; + } + + private void updateNetworkPermissions(@NonNull final NetworkAgentInfo nai, + @NonNull final NetworkCapabilities newNc) { + final int oldPermission = getNetworkPermission(nai.networkCapabilities); + final int newPermission = getNetworkPermission(newNc); + if (oldPermission != newPermission && nai.created && !nai.isVPN()) { + try { + mNetd.networkSetPermissionForNetwork(nai.network.getNetId(), newPermission); + } catch (RemoteException | ServiceSpecificException e) { + loge("Exception in networkSetPermissionForNetwork: " + e); + } + } + } + + /** + * Called when receiving NetworkCapabilities directly from a NetworkAgent. + * Stores into |nai| any data coming from the agent that might also be written to the network's + * NetworkCapabilities by ConnectivityService itself. This ensures that the data provided by the + * agent is not lost when updateCapabilities is called. + * This method should never alter the agent's NetworkCapabilities, only store data in |nai|. + */ + private void processCapabilitiesFromAgent(NetworkAgentInfo nai, NetworkCapabilities nc) { + // Note: resetting the owner UID before storing the agent capabilities in NAI means that if + // the agent attempts to change the owner UID, then nai.declaredCapabilities will not + // actually be the same as the capabilities sent by the agent. Still, it is safer to reset + // the owner UID here and behave as if the agent had never tried to change it. + if (nai.networkCapabilities.getOwnerUid() != nc.getOwnerUid()) { + Log.e(TAG, nai.toShortString() + ": ignoring attempt to change owner from " + + nai.networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid()); + nc.setOwnerUid(nai.networkCapabilities.getOwnerUid()); + } + nai.declaredCapabilities = new NetworkCapabilities(nc); + } + + /** Modifies |newNc| based on the capabilities of |underlyingNetworks| and |agentCaps|. */ + @VisibleForTesting + void applyUnderlyingCapabilities(@Nullable Network[] underlyingNetworks, + @NonNull NetworkCapabilities agentCaps, @NonNull NetworkCapabilities newNc) { + underlyingNetworks = underlyingNetworksOrDefault( + agentCaps.getOwnerUid(), underlyingNetworks); + long transportTypes = NetworkCapabilitiesUtils.packBits(agentCaps.getTransportTypes()); + int downKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; + int upKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; + // metered if any underlying is metered, or originally declared metered by the agent. + boolean metered = !agentCaps.hasCapability(NET_CAPABILITY_NOT_METERED); + boolean roaming = false; // roaming if any underlying is roaming + boolean congested = false; // congested if any underlying is congested + boolean suspended = true; // suspended if all underlying are suspended + + boolean hadUnderlyingNetworks = false; + if (null != underlyingNetworks) { + for (Network underlyingNetwork : underlyingNetworks) { + final NetworkAgentInfo underlying = + getNetworkAgentInfoForNetwork(underlyingNetwork); + if (underlying == null) continue; + + final NetworkCapabilities underlyingCaps = underlying.networkCapabilities; + hadUnderlyingNetworks = true; + for (int underlyingType : underlyingCaps.getTransportTypes()) { + transportTypes |= 1L << underlyingType; + } + + // Merge capabilities of this underlying network. For bandwidth, assume the + // worst case. + downKbps = NetworkCapabilities.minBandwidth(downKbps, + underlyingCaps.getLinkDownstreamBandwidthKbps()); + upKbps = NetworkCapabilities.minBandwidth(upKbps, + underlyingCaps.getLinkUpstreamBandwidthKbps()); + // If this underlying network is metered, the VPN is metered (it may cost money + // to send packets on this network). + metered |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_METERED); + // If this underlying network is roaming, the VPN is roaming (the billing structure + // is different than the usual, local one). + roaming |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_ROAMING); + // If this underlying network is congested, the VPN is congested (the current + // condition of the network affects the performance of this network). + congested |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_CONGESTED); + // If this network is not suspended, the VPN is not suspended (the VPN + // is able to transfer some data). + suspended &= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED); + } + } + if (!hadUnderlyingNetworks) { + // No idea what the underlying networks are; assume reasonable defaults + metered = true; + roaming = false; + congested = false; + suspended = false; + } + + newNc.setTransportTypes(NetworkCapabilitiesUtils.unpackBits(transportTypes)); + newNc.setLinkDownstreamBandwidthKbps(downKbps); + newNc.setLinkUpstreamBandwidthKbps(upKbps); + newNc.setCapability(NET_CAPABILITY_NOT_METERED, !metered); + newNc.setCapability(NET_CAPABILITY_NOT_ROAMING, !roaming); + newNc.setCapability(NET_CAPABILITY_NOT_CONGESTED, !congested); + newNc.setCapability(NET_CAPABILITY_NOT_SUSPENDED, !suspended); + } + + /** + * Augments the NetworkCapabilities passed in by a NetworkAgent with capabilities that are + * maintained here that the NetworkAgent is not aware of (e.g., validated, captive portal, + * and foreground status). + */ + @NonNull + private NetworkCapabilities mixInCapabilities(NetworkAgentInfo nai, NetworkCapabilities nc) { + // Once a NetworkAgent is connected, complain if some immutable capabilities are removed. + // Don't complain for VPNs since they're not driven by requests and there is no risk of + // causing a connect/teardown loop. + // TODO: remove this altogether and make it the responsibility of the NetworkProviders to + // avoid connect/teardown loops. + if (nai.everConnected && + !nai.isVPN() && + !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(nc)) { + // TODO: consider not complaining when a network agent degrades its capabilities if this + // does not cause any request (that is not a listen) currently matching that agent to + // stop being matched by the updated agent. + String diff = nai.networkCapabilities.describeImmutableDifferences(nc); + if (!TextUtils.isEmpty(diff)) { + Log.wtf(TAG, "BUG: " + nai + " lost immutable capabilities:" + diff); + } + } + + // Don't modify caller's NetworkCapabilities. + final NetworkCapabilities newNc = new NetworkCapabilities(nc); + if (nai.lastValidated) { + newNc.addCapability(NET_CAPABILITY_VALIDATED); + } else { + newNc.removeCapability(NET_CAPABILITY_VALIDATED); + } + if (nai.lastCaptivePortalDetected) { + newNc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + } else { + newNc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + } + if (nai.isBackgroundNetwork()) { + newNc.removeCapability(NET_CAPABILITY_FOREGROUND); + } else { + newNc.addCapability(NET_CAPABILITY_FOREGROUND); + } + if (nai.partialConnectivity) { + newNc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + } else { + newNc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + } + newNc.setPrivateDnsBroken(nai.networkCapabilities.isPrivateDnsBroken()); + + // TODO : remove this once all factories are updated to send NOT_SUSPENDED and NOT_ROAMING + if (!newNc.hasTransport(TRANSPORT_CELLULAR)) { + newNc.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + newNc.addCapability(NET_CAPABILITY_NOT_ROAMING); + } + + if (nai.supportsUnderlyingNetworks()) { + applyUnderlyingCapabilities(nai.declaredUnderlyingNetworks, nai.declaredCapabilities, + newNc); + } + + return newNc; + } + + private void updateNetworkInfoForRoamingAndSuspended(NetworkAgentInfo nai, + NetworkCapabilities prevNc, NetworkCapabilities newNc) { + final boolean prevSuspended = !prevNc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED); + final boolean suspended = !newNc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED); + final boolean prevRoaming = !prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING); + final boolean roaming = !newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING); + if (prevSuspended != suspended) { + // TODO (b/73132094) : remove this call once the few users of onSuspended and + // onResumed have been removed. + notifyNetworkCallbacks(nai, suspended ? ConnectivityManager.CALLBACK_SUSPENDED + : ConnectivityManager.CALLBACK_RESUMED); + } + if (prevSuspended != suspended || prevRoaming != roaming) { + // updateNetworkInfo will mix in the suspended info from the capabilities and + // take appropriate action for the network having possibly changed state. + updateNetworkInfo(nai, nai.networkInfo); + } + } + + /** + * Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically: + * + * 1. Calls mixInCapabilities to merge the passed-in NetworkCapabilities {@code nc} with the + * capabilities we manage and store in {@code nai}, such as validated status and captive + * portal status) + * 2. Takes action on the result: changes network permissions, sends CAP_CHANGED callbacks, and + * potentially triggers rematches. + * 3. Directly informs other network stack components (NetworkStatsService, VPNs, etc. of the + * change.) + * + * @param oldScore score of the network before any of the changes that prompted us + * to call this function. + * @param nai the network having its capabilities updated. + * @param nc the new network capabilities. + */ + private void updateCapabilities(final int oldScore, @NonNull final NetworkAgentInfo nai, + @NonNull final NetworkCapabilities nc) { + NetworkCapabilities newNc = mixInCapabilities(nai, nc); + if (Objects.equals(nai.networkCapabilities, newNc)) return; + updateNetworkPermissions(nai, newNc); + final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc); + + updateUids(nai, prevNc, newNc); + nai.updateScoreForNetworkAgentUpdate(); + + if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) { + // If the requestable capabilities haven't changed, and the score hasn't changed, then + // the change we're processing can't affect any requests, it can only affect the listens + // on this network. We might have been called by rematchNetworkAndRequests when a + // network changed foreground state. + processListenRequests(nai); + } else { + // If the requestable capabilities have changed or the score changed, we can't have been + // called by rematchNetworkAndRequests, so it's safe to start a rematch. + rematchAllNetworksAndRequests(); + notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED); + } + updateNetworkInfoForRoamingAndSuspended(nai, prevNc, newNc); + + final boolean oldMetered = prevNc.isMetered(); + final boolean newMetered = newNc.isMetered(); + final boolean meteredChanged = oldMetered != newMetered; + + if (meteredChanged) { + maybeNotifyNetworkBlocked(nai, oldMetered, newMetered, + mVpnBlockedUidRanges, mVpnBlockedUidRanges); + } + + final boolean roamingChanged = prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING) + != newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING); + + // Report changes that are interesting for network statistics tracking. + if (meteredChanged || roamingChanged) { + notifyIfacesChangedForNetworkStats(); + } + + // This network might have been underlying another network. Propagate its capabilities. + propagateUnderlyingNetworkCapabilities(nai.network); + + if (!newNc.equalsTransportTypes(prevNc)) { + mDnsManager.updateTransportsForNetwork( + nai.network.getNetId(), newNc.getTransportTypes()); + } + } + + /** Convenience method to update the capabilities for a given network. */ + private void updateCapabilitiesForNetwork(NetworkAgentInfo nai) { + updateCapabilities(nai.getCurrentScore(), nai, nai.networkCapabilities); + } + + /** + * Returns whether VPN isolation (ingress interface filtering) should be applied on the given + * network. + * + * Ingress interface filtering enforces that all apps under the given network can only receive + * packets from the network's interface (and loopback). This is important for VPNs because + * apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any + * non-VPN interfaces. + * + * As a result, this method should return true iff + * 1. the network is an app VPN (not legacy VPN) + * 2. the VPN does not allow bypass + * 3. the VPN is fully-routed + * 4. the VPN interface is non-null + * + * @see INetd#firewallAddUidInterfaceRules + * @see INetd#firewallRemoveUidInterfaceRules + */ + private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc, + LinkProperties lp) { + if (nc == null || lp == null) return false; + return nai.isVPN() + && !nai.networkAgentConfig.allowBypass + && nc.getOwnerUid() != Process.SYSTEM_UID + && lp.getInterfaceName() != null + && (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute()) + && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute()); + } + + private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) { + final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.size()]; + int index = 0; + for (UidRange range : ranges) { + stableRanges[index] = new UidRangeParcel(range.start, range.stop); + index++; + } + return stableRanges; + } + + private static UidRangeParcel[] toUidRangeStableParcels(UidRange[] ranges) { + final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.length]; + for (int i = 0; i < ranges.length; i++) { + stableRanges[i] = new UidRangeParcel(ranges[i].start, ranges[i].stop); + } + return stableRanges; + } + + private void maybeCloseSockets(NetworkAgentInfo nai, UidRangeParcel[] ranges, + int[] exemptUids) { + if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) { + try { + mNetd.socketDestroy(ranges, exemptUids); + } catch (Exception e) { + loge("Exception in socket destroy: ", e); + } + } + } + + private void updateUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) { + int[] exemptUids = new int[2]; + // TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used + // by PPTP. Fix this by making Vpn set the owner UID to VPN_UID instead of system when + // starting a legacy VPN, and remove VPN_UID here. (b/176542831) + exemptUids[0] = VPN_UID; + exemptUids[1] = nai.networkCapabilities.getOwnerUid(); + UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges); + + maybeCloseSockets(nai, ranges, exemptUids); + try { + if (add) { + mNetd.networkAddUidRanges(nai.network.netId, ranges); + } else { + mNetd.networkRemoveUidRanges(nai.network.netId, ranges); + } + } catch (Exception e) { + loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges + + " on netId " + nai.network.netId + ". " + e); + } + maybeCloseSockets(nai, ranges, exemptUids); + } + + private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc, + NetworkCapabilities newNc) { + Set<UidRange> prevRanges = null == prevNc ? null : prevNc.getUidRanges(); + Set<UidRange> newRanges = null == newNc ? null : newNc.getUidRanges(); + if (null == prevRanges) prevRanges = new ArraySet<>(); + if (null == newRanges) newRanges = new ArraySet<>(); + final Set<UidRange> prevRangesCopy = new ArraySet<>(prevRanges); + + prevRanges.removeAll(newRanges); + newRanges.removeAll(prevRangesCopy); + + try { + // When updating the VPN uid routing rules, add the new range first then remove the old + // range. If old range were removed first, there would be a window between the old + // range being removed and the new range being added, during which UIDs contained + // in both ranges are not subject to any VPN routing rules. Adding new range before + // removing old range works because, unlike the filtering rules below, it's possible to + // add duplicate UID routing rules. + // TODO: calculate the intersection of add & remove. Imagining that we are trying to + // remove uid 3 from a set containing 1-5. Intersection of the prev and new sets is: + // [1-5] & [1-2],[4-5] == [3] + // Then we can do: + // maybeCloseSockets([3]) + // mNetd.networkAddUidRanges([1-2],[4-5]) + // mNetd.networkRemoveUidRanges([1-5]) + // maybeCloseSockets([3]) + // This can prevent the sockets of uid 1-2, 4-5 from being closed. It also reduce the + // number of binder calls from 6 to 4. + if (!newRanges.isEmpty()) { + updateUidRanges(true, nai, newRanges); + } + if (!prevRanges.isEmpty()) { + updateUidRanges(false, nai, prevRanges); + } + final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties); + final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties); + final String iface = nai.linkProperties.getInterfaceName(); + // For VPN uid interface filtering, old ranges need to be removed before new ranges can + // be added, due to the range being expanded and stored as individual UIDs. For example + // the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means + // prevRanges = [0, 99999] while newRanges = [0, 10012], [10014, 99999]. If prevRanges + // were added first and then newRanges got removed later, there would be only one uid + // 10013 left. A consequence of removing old ranges before adding new ranges is that + // there is now a window of opportunity when the UIDs are not subject to any filtering. + // Note that this is in contrast with the (more robust) update of VPN routing rules + // above, where the addition of new ranges happens before the removal of old ranges. + // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range + // to be removed will never overlap with the new range to be added. + if (wasFiltering && !prevRanges.isEmpty()) { + mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid()); + } + if (shouldFilter && !newRanges.isEmpty()) { + mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid()); + } + } catch (Exception e) { + // Never crash! + loge("Exception in updateUids: ", e); + } + } + + public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) { + ensureRunningOnConnectivityServiceThread(); + + if (getNetworkAgentInfoForNetId(nai.network.getNetId()) != nai) { + // Ignore updates for disconnected networks + return; + } + if (VDBG || DDBG) { + log("Update of LinkProperties for " + nai.toShortString() + + "; created=" + nai.created + + "; everConnected=" + nai.everConnected); + } + // TODO: eliminate this defensive copy after confirming that updateLinkProperties does not + // modify its oldLp parameter. + updateLinkProperties(nai, newLp, new LinkProperties(nai.linkProperties)); + } + + private void sendPendingIntentForRequest(NetworkRequestInfo nri, NetworkAgentInfo networkAgent, + int notificationType) { + if (notificationType == ConnectivityManager.CALLBACK_AVAILABLE && !nri.mPendingIntentSent) { + Intent intent = new Intent(); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK, networkAgent.network); + // If apps could file multi-layer requests with PendingIntents, they'd need to know + // which of the layer is satisfied alongside with some ID for the request. Hence, if + // such an API is ever implemented, there is no doubt the right request to send in + // EXTRA_NETWORK_REQUEST is mActiveRequest, and whatever ID would be added would need to + // be sent as a separate extra. + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_REQUEST, nri.getActiveRequest()); + nri.mPendingIntentSent = true; + sendIntent(nri.mPendingIntent, intent); + } + // else not handled + } + + private void sendIntent(PendingIntent pendingIntent, Intent intent) { + mPendingIntentWakeLock.acquire(); + try { + if (DBG) log("Sending " + pendingIntent); + pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */); + } catch (PendingIntent.CanceledException e) { + if (DBG) log(pendingIntent + " was not sent, it had been canceled."); + mPendingIntentWakeLock.release(); + releasePendingNetworkRequest(pendingIntent); + } + // ...otherwise, mPendingIntentWakeLock.release() gets called by onSendFinished() + } + + @Override + public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode, + String resultData, Bundle resultExtras) { + if (DBG) log("Finished sending " + pendingIntent); + mPendingIntentWakeLock.release(); + // Release with a delay so the receiving client has an opportunity to put in its + // own request. + releasePendingNetworkRequestWithDelay(pendingIntent); + } + + private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri, + @NonNull final NetworkAgentInfo networkAgent, final int notificationType, + final int arg1) { + if (nri.mMessenger == null) { + // Default request has no msgr. Also prevents callbacks from being invoked for + // NetworkRequestInfos registered with ConnectivityDiagnostics requests. Those callbacks + // are Type.LISTEN, but should not have NetworkCallbacks invoked. + return; + } + Bundle bundle = new Bundle(); + // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects. + // TODO: check if defensive copies of data is needed. + final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback(); + putParcelable(bundle, nrForCallback); + Message msg = Message.obtain(); + if (notificationType != ConnectivityManager.CALLBACK_UNAVAIL) { + putParcelable(bundle, networkAgent.network); + } + final boolean includeLocationSensitiveInfo = + (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0; + switch (notificationType) { + case ConnectivityManager.CALLBACK_AVAILABLE: { + final NetworkCapabilities nc = + networkCapabilitiesRestrictedForCallerPermissions( + networkAgent.networkCapabilities, nri.mPid, nri.mUid); + putParcelable( + bundle, + createWithLocationInfoSanitizedIfNecessaryWhenParceled( + nc, includeLocationSensitiveInfo, nri.mPid, nri.mUid, + nrForCallback.getRequestorPackageName(), + nri.mCallingAttributionTag)); + putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions( + networkAgent.linkProperties, nri.mPid, nri.mUid)); + // For this notification, arg1 contains the blocked status. + msg.arg1 = arg1; + break; + } + case ConnectivityManager.CALLBACK_LOSING: { + msg.arg1 = arg1; + break; + } + case ConnectivityManager.CALLBACK_CAP_CHANGED: { + // networkAgent can't be null as it has been accessed a few lines above. + final NetworkCapabilities netCap = + networkCapabilitiesRestrictedForCallerPermissions( + networkAgent.networkCapabilities, nri.mPid, nri.mUid); + putParcelable( + bundle, + createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, includeLocationSensitiveInfo, nri.mPid, nri.mUid, + nrForCallback.getRequestorPackageName(), + nri.mCallingAttributionTag)); + break; + } + case ConnectivityManager.CALLBACK_IP_CHANGED: { + putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions( + networkAgent.linkProperties, nri.mPid, nri.mUid)); + break; + } + case ConnectivityManager.CALLBACK_BLK_CHANGED: { + maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1); + msg.arg1 = arg1; + break; + } + } + msg.what = notificationType; + msg.setData(bundle); + try { + if (VDBG) { + String notification = ConnectivityManager.getCallbackName(notificationType); + log("sending notification " + notification + " for " + nrForCallback); + } + nri.mMessenger.send(msg); + } catch (RemoteException e) { + // may occur naturally in the race of binder death. + loge("RemoteException caught trying to send a callback msg for " + nrForCallback); + } + } + + private static <T extends Parcelable> void putParcelable(Bundle bundle, T t) { + bundle.putParcelable(t.getClass().getSimpleName(), t); + } + + private void teardownUnneededNetwork(NetworkAgentInfo nai) { + if (nai.numRequestNetworkRequests() != 0) { + for (int i = 0; i < nai.numNetworkRequests(); i++) { + NetworkRequest nr = nai.requestAt(i); + // Ignore listening and track default requests. + if (!nr.isRequest()) continue; + loge("Dead network still had at least " + nr); + break; + } + } + nai.disconnect(); + } + + private void handleLingerComplete(NetworkAgentInfo oldNetwork) { + if (oldNetwork == null) { + loge("Unknown NetworkAgentInfo in handleLingerComplete"); + return; + } + if (DBG) log("handleLingerComplete for " + oldNetwork.toShortString()); + + // If we get here it means that the last linger timeout for this network expired. So there + // must be no other active linger timers, and we must stop lingering. + oldNetwork.clearInactivityState(); + + if (unneeded(oldNetwork, UnneededFor.TEARDOWN)) { + // Tear the network down. + teardownUnneededNetwork(oldNetwork); + } else { + // Put the network in the background if it doesn't satisfy any foreground request. + updateCapabilitiesForNetwork(oldNetwork); + } + } + + private void processDefaultNetworkChanges(@NonNull final NetworkReassignment changes) { + boolean isDefaultChanged = false; + for (final NetworkRequestInfo defaultRequestInfo : mDefaultNetworkRequests) { + final NetworkReassignment.RequestReassignment reassignment = + changes.getReassignment(defaultRequestInfo); + if (null == reassignment) { + continue; + } + // reassignment only contains those instances where the satisfying network changed. + isDefaultChanged = true; + // Notify system services of the new default. + makeDefault(defaultRequestInfo, reassignment.mOldNetwork, reassignment.mNewNetwork); + } + + if (isDefaultChanged) { + // Hold a wakelock for a short time to help apps in migrating to a new default. + scheduleReleaseNetworkTransitionWakelock(); + } + } + + private void makeDefault(@NonNull final NetworkRequestInfo nri, + @Nullable final NetworkAgentInfo oldDefaultNetwork, + @Nullable final NetworkAgentInfo newDefaultNetwork) { + if (DBG) { + log("Switching to new default network for: " + nri + " using " + newDefaultNetwork); + } + + // Fix up the NetworkCapabilities of any networks that have this network as underlying. + if (newDefaultNetwork != null) { + propagateUnderlyingNetworkCapabilities(newDefaultNetwork.network); + } + + // Set an app level managed default and return since further processing only applies to the + // default network. + if (mDefaultRequest != nri) { + makeDefaultForApps(nri, oldDefaultNetwork, newDefaultNetwork); + return; + } + + makeDefaultNetwork(newDefaultNetwork); + + if (oldDefaultNetwork != null) { + mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork); + } + mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork); + handleApplyDefaultProxy(null != newDefaultNetwork + ? newDefaultNetwork.linkProperties.getHttpProxy() : null); + updateTcpBufferSizes(null != newDefaultNetwork + ? newDefaultNetwork.linkProperties.getTcpBufferSizes() : null); + notifyIfacesChangedForNetworkStats(); + } + + private void makeDefaultForApps(@NonNull final NetworkRequestInfo nri, + @Nullable final NetworkAgentInfo oldDefaultNetwork, + @Nullable final NetworkAgentInfo newDefaultNetwork) { + try { + if (VDBG) { + log("Setting default network for " + nri + + " using UIDs " + nri.getUids() + + " with old network " + (oldDefaultNetwork != null + ? oldDefaultNetwork.network().getNetId() : "null") + + " and new network " + (newDefaultNetwork != null + ? newDefaultNetwork.network().getNetId() : "null")); + } + if (nri.getUids().isEmpty()) { + throw new IllegalStateException("makeDefaultForApps called without specifying" + + " any applications to set as the default." + nri); + } + if (null != newDefaultNetwork) { + mNetd.networkAddUidRanges( + newDefaultNetwork.network.getNetId(), + toUidRangeStableParcels(nri.getUids())); + } + if (null != oldDefaultNetwork) { + mNetd.networkRemoveUidRanges( + oldDefaultNetwork.network.getNetId(), + toUidRangeStableParcels(nri.getUids())); + } + } catch (RemoteException | ServiceSpecificException e) { + loge("Exception setting app default network", e); + } + } + + private void makeDefaultNetwork(@Nullable final NetworkAgentInfo newDefaultNetwork) { + try { + if (null != newDefaultNetwork) { + mNetd.networkSetDefault(newDefaultNetwork.network.getNetId()); + } else { + mNetd.networkClearDefault(); + } + } catch (RemoteException | ServiceSpecificException e) { + loge("Exception setting default network :" + e); + } + } + + private void processListenRequests(@NonNull final NetworkAgentInfo nai) { + // For consistency with previous behaviour, send onLost callbacks before onAvailable. + processNewlyLostListenRequests(nai); + notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED); + processNewlySatisfiedListenRequests(nai); + } + + private void processNewlyLostListenRequests(@NonNull final NetworkAgentInfo nai) { + for (final NetworkRequestInfo nri : mNetworkRequests.values()) { + if (nri.isMultilayerRequest()) { + continue; + } + final NetworkRequest nr = nri.mRequests.get(0); + if (!nr.isListen()) continue; + if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) { + nai.removeRequest(nr.requestId); + callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0); + } + } + } + + private void processNewlySatisfiedListenRequests(@NonNull final NetworkAgentInfo nai) { + for (final NetworkRequestInfo nri : mNetworkRequests.values()) { + if (nri.isMultilayerRequest()) { + continue; + } + final NetworkRequest nr = nri.mRequests.get(0); + if (!nr.isListen()) continue; + if (nai.satisfies(nr) && !nai.isSatisfyingRequest(nr.requestId)) { + nai.addRequest(nr); + notifyNetworkAvailable(nai, nri); + } + } + } + + // An accumulator class to gather the list of changes that result from a rematch. + private static class NetworkReassignment { + static class RequestReassignment { + @NonNull public final NetworkRequestInfo mNetworkRequestInfo; + @Nullable public final NetworkRequest mOldNetworkRequest; + @Nullable public final NetworkRequest mNewNetworkRequest; + @Nullable public final NetworkAgentInfo mOldNetwork; + @Nullable public final NetworkAgentInfo mNewNetwork; + RequestReassignment(@NonNull final NetworkRequestInfo networkRequestInfo, + @Nullable final NetworkRequest oldNetworkRequest, + @Nullable final NetworkRequest newNetworkRequest, + @Nullable final NetworkAgentInfo oldNetwork, + @Nullable final NetworkAgentInfo newNetwork) { + mNetworkRequestInfo = networkRequestInfo; + mOldNetworkRequest = oldNetworkRequest; + mNewNetworkRequest = newNetworkRequest; + mOldNetwork = oldNetwork; + mNewNetwork = newNetwork; + } + + public String toString() { + final NetworkRequest requestToShow = null != mNewNetworkRequest + ? mNewNetworkRequest : mNetworkRequestInfo.mRequests.get(0); + return requestToShow.requestId + " : " + + (null != mOldNetwork ? mOldNetwork.network.getNetId() : "null") + + " → " + (null != mNewNetwork ? mNewNetwork.network.getNetId() : "null"); + } + } + + @NonNull private final ArrayList<RequestReassignment> mReassignments = new ArrayList<>(); + + @NonNull Iterable<RequestReassignment> getRequestReassignments() { + return mReassignments; + } + + void addRequestReassignment(@NonNull final RequestReassignment reassignment) { + if (Build.isDebuggable()) { + // The code is never supposed to add two reassignments of the same request. Make + // sure this stays true, but without imposing this expensive check on all + // reassignments on all user devices. + for (final RequestReassignment existing : mReassignments) { + if (existing.mNetworkRequestInfo.equals(reassignment.mNetworkRequestInfo)) { + throw new IllegalStateException("Trying to reassign [" + + reassignment + "] but already have [" + + existing + "]"); + } + } + } + mReassignments.add(reassignment); + } + + // Will return null if this reassignment does not change the network assigned to + // the passed request. + @Nullable + private RequestReassignment getReassignment(@NonNull final NetworkRequestInfo nri) { + for (final RequestReassignment event : getRequestReassignments()) { + if (nri == event.mNetworkRequestInfo) return event; + } + return null; + } + + public String toString() { + final StringJoiner sj = new StringJoiner(", " /* delimiter */, + "NetReassign [" /* prefix */, "]" /* suffix */); + if (mReassignments.isEmpty()) return sj.add("no changes").toString(); + for (final RequestReassignment rr : getRequestReassignments()) { + sj.add(rr.toString()); + } + return sj.toString(); + } + + public String debugString() { + final StringBuilder sb = new StringBuilder(); + sb.append("NetworkReassignment :"); + if (mReassignments.isEmpty()) return sb.append(" no changes").toString(); + for (final RequestReassignment rr : getRequestReassignments()) { + sb.append("\n ").append(rr); + } + return sb.append("\n").toString(); + } + } + + private void updateSatisfiersForRematchRequest(@NonNull final NetworkRequestInfo nri, + @Nullable final NetworkRequest previousRequest, + @Nullable final NetworkRequest newRequest, + @Nullable final NetworkAgentInfo previousSatisfier, + @Nullable final NetworkAgentInfo newSatisfier, + final long now) { + if (null != newSatisfier && mNoServiceNetwork != newSatisfier) { + if (VDBG) log("rematch for " + newSatisfier.toShortString()); + if (null != previousRequest && null != previousSatisfier) { + if (VDBG || DDBG) { + log(" accepting network in place of " + previousSatisfier.toShortString()); + } + previousSatisfier.removeRequest(previousRequest.requestId); + previousSatisfier.lingerRequest(previousRequest.requestId, now); + } else { + if (VDBG || DDBG) log(" accepting network in place of null"); + } + + // To prevent constantly CPU wake up for nascent timer, if a network comes up + // and immediately satisfies a request then remove the timer. This will happen for + // all networks except in the case of an underlying network for a VCN. + if (newSatisfier.isNascent()) { + newSatisfier.unlingerRequest(NetworkRequest.REQUEST_ID_NONE); + newSatisfier.unsetInactive(); + } + + // if newSatisfier is not null, then newRequest may not be null. + newSatisfier.unlingerRequest(newRequest.requestId); + if (!newSatisfier.addRequest(newRequest)) { + Log.wtf(TAG, "BUG: " + newSatisfier.toShortString() + " already has " + + newRequest); + } + } else if (null != previousRequest && null != previousSatisfier) { + if (DBG) { + log("Network " + previousSatisfier.toShortString() + " stopped satisfying" + + " request " + previousRequest.requestId); + } + previousSatisfier.removeRequest(previousRequest.requestId); + } + nri.setSatisfier(newSatisfier, newRequest); + } + + /** + * This function is triggered when something can affect what network should satisfy what + * request, and it computes the network reassignment from the passed collection of requests to + * network match to the one that the system should now have. That data is encoded in an + * object that is a list of changes, each of them having an NRI, and old satisfier, and a new + * satisfier. + * + * After the reassignment is computed, it is applied to the state objects. + * + * @param networkRequests the nri objects to evaluate for possible network reassignment + * @return NetworkReassignment listing of proposed network assignment changes + */ + @NonNull + private NetworkReassignment computeNetworkReassignment( + @NonNull final Collection<NetworkRequestInfo> networkRequests) { + final NetworkReassignment changes = new NetworkReassignment(); + + // Gather the list of all relevant agents. + final ArrayList<NetworkAgentInfo> nais = new ArrayList<>(); + for (final NetworkAgentInfo nai : mNetworkAgentInfos) { + if (!nai.everConnected) { + continue; + } + nais.add(nai); + } + + for (final NetworkRequestInfo nri : networkRequests) { + // Non-multilayer listen requests can be ignored. + if (!nri.isMultilayerRequest() && nri.mRequests.get(0).isListen()) { + continue; + } + NetworkAgentInfo bestNetwork = null; + NetworkRequest bestRequest = null; + for (final NetworkRequest req : nri.mRequests) { + bestNetwork = mNetworkRanker.getBestNetwork(req, nais, nri.getSatisfier()); + // Stop evaluating as the highest possible priority request is satisfied. + if (null != bestNetwork) { + bestRequest = req; + break; + } + } + if (null == bestNetwork && isDefaultBlocked(nri)) { + // Remove default networking if disallowed for managed default requests. + bestNetwork = mNoServiceNetwork; + } + if (nri.getSatisfier() != bestNetwork) { + // bestNetwork may be null if no network can satisfy this request. + changes.addRequestReassignment(new NetworkReassignment.RequestReassignment( + nri, nri.mActiveRequest, bestRequest, nri.getSatisfier(), bestNetwork)); + } + } + return changes; + } + + private Set<NetworkRequestInfo> getNrisFromGlobalRequests() { + return new HashSet<>(mNetworkRequests.values()); + } + + /** + * Attempt to rematch all Networks with all NetworkRequests. This may result in Networks + * being disconnected. + */ + private void rematchAllNetworksAndRequests() { + rematchNetworksAndRequests(getNrisFromGlobalRequests()); + } + + /** + * Attempt to rematch all Networks with given NetworkRequests. This may result in Networks + * being disconnected. + */ + private void rematchNetworksAndRequests( + @NonNull final Set<NetworkRequestInfo> networkRequests) { + ensureRunningOnConnectivityServiceThread(); + // TODO: This may be slow, and should be optimized. + final long now = SystemClock.elapsedRealtime(); + final NetworkReassignment changes = computeNetworkReassignment(networkRequests); + if (VDBG || DDBG) { + log(changes.debugString()); + } else if (DBG) { + log(changes.toString()); // Shorter form, only one line of log + } + applyNetworkReassignment(changes, now); + issueNetworkNeeds(); + } + + private void applyNetworkReassignment(@NonNull final NetworkReassignment changes, + final long now) { + final Collection<NetworkAgentInfo> nais = mNetworkAgentInfos; + + // Since most of the time there are only 0 or 1 background networks, it would probably + // be more efficient to just use an ArrayList here. TODO : measure performance + final ArraySet<NetworkAgentInfo> oldBgNetworks = new ArraySet<>(); + for (final NetworkAgentInfo nai : nais) { + if (nai.isBackgroundNetwork()) oldBgNetworks.add(nai); + } + + // First, update the lists of satisfied requests in the network agents. This is necessary + // because some code later depends on this state to be correct, most prominently computing + // the linger status. + for (final NetworkReassignment.RequestReassignment event : + changes.getRequestReassignments()) { + updateSatisfiersForRematchRequest(event.mNetworkRequestInfo, + event.mOldNetworkRequest, event.mNewNetworkRequest, + event.mOldNetwork, event.mNewNetwork, + now); + } + + // Process default network changes if applicable. + processDefaultNetworkChanges(changes); + + // Notify requested networks are available after the default net is switched, but + // before LegacyTypeTracker sends legacy broadcasts + for (final NetworkReassignment.RequestReassignment event : + changes.getRequestReassignments()) { + if (null != event.mNewNetwork) { + notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo); + } else { + callCallbackForRequest(event.mNetworkRequestInfo, event.mOldNetwork, + ConnectivityManager.CALLBACK_LOST, 0); + } + } + + // Update the inactivity state before processing listen callbacks, because the background + // computation depends on whether the network is inactive. Don't send the LOSING callbacks + // just yet though, because they have to be sent after the listens are processed to keep + // backward compatibility. + final ArrayList<NetworkAgentInfo> inactiveNetworks = new ArrayList<>(); + for (final NetworkAgentInfo nai : nais) { + // Rematching may have altered the inactivity state of some networks, so update all + // inactivity timers. updateInactivityState reads the state from the network agent + // and does nothing if the state has not changed : the source of truth is controlled + // with NetworkAgentInfo#lingerRequest and NetworkAgentInfo#unlingerRequest, which + // have been called while rematching the individual networks above. + if (updateInactivityState(nai, now)) { + inactiveNetworks.add(nai); + } + } + + for (final NetworkAgentInfo nai : nais) { + if (!nai.everConnected) continue; + final boolean oldBackground = oldBgNetworks.contains(nai); + // Process listen requests and update capabilities if the background state has + // changed for this network. For consistency with previous behavior, send onLost + // callbacks before onAvailable. + processNewlyLostListenRequests(nai); + if (oldBackground != nai.isBackgroundNetwork()) { + applyBackgroundChangeForRematch(nai); + } + processNewlySatisfiedListenRequests(nai); + } + + for (final NetworkAgentInfo nai : inactiveNetworks) { + // For nascent networks, if connecting with no foreground request, skip broadcasting + // LOSING for backward compatibility. This is typical when mobile data connected while + // wifi connected with mobile data always-on enabled. + if (nai.isNascent()) continue; + notifyNetworkLosing(nai, now); + } + + updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais); + + // Tear down all unneeded networks. + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + if (unneeded(nai, UnneededFor.TEARDOWN)) { + if (nai.getInactivityExpiry() > 0) { + // This network has active linger timers and no requests, but is not + // lingering. Linger it. + // + // One way (the only way?) this can happen if this network is unvalidated + // and became unneeded due to another network improving its score to the + // point where this network will no longer be able to satisfy any requests + // even if it validates. + if (updateInactivityState(nai, now)) { + notifyNetworkLosing(nai, now); + } + } else { + if (DBG) log("Reaping " + nai.toShortString()); + teardownUnneededNetwork(nai); + } + } + } + } + + /** + * Apply a change in background state resulting from rematching networks with requests. + * + * During rematch, a network may change background states by starting to satisfy or stopping + * to satisfy a foreground request. Listens don't count for this. When a network changes + * background states, its capabilities need to be updated and callbacks fired for the + * capability change. + * + * @param nai The network that changed background states + */ + private void applyBackgroundChangeForRematch(@NonNull final NetworkAgentInfo nai) { + final NetworkCapabilities newNc = mixInCapabilities(nai, nai.networkCapabilities); + if (Objects.equals(nai.networkCapabilities, newNc)) return; + updateNetworkPermissions(nai, newNc); + nai.getAndSetNetworkCapabilities(newNc); + notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED); + } + + private void updateLegacyTypeTrackerAndVpnLockdownForRematch( + @NonNull final NetworkReassignment changes, + @NonNull final Collection<NetworkAgentInfo> nais) { + final NetworkReassignment.RequestReassignment reassignmentOfDefault = + changes.getReassignment(mDefaultRequest); + final NetworkAgentInfo oldDefaultNetwork = + null != reassignmentOfDefault ? reassignmentOfDefault.mOldNetwork : null; + final NetworkAgentInfo newDefaultNetwork = + null != reassignmentOfDefault ? reassignmentOfDefault.mNewNetwork : null; + + if (oldDefaultNetwork != newDefaultNetwork) { + // Maintain the illusion : since the legacy API only understands one network at a time, + // if the default network changed, apps should see a disconnected broadcast for the + // old default network before they see a connected broadcast for the new one. + if (oldDefaultNetwork != null) { + mLegacyTypeTracker.remove(oldDefaultNetwork.networkInfo.getType(), + oldDefaultNetwork, true); + } + if (newDefaultNetwork != null) { + // The new default network can be newly null if and only if the old default + // network doesn't satisfy the default request any more because it lost a + // capability. + mDefaultInetConditionPublished = newDefaultNetwork.lastValidated ? 100 : 0; + mLegacyTypeTracker.add( + newDefaultNetwork.networkInfo.getType(), newDefaultNetwork); + } + } + + // Now that all the callbacks have been sent, send the legacy network broadcasts + // as needed. This is necessary so that legacy requests correctly bind dns + // requests to this network. The legacy users are listening for this broadcast + // and will generally do a dns request so they can ensureRouteToHost and if + // they do that before the callbacks happen they'll use the default network. + // + // TODO: Is there still a race here? The legacy broadcast will be sent after sending + // callbacks, but if apps can receive the broadcast before the callback, they still might + // have an inconsistent view of networking. + // + // This *does* introduce a race where if the user uses the new api + // (notification callbacks) and then uses the old api (getNetworkInfo(type)) + // they may get old info. Reverse this after the old startUsing api is removed. + // This is on top of the multiple intent sequencing referenced in the todo above. + for (NetworkAgentInfo nai : nais) { + if (nai.everConnected) { + addNetworkToLegacyTypeTracker(nai); + } + } + } + + private void issueNetworkNeeds() { + ensureRunningOnConnectivityServiceThread(); + for (final NetworkOfferInfo noi : mNetworkOffers) { + issueNetworkNeeds(noi); + } + } + + private void issueNetworkNeeds(@NonNull final NetworkOfferInfo noi) { + ensureRunningOnConnectivityServiceThread(); + for (final NetworkRequestInfo nri : mNetworkRequests.values()) { + informOffer(nri, noi.offer, mNetworkRanker); + } + } + + /** + * Inform a NetworkOffer about any new situation of a request. + * + * This function handles updates to offers. A number of events may happen that require + * updating the registrant for this offer about the situation : + * • The offer itself was updated. This may lead the offer to no longer being able + * to satisfy a request or beat a satisfier (and therefore be no longer needed), + * or conversely being strengthened enough to beat the satisfier (and therefore + * start being needed) + * • The network satisfying a request changed (including cases where the request + * starts or stops being satisfied). The new network may be a stronger or weaker + * match than the old one, possibly affecting whether the offer is needed. + * • The network satisfying a request updated their score. This may lead the offer + * to no longer be able to beat it if the current satisfier got better, or + * conversely start being a good choice if the current satisfier got weaker. + * + * @param nri The request + * @param offer The offer. This may be an updated offer. + */ + private static void informOffer(@NonNull NetworkRequestInfo nri, + @NonNull final NetworkOffer offer, @NonNull final NetworkRanker networkRanker) { + final NetworkRequest activeRequest = nri.isBeingSatisfied() ? nri.getActiveRequest() : null; + final NetworkAgentInfo satisfier = null != activeRequest ? nri.getSatisfier() : null; + + // Multi-layer requests have a currently active request, the one being satisfied. + // Since the system will try to bring up a better network than is currently satisfying + // the request, NetworkProviders need to be told the offers matching the requests *above* + // the currently satisfied one are needed, that the ones *below* the satisfied one are + // not needed, and the offer is needed for the active request iff the offer can beat + // the satisfier. + // For non-multilayer requests, the logic above gracefully degenerates to only the + // last case. + // To achieve this, the loop below will proceed in three steps. In a first phase, inform + // providers that the offer is needed for this request, until the active request is found. + // In a second phase, deal with the currently active request. In a third phase, inform + // the providers that offer is unneeded for the remaining requests. + + // First phase : inform providers of all requests above the active request. + int i; + for (i = 0; nri.mRequests.size() > i; ++i) { + final NetworkRequest request = nri.mRequests.get(i); + if (activeRequest == request) break; // Found the active request : go to phase 2 + if (!request.isRequest()) continue; // Listens/track defaults are never sent to offers + // Since this request is higher-priority than the one currently satisfied, if the + // offer can satisfy it, the provider should try and bring up the network for sure ; + // no need to even ask the ranker – an offer that can satisfy is always better than + // no network. Hence tell the provider so unless it already knew. + if (request.canBeSatisfiedBy(offer.caps) && !offer.neededFor(request)) { + offer.onNetworkNeeded(request); + } + } + + // Second phase : deal with the active request (if any) + if (null != activeRequest && activeRequest.isRequest()) { + final boolean oldNeeded = offer.neededFor(activeRequest); + // An offer is needed if it is currently served by this provider or if this offer + // can beat the current satisfier. + final boolean currentlyServing = satisfier != null + && satisfier.factorySerialNumber == offer.providerId; + final boolean newNeeded = (currentlyServing + || (activeRequest.canBeSatisfiedBy(offer.caps) + && networkRanker.mightBeat(activeRequest, satisfier, offer))); + if (newNeeded != oldNeeded) { + if (newNeeded) { + offer.onNetworkNeeded(activeRequest); + } else { + // The offer used to be able to beat the satisfier. Now it can't. + offer.onNetworkUnneeded(activeRequest); + } + } + } + + // Third phase : inform the providers that the offer isn't needed for any request + // below the active one. + for (++i /* skip the active request */; nri.mRequests.size() > i; ++i) { + final NetworkRequest request = nri.mRequests.get(i); + if (!request.isRequest()) continue; // Listens/track defaults are never sent to offers + // Since this request is lower-priority than the one currently satisfied, if the + // offer can satisfy it, the provider should not try and bring up the network. + // Hence tell the provider so unless it already knew. + if (offer.neededFor(request)) { + offer.onNetworkUnneeded(request); + } + } + } + + private void addNetworkToLegacyTypeTracker(@NonNull final NetworkAgentInfo nai) { + for (int i = 0; i < nai.numNetworkRequests(); i++) { + NetworkRequest nr = nai.requestAt(i); + if (nr.legacyType != TYPE_NONE && nr.isRequest()) { + // legacy type tracker filters out repeat adds + mLegacyTypeTracker.add(nr.legacyType, nai); + } + } + + // A VPN generally won't get added to the legacy tracker in the "for (nri)" loop above, + // because usually there are no NetworkRequests it satisfies (e.g., mDefaultRequest + // wants the NOT_VPN capability, so it will never be satisfied by a VPN). So, add the + // newNetwork to the tracker explicitly (it's a no-op if it has already been added). + if (nai.isVPN()) { + mLegacyTypeTracker.add(TYPE_VPN, nai); + } + } + + private void updateInetCondition(NetworkAgentInfo nai) { + // Don't bother updating until we've graduated to validated at least once. + if (!nai.everValidated) return; + // For now only update icons for the default connection. + // TODO: Update WiFi and cellular icons separately. b/17237507 + if (!isDefaultNetwork(nai)) return; + + int newInetCondition = nai.lastValidated ? 100 : 0; + // Don't repeat publish. + if (newInetCondition == mDefaultInetConditionPublished) return; + + mDefaultInetConditionPublished = newInetCondition; + sendInetConditionBroadcast(nai.networkInfo); + } + + @NonNull + private NetworkInfo mixInInfo(@NonNull final NetworkAgentInfo nai, @NonNull NetworkInfo info) { + final NetworkInfo newInfo = new NetworkInfo(info); + // The suspended and roaming bits are managed in NetworkCapabilities. + final boolean suspended = + !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED); + if (suspended && info.getDetailedState() == NetworkInfo.DetailedState.CONNECTED) { + // Only override the state with SUSPENDED if the network is currently in CONNECTED + // state. This is because the network could have been suspended before connecting, + // or it could be disconnecting while being suspended, and in both these cases + // the state should not be overridden. Note that the only detailed state that + // maps to State.CONNECTED is DetailedState.CONNECTED, so there is also no need to + // worry about multiple different substates of CONNECTED. + newInfo.setDetailedState(NetworkInfo.DetailedState.SUSPENDED, info.getReason(), + info.getExtraInfo()); + } else if (!suspended && info.getDetailedState() == NetworkInfo.DetailedState.SUSPENDED) { + // SUSPENDED state is currently only overridden from CONNECTED state. In the case the + // network agent is created, then goes to suspended, then goes out of suspended without + // ever setting connected. Check if network agent is ever connected to update the state. + newInfo.setDetailedState(nai.everConnected + ? NetworkInfo.DetailedState.CONNECTED + : NetworkInfo.DetailedState.CONNECTING, + info.getReason(), + info.getExtraInfo()); + } + newInfo.setRoaming(!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + return newInfo; + } + + private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo info) { + final NetworkInfo newInfo = mixInInfo(networkAgent, info); + + final NetworkInfo.State state = newInfo.getState(); + NetworkInfo oldInfo = null; + synchronized (networkAgent) { + oldInfo = networkAgent.networkInfo; + networkAgent.networkInfo = newInfo; + } + + if (DBG) { + log(networkAgent.toShortString() + " EVENT_NETWORK_INFO_CHANGED, going from " + + oldInfo.getState() + " to " + state); + } + + if (!networkAgent.created + && (state == NetworkInfo.State.CONNECTED + || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) { + + // A network that has just connected has zero requests and is thus a foreground network. + networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND); + + if (!createNativeNetwork(networkAgent)) return; + if (networkAgent.supportsUnderlyingNetworks()) { + // Initialize the network's capabilities to their starting values according to the + // underlying networks. This ensures that the capabilities are correct before + // anything happens to the network. + updateCapabilitiesForNetwork(networkAgent); + } + networkAgent.created = true; + networkAgent.onNetworkCreated(); + } + + if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) { + networkAgent.everConnected = true; + + // NetworkCapabilities need to be set before sending the private DNS config to + // NetworkMonitor, otherwise NetworkMonitor cannot determine if validation is required. + networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities); + + handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig()); + updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties), + null); + + // Until parceled LinkProperties are sent directly to NetworkMonitor, the connect + // command must be sent after updating LinkProperties to maximize chances of + // NetworkMonitor seeing the correct LinkProperties when starting. + // TODO: pass LinkProperties to the NetworkMonitor in the notifyNetworkConnected call. + if (networkAgent.networkAgentConfig.acceptPartialConnectivity) { + networkAgent.networkMonitor().setAcceptPartialConnectivity(); + } + networkAgent.networkMonitor().notifyNetworkConnected( + new LinkProperties(networkAgent.linkProperties, + true /* parcelSensitiveFields */), + networkAgent.networkCapabilities); + scheduleUnvalidatedPrompt(networkAgent); + + // Whether a particular NetworkRequest listen should cause signal strength thresholds to + // be communicated to a particular NetworkAgent depends only on the network's immutable, + // capabilities, so it only needs to be done once on initial connect, not every time the + // network's capabilities change. Note that we do this before rematching the network, + // so we could decide to tear it down immediately afterwards. That's fine though - on + // disconnection NetworkAgents should stop any signal strength monitoring they have been + // doing. + updateSignalStrengthThresholds(networkAgent, "CONNECT", null); + + // Before first rematching networks, put an inactivity timer without any request, this + // allows {@code updateInactivityState} to update the state accordingly and prevent + // tearing down for any {@code unneeded} evaluation in this period. + // Note that the timer will not be rescheduled since the expiry time is + // fixed after connection regardless of the network satisfying other requests or not. + // But it will be removed as soon as the network satisfies a request for the first time. + networkAgent.lingerRequest(NetworkRequest.REQUEST_ID_NONE, + SystemClock.elapsedRealtime(), mNascentDelayMs); + networkAgent.setInactive(); + + // Consider network even though it is not yet validated. + rematchAllNetworksAndRequests(); + + // This has to happen after matching the requests, because callbacks are just requests. + notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK); + } else if (state == NetworkInfo.State.DISCONNECTED) { + networkAgent.disconnect(); + if (networkAgent.isVPN()) { + updateUids(networkAgent, networkAgent.networkCapabilities, null); + } + disconnectAndDestroyNetwork(networkAgent); + if (networkAgent.isVPN()) { + // As the active or bound network changes for apps, broadcast the default proxy, as + // apps may need to update their proxy data. This is called after disconnecting from + // VPN to make sure we do not broadcast the old proxy data. + // TODO(b/122649188): send the broadcast only to VPN users. + mProxyTracker.sendProxyBroadcast(); + } + } else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED || + state == NetworkInfo.State.SUSPENDED)) { + mLegacyTypeTracker.update(networkAgent); + } + } + + private void updateNetworkScore(@NonNull final NetworkAgentInfo nai, final NetworkScore score) { + if (VDBG || DDBG) log("updateNetworkScore for " + nai.toShortString() + " to " + score); + nai.setScore(score); + rematchAllNetworksAndRequests(); + } + + // Notify only this one new request of the current state. Transfer all the + // current state by calling NetworkCapabilities and LinkProperties callbacks + // so that callers can be guaranteed to have as close to atomicity in state + // transfer as can be supported by this current API. + protected void notifyNetworkAvailable(NetworkAgentInfo nai, NetworkRequestInfo nri) { + mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_REQUEST, nri); + if (nri.mPendingIntent != null) { + sendPendingIntentForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE); + // Attempt no subsequent state pushes where intents are involved. + return; + } + + final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE); + final boolean metered = nai.networkCapabilities.isMetered(); + final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges); + callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE, + getBlockedState(blockedReasons, metered, vpnBlocked)); + } + + // Notify the requests on this NAI that the network is now lingered. + private void notifyNetworkLosing(@NonNull final NetworkAgentInfo nai, final long now) { + final int lingerTime = (int) (nai.getInactivityExpiry() - now); + notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime); + } + + private static int getBlockedState(int reasons, boolean metered, boolean vpnBlocked) { + if (!metered) reasons &= ~BLOCKED_METERED_REASON_MASK; + return vpnBlocked + ? reasons | BLOCKED_REASON_LOCKDOWN_VPN + : reasons & ~BLOCKED_REASON_LOCKDOWN_VPN; + } + + private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) { + if (blockedReasons == BLOCKED_REASON_NONE) { + mUidBlockedReasons.delete(uid); + } else { + mUidBlockedReasons.put(uid, blockedReasons); + } + } + + /** + * Notify of the blocked state apps with a registered callback matching a given NAI. + * + * Unlike other callbacks, blocked status is different between each individual uid. So for + * any given nai, all requests need to be considered according to the uid who filed it. + * + * @param nai The target NetworkAgentInfo. + * @param oldMetered True if the previous network capabilities were metered. + * @param newMetered True if the current network capabilities are metered. + * @param oldBlockedUidRanges list of UID ranges previously blocked by lockdown VPN. + * @param newBlockedUidRanges list of UID ranges blocked by lockdown VPN. + */ + private void maybeNotifyNetworkBlocked(NetworkAgentInfo nai, boolean oldMetered, + boolean newMetered, List<UidRange> oldBlockedUidRanges, + List<UidRange> newBlockedUidRanges) { + + for (int i = 0; i < nai.numNetworkRequests(); i++) { + NetworkRequest nr = nai.requestAt(i); + NetworkRequestInfo nri = mNetworkRequests.get(nr); + + final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE); + final boolean oldVpnBlocked = isUidBlockedByVpn(nri.mAsUid, oldBlockedUidRanges); + final boolean newVpnBlocked = (oldBlockedUidRanges != newBlockedUidRanges) + ? isUidBlockedByVpn(nri.mAsUid, newBlockedUidRanges) + : oldVpnBlocked; + + final int oldBlockedState = getBlockedState(blockedReasons, oldMetered, oldVpnBlocked); + final int newBlockedState = getBlockedState(blockedReasons, newMetered, newVpnBlocked); + if (oldBlockedState != newBlockedState) { + callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED, + newBlockedState); + } + } + } + + /** + * Notify apps with a given UID of the new blocked state according to new uid state. + * @param uid The uid for which the rules changed. + * @param blockedReasons The reasons for why an uid is blocked. + */ + private void maybeNotifyNetworkBlockedForNewState(int uid, @BlockedReason int blockedReasons) { + for (final NetworkAgentInfo nai : mNetworkAgentInfos) { + final boolean metered = nai.networkCapabilities.isMetered(); + final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges); + + final int oldBlockedState = getBlockedState( + mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked); + final int newBlockedState = getBlockedState(blockedReasons, metered, vpnBlocked); + if (oldBlockedState == newBlockedState) { + continue; + } + for (int i = 0; i < nai.numNetworkRequests(); i++) { + NetworkRequest nr = nai.requestAt(i); + NetworkRequestInfo nri = mNetworkRequests.get(nr); + if (nri != null && nri.mAsUid == uid) { + callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED, + newBlockedState); + } + } + } + } + + @VisibleForTesting + protected void sendLegacyNetworkBroadcast(NetworkAgentInfo nai, DetailedState state, int type) { + // The NetworkInfo we actually send out has no bearing on the real + // state of affairs. For example, if the default connection is mobile, + // and a request for HIPRI has just gone away, we need to pretend that + // HIPRI has just disconnected. So we need to set the type to HIPRI and + // the state to DISCONNECTED, even though the network is of type MOBILE + // and is still connected. + NetworkInfo info = new NetworkInfo(nai.networkInfo); + info.setType(type); + filterForLegacyLockdown(info); + if (state != DetailedState.DISCONNECTED) { + info.setDetailedState(state, null, info.getExtraInfo()); + sendConnectedBroadcast(info); + } else { + info.setDetailedState(state, info.getReason(), info.getExtraInfo()); + Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info); + intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType()); + if (info.isFailover()) { + intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true); + nai.networkInfo.setFailover(false); + } + if (info.getReason() != null) { + intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason()); + } + if (info.getExtraInfo() != null) { + intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, info.getExtraInfo()); + } + NetworkAgentInfo newDefaultAgent = null; + if (nai.isSatisfyingRequest(mDefaultRequest.mRequests.get(0).requestId)) { + newDefaultAgent = mDefaultRequest.getSatisfier(); + if (newDefaultAgent != null) { + intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO, + newDefaultAgent.networkInfo); + } else { + intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true); + } + } + intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, + mDefaultInetConditionPublished); + sendStickyBroadcast(intent); + if (newDefaultAgent != null) { + sendConnectedBroadcast(newDefaultAgent.networkInfo); + } + } + } + + protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType, int arg1) { + if (VDBG || DDBG) { + String notification = ConnectivityManager.getCallbackName(notifyType); + log("notifyType " + notification + " for " + networkAgent.toShortString()); + } + for (int i = 0; i < networkAgent.numNetworkRequests(); i++) { + NetworkRequest nr = networkAgent.requestAt(i); + NetworkRequestInfo nri = mNetworkRequests.get(nr); + if (VDBG) log(" sending notification for " + nr); + if (nri.mPendingIntent == null) { + callCallbackForRequest(nri, networkAgent, notifyType, arg1); + } else { + sendPendingIntentForRequest(nri, networkAgent, notifyType); + } + } + } + + protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) { + notifyNetworkCallbacks(networkAgent, notifyType, 0); + } + + /** + * Returns the list of all interfaces that could be used by network traffic that does not + * explicitly specify a network. This includes the default network, but also all VPNs that are + * currently connected. + * + * Must be called on the handler thread. + */ + @NonNull + private ArrayList<Network> getDefaultNetworks() { + ensureRunningOnConnectivityServiceThread(); + final ArrayList<Network> defaultNetworks = new ArrayList<>(); + final Set<Integer> activeNetIds = new ArraySet<>(); + for (final NetworkRequestInfo nri : mDefaultNetworkRequests) { + if (nri.isBeingSatisfied()) { + activeNetIds.add(nri.getSatisfier().network().netId); + } + } + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + if (nai.everConnected && (activeNetIds.contains(nai.network().netId) || nai.isVPN())) { + defaultNetworks.add(nai.network); + } + } + return defaultNetworks; + } + + /** + * Notify NetworkStatsService that the set of active ifaces has changed, or that one of the + * active iface's tracked properties has changed. + */ + private void notifyIfacesChangedForNetworkStats() { + ensureRunningOnConnectivityServiceThread(); + String activeIface = null; + LinkProperties activeLinkProperties = getActiveLinkProperties(); + if (activeLinkProperties != null) { + activeIface = activeLinkProperties.getInterfaceName(); + } + + final UnderlyingNetworkInfo[] underlyingNetworkInfos = getAllVpnInfo(); + try { + final ArrayList<NetworkStateSnapshot> snapshots = new ArrayList<>(); + for (final NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) { + snapshots.add(snapshot); + } + mStatsManager.notifyNetworkStatus(getDefaultNetworks(), + snapshots, activeIface, Arrays.asList(underlyingNetworkInfos)); + } catch (Exception ignored) { + } + } + + @Override + public String getCaptivePortalServerUrl() { + enforceNetworkStackOrSettingsPermission(); + String settingUrl = mResources.get().getString( + R.string.config_networkCaptivePortalServerUrl); + + if (!TextUtils.isEmpty(settingUrl)) { + return settingUrl; + } + + settingUrl = Settings.Global.getString(mContext.getContentResolver(), + ConnectivitySettingsManager.CAPTIVE_PORTAL_HTTP_URL); + if (!TextUtils.isEmpty(settingUrl)) { + return settingUrl; + } + + return DEFAULT_CAPTIVE_PORTAL_HTTP_URL; + } + + @Override + public void startNattKeepalive(Network network, int intervalSeconds, + ISocketKeepaliveCallback cb, String srcAddr, int srcPort, String dstAddr) { + enforceKeepalivePermission(); + mKeepaliveTracker.startNattKeepalive( + getNetworkAgentInfoForNetwork(network), null /* fd */, + intervalSeconds, cb, + srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT); + } + + @Override + public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId, + int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr, + String dstAddr) { + try { + final FileDescriptor fd = pfd.getFileDescriptor(); + mKeepaliveTracker.startNattKeepalive( + getNetworkAgentInfoForNetwork(network), fd, resourceId, + intervalSeconds, cb, + srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT); + } finally { + // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks. + // startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately. + if (pfd != null && Binder.getCallingPid() != Process.myPid()) { + IoUtils.closeQuietly(pfd); + } + } + } + + @Override + public void startTcpKeepalive(Network network, ParcelFileDescriptor pfd, int intervalSeconds, + ISocketKeepaliveCallback cb) { + try { + enforceKeepalivePermission(); + final FileDescriptor fd = pfd.getFileDescriptor(); + mKeepaliveTracker.startTcpKeepalive( + getNetworkAgentInfoForNetwork(network), fd, intervalSeconds, cb); + } finally { + // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks. + // startTcpKeepalive calls Os.dup(fd) before returning, so we can close immediately. + if (pfd != null && Binder.getCallingPid() != Process.myPid()) { + IoUtils.closeQuietly(pfd); + } + } + } + + @Override + public void stopKeepalive(Network network, int slot) { + mHandler.sendMessage(mHandler.obtainMessage( + NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE, slot, SocketKeepalive.SUCCESS, network)); + } + + @Override + public void factoryReset() { + enforceSettingsPermission(); + + final int uid = mDeps.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + if (mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_NETWORK_RESET, + UserHandle.getUserHandleForUid(uid))) { + return; + } + + final IpMemoryStore ipMemoryStore = IpMemoryStore.getMemoryStore(mContext); + ipMemoryStore.factoryReset(); + + // Turn airplane mode off + setAirplaneMode(false); + + // restore private DNS settings to default mode (opportunistic) + if (!mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_CONFIG_PRIVATE_DNS, + UserHandle.getUserHandleForUid(uid))) { + ConnectivitySettingsManager.setPrivateDnsMode(mContext, + PRIVATE_DNS_MODE_OPPORTUNISTIC); + } + + Settings.Global.putString(mContext.getContentResolver(), + ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public byte[] getNetworkWatchlistConfigHash() { + NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class); + if (nwm == null) { + loge("Unable to get NetworkWatchlistManager"); + return null; + } + // Redirect it to network watchlist service to access watchlist file and calculate hash. + return nwm.getWatchlistConfigHash(); + } + + private void logNetworkEvent(NetworkAgentInfo nai, int evtype) { + int[] transports = nai.networkCapabilities.getTransportTypes(); + mMetricsLog.log(nai.network.getNetId(), transports, new NetworkEvent(evtype)); + } + + private static boolean toBool(int encodedBoolean) { + return encodedBoolean != 0; // Only 0 means false. + } + + private static int encodeBool(boolean b) { + return b ? 1 : 0; + } + + @Override + public int handleShellCommand(@NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + return new ShellCmd().exec(this, in.getFileDescriptor(), out.getFileDescriptor(), + err.getFileDescriptor(), args); + } + + private class ShellCmd extends BasicShellCommandHandler { + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + final PrintWriter pw = getOutPrintWriter(); + try { + switch (cmd) { + case "airplane-mode": + final String action = getNextArg(); + if ("enable".equals(action)) { + setAirplaneMode(true); + return 0; + } else if ("disable".equals(action)) { + setAirplaneMode(false); + return 0; + } else if (action == null) { + final ContentResolver cr = mContext.getContentResolver(); + final int enabled = Settings.Global.getInt(cr, + Settings.Global.AIRPLANE_MODE_ON); + pw.println(enabled == 0 ? "disabled" : "enabled"); + return 0; + } else { + onHelp(); + return -1; + } + default: + return handleDefaultCommands(cmd); + } + } catch (Exception e) { + pw.println(e); + } + return -1; + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("Connectivity service commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" airplane-mode [enable|disable]"); + pw.println(" Turn airplane mode on or off."); + pw.println(" airplane-mode"); + pw.println(" Get airplane mode."); + } + } + + private int getVpnType(@Nullable NetworkAgentInfo vpn) { + if (vpn == null) return VpnManager.TYPE_VPN_NONE; + final TransportInfo ti = vpn.networkCapabilities.getTransportInfo(); + if (!(ti instanceof VpnTransportInfo)) return VpnManager.TYPE_VPN_NONE; + return ((VpnTransportInfo) ti).getType(); + } + + /** + * @param connectionInfo the connection to resolve. + * @return {@code uid} if the connection is found and the app has permission to observe it + * (e.g., if it is associated with the calling VPN app's tunnel) or {@code INVALID_UID} if the + * connection is not found. + */ + public int getConnectionOwnerUid(ConnectionInfo connectionInfo) { + if (connectionInfo.protocol != IPPROTO_TCP && connectionInfo.protocol != IPPROTO_UDP) { + throw new IllegalArgumentException("Unsupported protocol " + connectionInfo.protocol); + } + + final int uid = mDeps.getConnectionOwnerUid(connectionInfo.protocol, + connectionInfo.local, connectionInfo.remote); + + if (uid == INVALID_UID) return uid; // Not found. + + // Connection owner UIDs are visible only to the network stack and to the VpnService-based + // VPN, if any, that applies to the UID that owns the connection. + if (checkNetworkStackPermission()) return uid; + + final NetworkAgentInfo vpn = getVpnForUid(uid); + if (vpn == null || getVpnType(vpn) != VpnManager.TYPE_VPN_SERVICE + || vpn.networkCapabilities.getOwnerUid() != mDeps.getCallingUid()) { + return INVALID_UID; + } + + return uid; + } + + /** + * Returns a IBinder to a TestNetworkService. Will be lazily created as needed. + * + * <p>The TestNetworkService must be run in the system server due to TUN creation. + */ + @Override + public IBinder startOrGetTestNetworkService() { + synchronized (mTNSLock) { + TestNetworkService.enforceTestNetworkPermissions(mContext); + + if (mTNS == null) { + mTNS = new TestNetworkService(mContext); + } + + return mTNS; + } + } + + /** + * Handler used for managing all Connectivity Diagnostics related functions. + * + * @see android.net.ConnectivityDiagnosticsManager + * + * TODO(b/147816404): Explore moving ConnectivityDiagnosticsHandler to a separate file + */ + @VisibleForTesting + class ConnectivityDiagnosticsHandler extends Handler { + private final String mTag = ConnectivityDiagnosticsHandler.class.getSimpleName(); + + /** + * Used to handle ConnectivityDiagnosticsCallback registration events from {@link + * android.net.ConnectivityDiagnosticsManager}. + * obj = ConnectivityDiagnosticsCallbackInfo with IConnectivityDiagnosticsCallback and + * NetworkRequestInfo to be registered + */ + private static final int EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK = 1; + + /** + * Used to handle ConnectivityDiagnosticsCallback unregister events from {@link + * android.net.ConnectivityDiagnosticsManager}. + * obj = the IConnectivityDiagnosticsCallback to be unregistered + * arg1 = the uid of the caller + */ + private static final int EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK = 2; + + /** + * Event for {@link NetworkStateTrackerHandler} to trigger ConnectivityReport callbacks + * after processing {@link #EVENT_NETWORK_TESTED} events. + * obj = {@link ConnectivityReportEvent} representing ConnectivityReport info reported from + * NetworkMonitor. + * data = PersistableBundle of extras passed from NetworkMonitor. + * + * <p>See {@link ConnectivityService#EVENT_NETWORK_TESTED}. + */ + private static final int EVENT_NETWORK_TESTED = ConnectivityService.EVENT_NETWORK_TESTED; + + /** + * Event for NetworkMonitor to inform ConnectivityService that a potential data stall has + * been detected on the network. + * obj = Long the timestamp (in millis) for when the suspected data stall was detected. + * arg1 = {@link DataStallReport#DetectionMethod} indicating the detection method. + * arg2 = NetID. + * data = PersistableBundle of extras passed from NetworkMonitor. + */ + private static final int EVENT_DATA_STALL_SUSPECTED = 4; + + /** + * Event for ConnectivityDiagnosticsHandler to handle network connectivity being reported to + * the platform. This event will invoke {@link + * IConnectivityDiagnosticsCallback#onNetworkConnectivityReported} for permissioned + * callbacks. + * obj = Network that was reported on + * arg1 = boolint for the quality reported + */ + private static final int EVENT_NETWORK_CONNECTIVITY_REPORTED = 5; + + private ConnectivityDiagnosticsHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK: { + handleRegisterConnectivityDiagnosticsCallback( + (ConnectivityDiagnosticsCallbackInfo) msg.obj); + break; + } + case EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK: { + handleUnregisterConnectivityDiagnosticsCallback( + (IConnectivityDiagnosticsCallback) msg.obj, msg.arg1); + break; + } + case EVENT_NETWORK_TESTED: { + final ConnectivityReportEvent reportEvent = + (ConnectivityReportEvent) msg.obj; + + handleNetworkTestedWithExtras(reportEvent, reportEvent.mExtras); + break; + } + case EVENT_DATA_STALL_SUSPECTED: { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2); + final Pair<Long, PersistableBundle> arg = + (Pair<Long, PersistableBundle>) msg.obj; + if (nai == null) break; + + handleDataStallSuspected(nai, arg.first, msg.arg1, arg.second); + break; + } + case EVENT_NETWORK_CONNECTIVITY_REPORTED: { + handleNetworkConnectivityReported((NetworkAgentInfo) msg.obj, toBool(msg.arg1)); + break; + } + default: { + Log.e(mTag, "Unrecognized event in ConnectivityDiagnostics: " + msg.what); + } + } + } + } + + /** Class used for cleaning up IConnectivityDiagnosticsCallback instances after their death. */ + @VisibleForTesting + class ConnectivityDiagnosticsCallbackInfo implements Binder.DeathRecipient { + @NonNull private final IConnectivityDiagnosticsCallback mCb; + @NonNull private final NetworkRequestInfo mRequestInfo; + @NonNull private final String mCallingPackageName; + + @VisibleForTesting + ConnectivityDiagnosticsCallbackInfo( + @NonNull IConnectivityDiagnosticsCallback cb, + @NonNull NetworkRequestInfo nri, + @NonNull String callingPackageName) { + mCb = cb; + mRequestInfo = nri; + mCallingPackageName = callingPackageName; + } + + @Override + public void binderDied() { + log("ConnectivityDiagnosticsCallback IBinder died."); + unregisterConnectivityDiagnosticsCallback(mCb); + } + } + + /** + * Class used for sending information from {@link + * NetworkMonitorCallbacks#notifyNetworkTestedWithExtras} to the handler for processing it. + */ + private static class NetworkTestedResults { + private final int mNetId; + private final int mTestResult; + private final long mTimestampMillis; + @Nullable private final String mRedirectUrl; + + private NetworkTestedResults( + int netId, int testResult, long timestampMillis, @Nullable String redirectUrl) { + mNetId = netId; + mTestResult = testResult; + mTimestampMillis = timestampMillis; + mRedirectUrl = redirectUrl; + } + } + + /** + * Class used for sending information from {@link NetworkStateTrackerHandler} to {@link + * ConnectivityDiagnosticsHandler}. + */ + private static class ConnectivityReportEvent { + private final long mTimestampMillis; + @NonNull private final NetworkAgentInfo mNai; + private final PersistableBundle mExtras; + + private ConnectivityReportEvent(long timestampMillis, @NonNull NetworkAgentInfo nai, + PersistableBundle p) { + mTimestampMillis = timestampMillis; + mNai = nai; + mExtras = p; + } + } + + private void handleRegisterConnectivityDiagnosticsCallback( + @NonNull ConnectivityDiagnosticsCallbackInfo cbInfo) { + ensureRunningOnConnectivityServiceThread(); + + final IConnectivityDiagnosticsCallback cb = cbInfo.mCb; + final IBinder iCb = cb.asBinder(); + final NetworkRequestInfo nri = cbInfo.mRequestInfo; + + // Connectivity Diagnostics are meant to be used with a single network request. It would be + // confusing for these networks to change when an NRI is satisfied in another layer. + if (nri.isMultilayerRequest()) { + throw new IllegalArgumentException("Connectivity Diagnostics do not support multilayer " + + "network requests."); + } + + // This means that the client registered the same callback multiple times. Do + // not override the previous entry, and exit silently. + if (mConnectivityDiagnosticsCallbacks.containsKey(iCb)) { + if (VDBG) log("Diagnostics callback is already registered"); + + // Decrement the reference count for this NetworkRequestInfo. The reference count is + // incremented when the NetworkRequestInfo is created as part of + // enforceRequestCountLimit(). + nri.decrementRequestCount(); + return; + } + + mConnectivityDiagnosticsCallbacks.put(iCb, cbInfo); + + try { + iCb.linkToDeath(cbInfo, 0); + } catch (RemoteException e) { + cbInfo.binderDied(); + return; + } + + // Once registered, provide ConnectivityReports for matching Networks + final List<NetworkAgentInfo> matchingNetworks = new ArrayList<>(); + synchronized (mNetworkForNetId) { + for (int i = 0; i < mNetworkForNetId.size(); i++) { + final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i); + // Connectivity Diagnostics rejects multilayer requests at registration hence get(0) + if (nai.satisfies(nri.mRequests.get(0))) { + matchingNetworks.add(nai); + } + } + } + for (final NetworkAgentInfo nai : matchingNetworks) { + final ConnectivityReport report = nai.getConnectivityReport(); + if (report == null) { + continue; + } + if (!checkConnectivityDiagnosticsPermissions( + nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) { + continue; + } + + try { + cb.onConnectivityReportAvailable(report); + } catch (RemoteException e) { + // Exception while sending the ConnectivityReport. Move on to the next network. + } + } + } + + private void handleUnregisterConnectivityDiagnosticsCallback( + @NonNull IConnectivityDiagnosticsCallback cb, int uid) { + ensureRunningOnConnectivityServiceThread(); + final IBinder iCb = cb.asBinder(); + + final ConnectivityDiagnosticsCallbackInfo cbInfo = + mConnectivityDiagnosticsCallbacks.remove(iCb); + if (cbInfo == null) { + if (VDBG) log("Removing diagnostics callback that is not currently registered"); + return; + } + + final NetworkRequestInfo nri = cbInfo.mRequestInfo; + + // Caller's UID must either be the registrants (if they are unregistering) or the System's + // (if the Binder died) + if (uid != nri.mUid && uid != Process.SYSTEM_UID) { + if (DBG) loge("Uid(" + uid + ") not registrant's (" + nri.mUid + ") or System's"); + return; + } + + // Decrement the reference count for this NetworkRequestInfo. The reference count is + // incremented when the NetworkRequestInfo is created as part of + // enforceRequestCountLimit(). + nri.decrementRequestCount(); + + iCb.unlinkToDeath(cbInfo, 0); + } + + private void handleNetworkTestedWithExtras( + @NonNull ConnectivityReportEvent reportEvent, @NonNull PersistableBundle extras) { + final NetworkAgentInfo nai = reportEvent.mNai; + final NetworkCapabilities networkCapabilities = + getNetworkCapabilitiesWithoutUids(nai.networkCapabilities); + final ConnectivityReport report = + new ConnectivityReport( + reportEvent.mNai.network, + reportEvent.mTimestampMillis, + nai.linkProperties, + networkCapabilities, + extras); + nai.setConnectivityReport(report); + final List<IConnectivityDiagnosticsCallback> results = + getMatchingPermissionedCallbacks(nai); + for (final IConnectivityDiagnosticsCallback cb : results) { + try { + cb.onConnectivityReportAvailable(report); + } catch (RemoteException ex) { + loge("Error invoking onConnectivityReport", ex); + } + } + } + + private void handleDataStallSuspected( + @NonNull NetworkAgentInfo nai, long timestampMillis, int detectionMethod, + @NonNull PersistableBundle extras) { + final NetworkCapabilities networkCapabilities = + getNetworkCapabilitiesWithoutUids(nai.networkCapabilities); + final DataStallReport report = + new DataStallReport( + nai.network, + timestampMillis, + detectionMethod, + nai.linkProperties, + networkCapabilities, + extras); + final List<IConnectivityDiagnosticsCallback> results = + getMatchingPermissionedCallbacks(nai); + for (final IConnectivityDiagnosticsCallback cb : results) { + try { + cb.onDataStallSuspected(report); + } catch (RemoteException ex) { + loge("Error invoking onDataStallSuspected", ex); + } + } + } + + private void handleNetworkConnectivityReported( + @NonNull NetworkAgentInfo nai, boolean connectivity) { + final List<IConnectivityDiagnosticsCallback> results = + getMatchingPermissionedCallbacks(nai); + for (final IConnectivityDiagnosticsCallback cb : results) { + try { + cb.onNetworkConnectivityReported(nai.network, connectivity); + } catch (RemoteException ex) { + loge("Error invoking onNetworkConnectivityReported", ex); + } + } + } + + private NetworkCapabilities getNetworkCapabilitiesWithoutUids(@NonNull NetworkCapabilities nc) { + final NetworkCapabilities sanitized = new NetworkCapabilities(nc, + NetworkCapabilities.REDACT_ALL); + sanitized.setUids(null); + sanitized.setAdministratorUids(new int[0]); + sanitized.setOwnerUid(Process.INVALID_UID); + return sanitized; + } + + private List<IConnectivityDiagnosticsCallback> getMatchingPermissionedCallbacks( + @NonNull NetworkAgentInfo nai) { + final List<IConnectivityDiagnosticsCallback> results = new ArrayList<>(); + for (Entry<IBinder, ConnectivityDiagnosticsCallbackInfo> entry : + mConnectivityDiagnosticsCallbacks.entrySet()) { + final ConnectivityDiagnosticsCallbackInfo cbInfo = entry.getValue(); + final NetworkRequestInfo nri = cbInfo.mRequestInfo; + // Connectivity Diagnostics rejects multilayer requests at registration hence get(0). + if (nai.satisfies(nri.mRequests.get(0))) { + if (checkConnectivityDiagnosticsPermissions( + nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) { + results.add(entry.getValue().mCb); + } + } + } + return results; + } + + private boolean hasLocationPermission(String packageName, int uid) { + // LocationPermissionChecker#checkLocationPermission can throw SecurityException if the uid + // and package name don't match. Throwing on the CS thread is not acceptable, so wrap the + // call in a try-catch. + try { + if (!mLocationPermissionChecker.checkLocationPermission( + packageName, null /* featureId */, uid, null /* message */)) { + return false; + } + } catch (SecurityException e) { + return false; + } + + return true; + } + + private boolean ownsVpnRunningOverNetwork(int uid, Network network) { + for (NetworkAgentInfo virtual : mNetworkAgentInfos) { + if (virtual.supportsUnderlyingNetworks() + && virtual.networkCapabilities.getOwnerUid() == uid + && CollectionUtils.contains(virtual.declaredUnderlyingNetworks, network)) { + return true; + } + } + + return false; + } + + @VisibleForTesting + boolean checkConnectivityDiagnosticsPermissions( + int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) { + if (checkNetworkStackPermission(callbackPid, callbackUid)) { + return true; + } + + // Administrator UIDs also contains the Owner UID + final int[] administratorUids = nai.networkCapabilities.getAdministratorUids(); + if (!CollectionUtils.contains(administratorUids, callbackUid) + && !ownsVpnRunningOverNetwork(callbackUid, nai.network)) { + return false; + } + + return hasLocationPermission(callbackPackageName, callbackUid); + } + + @Override + public void registerConnectivityDiagnosticsCallback( + @NonNull IConnectivityDiagnosticsCallback callback, + @NonNull NetworkRequest request, + @NonNull String callingPackageName) { + if (request.legacyType != TYPE_NONE) { + throw new IllegalArgumentException("ConnectivityManager.TYPE_* are deprecated." + + " Please use NetworkCapabilities instead."); + } + final int callingUid = mDeps.getCallingUid(); + mAppOpsManager.checkPackage(callingUid, callingPackageName); + + // This NetworkCapabilities is only used for matching to Networks. Clear out its owner uid + // and administrator uids to be safe. + final NetworkCapabilities nc = new NetworkCapabilities(request.networkCapabilities); + restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName); + + final NetworkRequest requestWithId = + new NetworkRequest( + nc, TYPE_NONE, nextNetworkRequestId(), NetworkRequest.Type.LISTEN); + + // NetworkRequestInfos created here count towards MAX_NETWORK_REQUESTS_PER_UID limit. + // + // nri is not bound to the death of callback. Instead, callback.bindToDeath() is set in + // handleRegisterConnectivityDiagnosticsCallback(). nri will be cleaned up as part of the + // callback's binder death. + final NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, requestWithId); + final ConnectivityDiagnosticsCallbackInfo cbInfo = + new ConnectivityDiagnosticsCallbackInfo(callback, nri, callingPackageName); + + mConnectivityDiagnosticsHandler.sendMessage( + mConnectivityDiagnosticsHandler.obtainMessage( + ConnectivityDiagnosticsHandler + .EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK, + cbInfo)); + } + + @Override + public void unregisterConnectivityDiagnosticsCallback( + @NonNull IConnectivityDiagnosticsCallback callback) { + Objects.requireNonNull(callback, "callback must be non-null"); + mConnectivityDiagnosticsHandler.sendMessage( + mConnectivityDiagnosticsHandler.obtainMessage( + ConnectivityDiagnosticsHandler + .EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK, + mDeps.getCallingUid(), + 0, + callback)); + } + + @Override + public void simulateDataStall(int detectionMethod, long timestampMillis, + @NonNull Network network, @NonNull PersistableBundle extras) { + enforceAnyPermissionOf(android.Manifest.permission.MANAGE_TEST_NETWORKS, + android.Manifest.permission.NETWORK_STACK); + final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network); + if (!nc.hasTransport(TRANSPORT_TEST)) { + throw new SecurityException("Data Stall simluation is only possible for test networks"); + } + + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network); + if (nai == null || nai.creatorUid != mDeps.getCallingUid()) { + throw new SecurityException("Data Stall simulation is only possible for network " + + "creators"); + } + + // Instead of passing the data stall directly to the ConnectivityDiagnostics handler, treat + // this as a Data Stall received directly from NetworkMonitor. This requires wrapping the + // Data Stall information as a DataStallReportParcelable and passing to + // #notifyDataStallSuspected. This ensures that unknown Data Stall detection methods are + // still passed to ConnectivityDiagnostics (with new detection methods masked). + final DataStallReportParcelable p = new DataStallReportParcelable(); + p.timestampMillis = timestampMillis; + p.detectionMethod = detectionMethod; + + if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) { + p.dnsConsecutiveTimeouts = extras.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS); + } + if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) { + p.tcpPacketFailRate = extras.getInt(KEY_TCP_PACKET_FAIL_RATE); + p.tcpMetricsCollectionPeriodMillis = extras.getInt( + KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS); + } + + notifyDataStallSuspected(p, network.getNetId()); + } + + private class NetdCallback extends BaseNetdUnsolicitedEventListener { + @Override + public void onInterfaceClassActivityChanged(boolean isActive, int transportType, + long timestampNs, int uid) { + mNetworkActivityTracker.setAndReportNetworkActive(isActive, transportType, timestampNs); + } + + @Override + public void onInterfaceLinkStateChanged(String iface, boolean up) { + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + nai.clatd.interfaceLinkStateChanged(iface, up); + } + } + + @Override + public void onInterfaceRemoved(String iface) { + for (NetworkAgentInfo nai : mNetworkAgentInfos) { + nai.clatd.interfaceRemoved(iface); + } + } + } + + private final LegacyNetworkActivityTracker mNetworkActivityTracker; + + /** + * Class used for updating network activity tracking with netd and notify network activity + * changes. + */ + private static final class LegacyNetworkActivityTracker { + private static final int NO_UID = -1; + private final Context mContext; + private final INetd mNetd; + private final RemoteCallbackList<INetworkActivityListener> mNetworkActivityListeners = + new RemoteCallbackList<>(); + // Indicate the current system default network activity is active or not. + @GuardedBy("mActiveIdleTimers") + private boolean mNetworkActive; + @GuardedBy("mActiveIdleTimers") + private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap(); + private final Handler mHandler; + + private class IdleTimerParams { + public final int timeout; + public final int transportType; + + IdleTimerParams(int timeout, int transport) { + this.timeout = timeout; + this.transportType = transport; + } + } + + LegacyNetworkActivityTracker(@NonNull Context context, @NonNull Handler handler, + @NonNull INetd netd) { + mContext = context; + mNetd = netd; + mHandler = handler; + } + + public void setAndReportNetworkActive(boolean active, int transportType, long tsNanos) { + sendDataActivityBroadcast(transportTypeToLegacyType(transportType), active, tsNanos); + synchronized (mActiveIdleTimers) { + mNetworkActive = active; + // If there are no idle timers, it means that system is not monitoring + // activity, so the system default network for those default network + // unspecified apps is always considered active. + // + // TODO: If the mActiveIdleTimers is empty, netd will actually not send + // any network activity change event. Whenever this event is received, + // the mActiveIdleTimers should be always not empty. The legacy behavior + // is no-op. Remove to refer to mNetworkActive only. + if (mNetworkActive || mActiveIdleTimers.isEmpty()) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_REPORT_NETWORK_ACTIVITY)); + } + } + } + + // The network activity should only be updated from ConnectivityService handler thread + // when mActiveIdleTimers lock is held. + @GuardedBy("mActiveIdleTimers") + private void reportNetworkActive() { + final int length = mNetworkActivityListeners.beginBroadcast(); + if (DDBG) log("reportNetworkActive, notify " + length + " listeners"); + try { + for (int i = 0; i < length; i++) { + try { + mNetworkActivityListeners.getBroadcastItem(i).onNetworkActive(); + } catch (RemoteException | RuntimeException e) { + loge("Fail to send network activie to listener " + e); + } + } + } finally { + mNetworkActivityListeners.finishBroadcast(); + } + } + + @GuardedBy("mActiveIdleTimers") + public void handleReportNetworkActivity() { + synchronized (mActiveIdleTimers) { + reportNetworkActive(); + } + } + + // This is deprecated and only to support legacy use cases. + private int transportTypeToLegacyType(int type) { + switch (type) { + case NetworkCapabilities.TRANSPORT_CELLULAR: + return TYPE_MOBILE; + case NetworkCapabilities.TRANSPORT_WIFI: + return TYPE_WIFI; + case NetworkCapabilities.TRANSPORT_BLUETOOTH: + return TYPE_BLUETOOTH; + case NetworkCapabilities.TRANSPORT_ETHERNET: + return TYPE_ETHERNET; + default: + loge("Unexpected transport in transportTypeToLegacyType: " + type); + } + return ConnectivityManager.TYPE_NONE; + } + + public void sendDataActivityBroadcast(int deviceType, boolean active, long tsNanos) { + final Intent intent = new Intent(ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE); + intent.putExtra(ConnectivityManager.EXTRA_DEVICE_TYPE, deviceType); + intent.putExtra(ConnectivityManager.EXTRA_IS_ACTIVE, active); + intent.putExtra(ConnectivityManager.EXTRA_REALTIME_NS, tsNanos); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, + RECEIVE_DATA_ACTIVITY_CHANGE, + null /* resultReceiver */, + null /* scheduler */, + 0 /* initialCode */, + null /* initialData */, + null /* initialExtra */); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * Setup data activity tracking for the given network. + * + * Every {@code setupDataActivityTracking} should be paired with a + * {@link #removeDataActivityTracking} for cleanup. + */ + private void setupDataActivityTracking(NetworkAgentInfo networkAgent) { + final String iface = networkAgent.linkProperties.getInterfaceName(); + + final int timeout; + final int type; + + if (networkAgent.networkCapabilities.hasTransport( + NetworkCapabilities.TRANSPORT_CELLULAR)) { + timeout = Settings.Global.getInt(mContext.getContentResolver(), + ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE, + 10); + type = NetworkCapabilities.TRANSPORT_CELLULAR; + } else if (networkAgent.networkCapabilities.hasTransport( + NetworkCapabilities.TRANSPORT_WIFI)) { + timeout = Settings.Global.getInt(mContext.getContentResolver(), + ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_WIFI, + 15); + type = NetworkCapabilities.TRANSPORT_WIFI; + } else { + return; // do not track any other networks + } + + updateRadioPowerState(true /* isActive */, type); + + if (timeout > 0 && iface != null) { + try { + synchronized (mActiveIdleTimers) { + // Networks start up. + mNetworkActive = true; + mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type)); + mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type)); + reportNetworkActive(); + } + } catch (Exception e) { + // You shall not crash! + loge("Exception in setupDataActivityTracking " + e); + } + } + } + + /** + * Remove data activity tracking when network disconnects. + */ + private void removeDataActivityTracking(NetworkAgentInfo networkAgent) { + final String iface = networkAgent.linkProperties.getInterfaceName(); + final NetworkCapabilities caps = networkAgent.networkCapabilities; + + if (iface == null) return; + + final int type; + if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + type = NetworkCapabilities.TRANSPORT_CELLULAR; + } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + type = NetworkCapabilities.TRANSPORT_WIFI; + } else { + return; // do not track any other networks + } + + try { + updateRadioPowerState(false /* isActive */, type); + synchronized (mActiveIdleTimers) { + final IdleTimerParams params = mActiveIdleTimers.remove(iface); + // The call fails silently if no idle timer setup for this interface + mNetd.idletimerRemoveInterface(iface, params.timeout, + Integer.toString(params.transportType)); + } + } catch (Exception e) { + // You shall not crash! + loge("Exception in removeDataActivityTracking " + e); + } + } + + /** + * Update data activity tracking when network state is updated. + */ + public void updateDataActivityTracking(NetworkAgentInfo newNetwork, + NetworkAgentInfo oldNetwork) { + if (newNetwork != null) { + setupDataActivityTracking(newNetwork); + } + if (oldNetwork != null) { + removeDataActivityTracking(oldNetwork); + } + } + + private void updateRadioPowerState(boolean isActive, int transportType) { + final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class); + switch (transportType) { + case NetworkCapabilities.TRANSPORT_CELLULAR: + bs.reportMobileRadioPowerState(isActive, NO_UID); + break; + case NetworkCapabilities.TRANSPORT_WIFI: + bs.reportWifiRadioPowerState(isActive, NO_UID); + break; + default: + logw("Untracked transport type:" + transportType); + } + } + + public boolean isDefaultNetworkActive() { + synchronized (mActiveIdleTimers) { + // If there are no idle timers, it means that system is not monitoring activity, + // so the default network is always considered active. + // + // TODO : Distinguish between the cases where mActiveIdleTimers is empty because + // tracking is disabled (negative idle timer value configured), or no active default + // network. In the latter case, this reports active but it should report inactive. + return mNetworkActive || mActiveIdleTimers.isEmpty(); + } + } + + public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) { + mNetworkActivityListeners.register(l); + } + + public void unregisterNetworkActivityListener(@NonNull INetworkActivityListener l) { + mNetworkActivityListeners.unregister(l); + } + + public void dump(IndentingPrintWriter pw) { + synchronized (mActiveIdleTimers) { + pw.print("mNetworkActive="); pw.println(mNetworkActive); + pw.println("Idle timers:"); + for (HashMap.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) { + pw.print(" "); pw.print(ent.getKey()); pw.println(":"); + final IdleTimerParams params = ent.getValue(); + pw.print(" timeout="); pw.print(params.timeout); + pw.print(" type="); pw.println(params.transportType); + } + } + } + } + + /** + * Registers {@link QosSocketFilter} with {@link IQosCallback}. + * + * @param socketInfo the socket information + * @param callback the callback to register + */ + @Override + public void registerQosSocketCallback(@NonNull final QosSocketInfo socketInfo, + @NonNull final IQosCallback callback) { + final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(socketInfo.getNetwork()); + if (nai == null || nai.networkCapabilities == null) { + try { + callback.onError(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED); + } catch (final RemoteException ex) { + loge("registerQosCallbackInternal: RemoteException", ex); + } + return; + } + registerQosCallbackInternal(new QosSocketFilter(socketInfo), callback, nai); + } + + /** + * Register a {@link IQosCallback} with base {@link QosFilter}. + * + * @param filter the filter to register + * @param callback the callback to register + * @param nai the agent information related to the filter's network + */ + @VisibleForTesting + public void registerQosCallbackInternal(@NonNull final QosFilter filter, + @NonNull final IQosCallback callback, @NonNull final NetworkAgentInfo nai) { + if (filter == null) throw new IllegalArgumentException("filter must be non-null"); + if (callback == null) throw new IllegalArgumentException("callback must be non-null"); + + if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) { + enforceConnectivityRestrictedNetworksPermission(); + } + mQosCallbackTracker.registerCallback(callback, filter, nai); + } + + /** + * Unregisters the given callback. + * + * @param callback the callback to unregister + */ + @Override + public void unregisterQosCallback(@NonNull final IQosCallback callback) { + Objects.requireNonNull(callback, "callback must be non-null"); + mQosCallbackTracker.unregisterCallback(callback); + } + + // Network preference per-profile and OEM network preferences can't be set at the same + // time, because it is unclear what should happen if both preferences are active for + // one given UID. To make it possible, the stack would have to clarify what would happen + // in case both are active at the same time. The implementation may have to be adjusted + // to implement the resulting rules. For example, a priority could be defined between them, + // where the OEM preference would be considered less or more important than the enterprise + // preference ; this would entail implementing the priorities somehow, e.g. by doing + // UID arithmetic with UID ranges or passing a priority to netd so that the routing rules + // are set at the right level. Other solutions are possible, e.g. merging of the + // preferences for the relevant UIDs. + private static void throwConcurrentPreferenceException() { + throw new IllegalStateException("Can't set NetworkPreferenceForUser and " + + "set OemNetworkPreference at the same time"); + } + + /** + * Request that a user profile is put by default on a network matching a given preference. + * + * See the documentation for the individual preferences for a description of the supported + * behaviors. + * + * @param profile the profile concerned. + * @param preference the preference for this profile, as one of the PROFILE_NETWORK_PREFERENCE_* + * constants. + * @param listener an optional listener to listen for completion of the operation. + */ + @Override + public void setProfileNetworkPreference(@NonNull final UserHandle profile, + @ConnectivityManager.ProfileNetworkPreference final int preference, + @Nullable final IOnCompleteListener listener) { + Objects.requireNonNull(profile); + PermissionUtils.enforceNetworkStackPermission(mContext); + if (DBG) { + log("setProfileNetworkPreference " + profile + " to " + preference); + } + if (profile.getIdentifier() < 0) { + throw new IllegalArgumentException("Must explicitly specify a user handle (" + + "UserHandle.CURRENT not supported)"); + } + final UserManager um = mContext.getSystemService(UserManager.class); + if (!um.isManagedProfile(profile.getIdentifier())) { + throw new IllegalArgumentException("Profile must be a managed profile"); + } + // Strictly speaking, mOemNetworkPreferences should only be touched on the + // handler thread. However it is an immutable object, so reading the reference is + // safe - it's just possible the value is slightly outdated. For the final check, + // see #handleSetProfileNetworkPreference. But if this can be caught here it is a + // lot easier to understand, so opportunistically check it. + if (!mOemNetworkPreferences.isEmpty()) { + throwConcurrentPreferenceException(); + } + final NetworkCapabilities nc; + switch (preference) { + case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT: + nc = null; + break; + case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE: + final UidRange uids = UidRange.createForUser(profile); + nc = createDefaultNetworkCapabilitiesForUidRange(uids); + nc.addCapability(NET_CAPABILITY_ENTERPRISE); + nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + break; + default: + throw new IllegalArgumentException( + "Invalid preference in setProfileNetworkPreference"); + } + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE, + new Pair<>(new ProfileNetworkPreferences.Preference(profile, nc), listener))); + } + + private void validateNetworkCapabilitiesOfProfileNetworkPreference( + @Nullable final NetworkCapabilities nc) { + if (null == nc) return; // Null caps are always allowed. It means to remove the setting. + ensureRequestableCapabilities(nc); + } + + private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences( + @NonNull final ProfileNetworkPreferences prefs) { + final ArraySet<NetworkRequestInfo> result = new ArraySet<>(); + for (final ProfileNetworkPreferences.Preference pref : prefs.preferences) { + // The NRI for a user should be comprised of two layers: + // - The request for the capabilities + // - The request for the default network, for fallback. Create an image of it to + // have the correct UIDs in it (also a request can only be part of one NRI, because + // of lookups in 1:1 associations like mNetworkRequests). + // Note that denying a fallback can be implemented simply by not adding the second + // request. + final ArrayList<NetworkRequest> nrs = new ArrayList<>(); + nrs.add(createNetworkRequest(NetworkRequest.Type.REQUEST, pref.capabilities)); + nrs.add(createDefaultInternetRequestForTransport( + TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT)); + setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids())); + final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs); + result.add(nri); + } + return result; + } + + private void handleSetProfileNetworkPreference( + @NonNull final ProfileNetworkPreferences.Preference preference, + @Nullable final IOnCompleteListener listener) { + // setProfileNetworkPreference and setOemNetworkPreference are mutually exclusive, in + // particular because it's not clear what preference should win in case both apply + // to the same app. + // The binder call has already checked this, but as mOemNetworkPreferences is only + // touched on the handler thread, it's theoretically not impossible that it has changed + // since. + if (!mOemNetworkPreferences.isEmpty()) { + // This may happen on a device with an OEM preference set when a user is removed. + // In this case, it's safe to ignore. In particular this happens in the tests. + loge("handleSetProfileNetworkPreference, but OEM network preferences not empty"); + return; + } + + validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities); + + mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference); + mSystemNetworkRequestCounter.transact( + mDeps.getCallingUid(), mProfileNetworkPreferences.preferences.size(), + () -> { + final ArraySet<NetworkRequestInfo> nris = + createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences); + replaceDefaultNetworkRequestsForPreference(nris); + }); + // Finally, rematch. + rematchAllNetworksAndRequests(); + + if (null != listener) { + try { + listener.onComplete(); + } catch (RemoteException e) { + loge("Listener for setProfileNetworkPreference has died"); + } + } + } + + private void enforceAutomotiveDevice() { + final boolean isAutomotiveDevice = + mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); + if (!isAutomotiveDevice) { + throw new UnsupportedOperationException( + "setOemNetworkPreference() is only available on automotive devices."); + } + } + + /** + * Used by automotive devices to set the network preferences used to direct traffic at an + * application level as per the given OemNetworkPreferences. An example use-case would be an + * automotive OEM wanting to provide connectivity for applications critical to the usage of a + * vehicle via a particular network. + * + * Calling this will overwrite the existing preference. + * + * @param preference {@link OemNetworkPreferences} The application network preference to be set. + * @param listener {@link ConnectivityManager.OnCompleteListener} Listener used + * to communicate completion of setOemNetworkPreference(); + */ + @Override + public void setOemNetworkPreference( + @NonNull final OemNetworkPreferences preference, + @Nullable final IOnCompleteListener listener) { + + enforceAutomotiveDevice(); + enforceOemNetworkPreferencesPermission(); + + if (!mProfileNetworkPreferences.isEmpty()) { + // Strictly speaking, mProfileNetworkPreferences should only be touched on the + // handler thread. However it is an immutable object, so reading the reference is + // safe - it's just possible the value is slightly outdated. For the final check, + // see #handleSetOemPreference. But if this can be caught here it is a + // lot easier to understand, so opportunistically check it. + throwConcurrentPreferenceException(); + } + + Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null"); + validateOemNetworkPreferences(preference); + mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_OEM_NETWORK_PREFERENCE, + new Pair<>(preference, listener))); + } + + private void validateOemNetworkPreferences(@NonNull OemNetworkPreferences preference) { + for (@OemNetworkPreferences.OemNetworkPreference final int pref + : preference.getNetworkPreferences().values()) { + if (OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED == pref) { + final String msg = "OEM_NETWORK_PREFERENCE_UNINITIALIZED is an invalid value."; + throw new IllegalArgumentException(msg); + } + } + } + + private void handleSetOemNetworkPreference( + @NonNull final OemNetworkPreferences preference, + @Nullable final IOnCompleteListener listener) { + Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null"); + if (DBG) { + log("set OEM network preferences :" + preference.toString()); + } + // setProfileNetworkPreference and setOemNetworkPreference are mutually exclusive, in + // particular because it's not clear what preference should win in case both apply + // to the same app. + // The binder call has already checked this, but as mOemNetworkPreferences is only + // touched on the handler thread, it's theoretically not impossible that it has changed + // since. + if (!mProfileNetworkPreferences.isEmpty()) { + logwtf("handleSetOemPreference, but per-profile network preferences not empty"); + return; + } + + mOemNetworkPreferencesLogs.log("UPDATE INITIATED: " + preference); + final int uniquePreferenceCount = new ArraySet<>( + preference.getNetworkPreferences().values()).size(); + mSystemNetworkRequestCounter.transact( + mDeps.getCallingUid(), uniquePreferenceCount, + () -> { + final ArraySet<NetworkRequestInfo> nris = + new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences(preference); + replaceDefaultNetworkRequestsForPreference(nris); + }); + mOemNetworkPreferences = preference; + + if (null != listener) { + try { + listener.onComplete(); + } catch (RemoteException e) { + loge("Can't send onComplete in handleSetOemNetworkPreference", e); + } + } + } + + private void replaceDefaultNetworkRequestsForPreference( + @NonNull final Set<NetworkRequestInfo> nris) { + // Pass in a defensive copy as this collection will be updated on remove. + handleRemoveNetworkRequests(new ArraySet<>(mDefaultNetworkRequests)); + addPerAppDefaultNetworkRequests(nris); + } + + private void addPerAppDefaultNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) { + ensureRunningOnConnectivityServiceThread(); + mDefaultNetworkRequests.addAll(nris); + final ArraySet<NetworkRequestInfo> perAppCallbackRequestsToUpdate = + getPerAppCallbackRequestsToUpdate(); + final ArraySet<NetworkRequestInfo> nrisToRegister = new ArraySet<>(nris); + mSystemNetworkRequestCounter.transact( + mDeps.getCallingUid(), perAppCallbackRequestsToUpdate.size(), + () -> { + nrisToRegister.addAll( + createPerAppCallbackRequestsToRegister(perAppCallbackRequestsToUpdate)); + handleRemoveNetworkRequests(perAppCallbackRequestsToUpdate); + handleRegisterNetworkRequests(nrisToRegister); + }); + } + + /** + * All current requests that are tracking the default network need to be assessed as to whether + * or not the current set of per-application default requests will be changing their default + * network. If so, those requests will need to be updated so that they will send callbacks for + * default network changes at the appropriate time. Additionally, those requests tracking the + * default that were previously updated by this flow will need to be reassessed. + * @return the nris which will need to be updated. + */ + private ArraySet<NetworkRequestInfo> getPerAppCallbackRequestsToUpdate() { + final ArraySet<NetworkRequestInfo> defaultCallbackRequests = new ArraySet<>(); + // Get the distinct nris to check since for multilayer requests, it is possible to have the + // same nri in the map's values for each of its NetworkRequest objects. + final ArraySet<NetworkRequestInfo> nris = new ArraySet<>(mNetworkRequests.values()); + for (final NetworkRequestInfo nri : nris) { + // Include this nri if it is currently being tracked. + if (isPerAppTrackedNri(nri)) { + defaultCallbackRequests.add(nri); + continue; + } + // We only track callbacks for requests tracking the default. + if (NetworkRequest.Type.TRACK_DEFAULT != nri.mRequests.get(0).type) { + continue; + } + // Include this nri if it will be tracked by the new per-app default requests. + final boolean isNriGoingToBeTracked = + getDefaultRequestTrackingUid(nri.mAsUid) != mDefaultRequest; + if (isNriGoingToBeTracked) { + defaultCallbackRequests.add(nri); + } + } + return defaultCallbackRequests; + } + + /** + * Create nris for those network requests that are currently tracking the default network that + * are being controlled by a per-application default. + * @param perAppCallbackRequestsForUpdate the baseline network requests to be used as the + * foundation when creating the nri. Important items include the calling uid's original + * NetworkRequest to be used when mapping callbacks as well as the caller's uid and name. These + * requests are assumed to have already been validated as needing to be updated. + * @return the Set of nris to use when registering network requests. + */ + private ArraySet<NetworkRequestInfo> createPerAppCallbackRequestsToRegister( + @NonNull final ArraySet<NetworkRequestInfo> perAppCallbackRequestsForUpdate) { + final ArraySet<NetworkRequestInfo> callbackRequestsToRegister = new ArraySet<>(); + for (final NetworkRequestInfo callbackRequest : perAppCallbackRequestsForUpdate) { + final NetworkRequestInfo trackingNri = + getDefaultRequestTrackingUid(callbackRequest.mAsUid); + + // If this nri is not being tracked, the change it back to an untracked nri. + if (trackingNri == mDefaultRequest) { + callbackRequestsToRegister.add(new NetworkRequestInfo( + callbackRequest, + Collections.singletonList(callbackRequest.getNetworkRequestForCallback()))); + continue; + } + + final NetworkRequest request = callbackRequest.mRequests.get(0); + callbackRequestsToRegister.add(new NetworkRequestInfo( + callbackRequest, + copyNetworkRequestsForUid( + trackingNri.mRequests, callbackRequest.mAsUid, + callbackRequest.mUid, request.getRequestorPackageName()))); + } + return callbackRequestsToRegister; + } + + private static void setNetworkRequestUids(@NonNull final List<NetworkRequest> requests, + @NonNull final Set<UidRange> uids) { + for (final NetworkRequest req : requests) { + req.networkCapabilities.setUids(UidRange.toIntRanges(uids)); + } + } + + /** + * Class used to generate {@link NetworkRequestInfo} based off of {@link OemNetworkPreferences}. + */ + @VisibleForTesting + final class OemNetworkRequestFactory { + ArraySet<NetworkRequestInfo> createNrisFromOemNetworkPreferences( + @NonNull final OemNetworkPreferences preference) { + final ArraySet<NetworkRequestInfo> nris = new ArraySet<>(); + final SparseArray<Set<Integer>> uids = + createUidsFromOemNetworkPreferences(preference); + for (int i = 0; i < uids.size(); i++) { + final int key = uids.keyAt(i); + final Set<Integer> value = uids.valueAt(i); + final NetworkRequestInfo nri = createNriFromOemNetworkPreferences(key, value); + // No need to add an nri without any requests. + if (0 == nri.mRequests.size()) { + continue; + } + nris.add(nri); + } + + return nris; + } + + private SparseArray<Set<Integer>> createUidsFromOemNetworkPreferences( + @NonNull final OemNetworkPreferences preference) { + final SparseArray<Set<Integer>> uids = new SparseArray<>(); + final PackageManager pm = mContext.getPackageManager(); + final List<UserHandle> users = + mContext.getSystemService(UserManager.class).getUserHandles(true); + if (null == users || users.size() == 0) { + if (VDBG || DDBG) { + log("No users currently available for setting the OEM network preference."); + } + return uids; + } + for (final Map.Entry<String, Integer> entry : + preference.getNetworkPreferences().entrySet()) { + @OemNetworkPreferences.OemNetworkPreference final int pref = entry.getValue(); + try { + final int uid = pm.getApplicationInfo(entry.getKey(), 0).uid; + if (!uids.contains(pref)) { + uids.put(pref, new ArraySet<>()); + } + for (final UserHandle ui : users) { + // Add the rules for all users as this policy is device wide. + uids.get(pref).add(ui.getUid(uid)); + } + } catch (PackageManager.NameNotFoundException e) { + // Although this may seem like an error scenario, it is ok that uninstalled + // packages are sent on a network preference as the system will watch for + // package installations associated with this network preference and update + // accordingly. This is done so as to minimize race conditions on app install. + continue; + } + } + return uids; + } + + private NetworkRequestInfo createNriFromOemNetworkPreferences( + @OemNetworkPreferences.OemNetworkPreference final int preference, + @NonNull final Set<Integer> uids) { + final List<NetworkRequest> requests = new ArrayList<>(); + // Requests will ultimately be evaluated by order of insertion therefore it matters. + switch (preference) { + case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID: + requests.add(createUnmeteredNetworkRequest()); + requests.add(createOemPaidNetworkRequest()); + requests.add(createDefaultInternetRequestForTransport( + TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT)); + break; + case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK: + requests.add(createUnmeteredNetworkRequest()); + requests.add(createOemPaidNetworkRequest()); + break; + case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY: + requests.add(createOemPaidNetworkRequest()); + break; + case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY: + requests.add(createOemPrivateNetworkRequest()); + break; + default: + // This should never happen. + throw new IllegalArgumentException("createNriFromOemNetworkPreferences()" + + " called with invalid preference of " + preference); + } + + final ArraySet ranges = new ArraySet<Integer>(); + for (final int uid : uids) { + ranges.add(new UidRange(uid, uid)); + } + setNetworkRequestUids(requests, ranges); + return new NetworkRequestInfo(Process.myUid(), requests); + } + + private NetworkRequest createUnmeteredNetworkRequest() { + final NetworkCapabilities netcap = createDefaultPerAppNetCap() + .addCapability(NET_CAPABILITY_NOT_METERED) + .addCapability(NET_CAPABILITY_VALIDATED); + return createNetworkRequest(NetworkRequest.Type.LISTEN, netcap); + } + + private NetworkRequest createOemPaidNetworkRequest() { + // NET_CAPABILITY_OEM_PAID is a restricted capability. + final NetworkCapabilities netcap = createDefaultPerAppNetCap() + .addCapability(NET_CAPABILITY_OEM_PAID) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap); + } + + private NetworkRequest createOemPrivateNetworkRequest() { + // NET_CAPABILITY_OEM_PRIVATE is a restricted capability. + final NetworkCapabilities netcap = createDefaultPerAppNetCap() + .addCapability(NET_CAPABILITY_OEM_PRIVATE) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap); + } + + private NetworkCapabilities createDefaultPerAppNetCap() { + final NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName()); + return netCap; + } + } +} diff --git a/service/src/com/android/server/ConnectivityServiceInitializer.java b/service/src/com/android/server/ConnectivityServiceInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..2465479aadd85d0a47dc542c53ce95808b41baaa --- /dev/null +++ b/service/src/com/android/server/ConnectivityServiceInitializer.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.server; + +import android.content.Context; +import android.util.Log; + +/** + * Connectivity service initializer for core networking. This is called by system server to create + * a new instance of ConnectivityService. + */ +public final class ConnectivityServiceInitializer extends SystemService { + private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName(); + private final ConnectivityService mConnectivity; + + public ConnectivityServiceInitializer(Context context) { + super(context); + // Load JNI libraries used by ConnectivityService and its dependencies + System.loadLibrary("service-connectivity"); + // TODO: Define formal APIs to get the needed services. + mConnectivity = new ConnectivityService(context); + } + + @Override + public void onStart() { + Log.i(TAG, "Registering " + Context.CONNECTIVITY_SERVICE); + publishBinderService(Context.CONNECTIVITY_SERVICE, mConnectivity, + /* allowIsolated= */ false); + } +} diff --git a/service/src/com/android/server/NetIdManager.java b/service/src/com/android/server/NetIdManager.java new file mode 100644 index 0000000000000000000000000000000000000000..61925c80a22bb520df3a6b291ba2fdc1d915deb7 --- /dev/null +++ b/service/src/com/android/server/NetIdManager.java @@ -0,0 +1,89 @@ +/* + * 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.server; + +import android.annotation.NonNull; +import android.net.ConnectivityManager; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +/** + * Class used to reserve and release net IDs. + * + * <p>Instances of this class are thread-safe. + */ +public class NetIdManager { + // Sequence number for Networks; keep in sync with system/netd/NetworkController.cpp + public static final int MIN_NET_ID = 100; // some reserved marks + // Top IDs reserved by IpSecService + public static final int MAX_NET_ID = ConnectivityManager.getIpSecNetIdRange().getLower() - 1; + + @GuardedBy("mNetIdInUse") + private final SparseBooleanArray mNetIdInUse = new SparseBooleanArray(); + + @GuardedBy("mNetIdInUse") + private int mLastNetId = MIN_NET_ID - 1; + + private final int mMaxNetId; + + public NetIdManager() { + this(MAX_NET_ID); + } + + @VisibleForTesting + NetIdManager(int maxNetId) { + mMaxNetId = maxNetId; + } + + /** + * Get the first netId that follows the provided lastId and is available. + */ + private int getNextAvailableNetIdLocked( + int lastId, @NonNull SparseBooleanArray netIdInUse) { + int netId = lastId; + for (int i = MIN_NET_ID; i <= mMaxNetId; i++) { + netId = netId < mMaxNetId ? netId + 1 : MIN_NET_ID; + if (!netIdInUse.get(netId)) { + return netId; + } + } + throw new IllegalStateException("No free netIds"); + } + + /** + * Reserve a new ID for a network. + */ + public int reserveNetId() { + synchronized (mNetIdInUse) { + mLastNetId = getNextAvailableNetIdLocked(mLastNetId, mNetIdInUse); + // Make sure NetID unused. http://b/16815182 + mNetIdInUse.put(mLastNetId, true); + return mLastNetId; + } + } + + /** + * Clear a previously reserved ID for a network. + */ + public void releaseNetId(int id) { + synchronized (mNetIdInUse) { + mNetIdInUse.delete(id); + } + } +} diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java new file mode 100644 index 0000000000000000000000000000000000000000..09873f4db045d71da7102fcf7456c11967b3c4f8 --- /dev/null +++ b/service/src/com/android/server/TestNetworkService.java @@ -0,0 +1,387 @@ +/* + * 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.server; + +import static android.net.TestNetworkManager.TEST_TAP_PREFIX; +import static android.net.TestNetworkManager.TEST_TUN_PREFIX; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.ITestNetworkManager; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.RouteInfo; +import android.net.TestNetworkInterface; +import android.net.TestNetworkSpecifier; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.NetdUtils; +import com.android.net.module.util.NetworkStackConstants; +import com.android.net.module.util.PermissionUtils; + +import java.io.UncheckedIOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +/** @hide */ +class TestNetworkService extends ITestNetworkManager.Stub { + @NonNull private static final String TEST_NETWORK_LOGTAG = "TestNetworkAgent"; + @NonNull private static final String TEST_NETWORK_PROVIDER_NAME = "TestNetworkProvider"; + @NonNull private static final AtomicInteger sTestTunIndex = new AtomicInteger(); + + @NonNull private final Context mContext; + @NonNull private final INetd mNetd; + + @NonNull private final HandlerThread mHandlerThread; + @NonNull private final Handler mHandler; + + @NonNull private final ConnectivityManager mCm; + @NonNull private final NetworkProvider mNetworkProvider; + + // Native method stubs + private static native int jniCreateTunTap(boolean isTun, @NonNull String iface); + + @VisibleForTesting + protected TestNetworkService(@NonNull Context context) { + mHandlerThread = new HandlerThread("TestNetworkServiceThread"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + mContext = Objects.requireNonNull(context, "missing Context"); + mNetd = Objects.requireNonNull( + INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)), + "could not get netd instance"); + mCm = mContext.getSystemService(ConnectivityManager.class); + mNetworkProvider = new NetworkProvider(mContext, mHandler.getLooper(), + TEST_NETWORK_PROVIDER_NAME); + final long token = Binder.clearCallingIdentity(); + try { + mCm.registerNetworkProvider(mNetworkProvider); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Create a TUN or TAP interface with the given interface name and link addresses + * + * <p>This method will return the FileDescriptor to the interface. Close it to tear down the + * interface. + */ + private TestNetworkInterface createInterface(boolean isTun, LinkAddress[] linkAddrs) { + enforceTestNetworkPermissions(mContext); + + Objects.requireNonNull(linkAddrs, "missing linkAddrs"); + + String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX; + String iface = ifacePrefix + sTestTunIndex.getAndIncrement(); + final long token = Binder.clearCallingIdentity(); + try { + ParcelFileDescriptor tunIntf = + ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface)); + for (LinkAddress addr : linkAddrs) { + mNetd.interfaceAddAddress( + iface, + addr.getAddress().getHostAddress(), + addr.getPrefixLength()); + } + + return new TestNetworkInterface(tunIntf, iface); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Create a TUN interface with the given interface name and link addresses + * + * <p>This method will return the FileDescriptor to the TUN interface. Close it to tear down the + * TUN interface. + */ + @Override + public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) { + return createInterface(true, linkAddrs); + } + + /** + * Create a TAP interface with the given interface name + * + * <p>This method will return the FileDescriptor to the TAP interface. Close it to tear down the + * TAP interface. + */ + @Override + public TestNetworkInterface createTapInterface() { + return createInterface(false, new LinkAddress[0]); + } + + // Tracker for TestNetworkAgents + @GuardedBy("mTestNetworkTracker") + @NonNull + private final SparseArray<TestNetworkAgent> mTestNetworkTracker = new SparseArray<>(); + + public class TestNetworkAgent extends NetworkAgent implements IBinder.DeathRecipient { + private static final int NETWORK_SCORE = 1; // Use a low, non-zero score. + + private final int mUid; + + @GuardedBy("mBinderLock") + @NonNull + private IBinder mBinder; + + @NonNull private final Object mBinderLock = new Object(); + + private TestNetworkAgent( + @NonNull Context context, + @NonNull Looper looper, + @NonNull NetworkCapabilities nc, + @NonNull LinkProperties lp, + @NonNull NetworkAgentConfig config, + int uid, + @NonNull IBinder binder, + @NonNull NetworkProvider np) + throws RemoteException { + super(context, looper, TEST_NETWORK_LOGTAG, nc, lp, NETWORK_SCORE, config, np); + mUid = uid; + synchronized (mBinderLock) { + mBinder = binder; // Binder null-checks in create() + + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + throw e; // Abort, signal failure up the stack. + } + } + } + + /** + * If the Binder object dies, this function is called to free the resources of this + * TestNetworkAgent + */ + @Override + public void binderDied() { + teardown(); + } + + @Override + protected void unwanted() { + teardown(); + } + + private void teardown() { + unregister(); + + // Synchronize on mBinderLock to ensure that unlinkToDeath is never called more than + // once (otherwise it could throw an exception) + synchronized (mBinderLock) { + // If mBinder is null, this Test Network has already been cleaned up. + if (mBinder == null) return; + mBinder.unlinkToDeath(this, 0); + mBinder = null; + } + + // Has to be in TestNetworkAgent to ensure all teardown codepaths properly clean up + // resources, even for binder death or unwanted calls. + synchronized (mTestNetworkTracker) { + mTestNetworkTracker.remove(getNetwork().getNetId()); + } + } + } + + private TestNetworkAgent registerTestNetworkAgent( + @NonNull Looper looper, + @NonNull Context context, + @NonNull String iface, + @Nullable LinkProperties lp, + boolean isMetered, + int callingUid, + @NonNull int[] administratorUids, + @NonNull IBinder binder) + throws RemoteException, SocketException { + Objects.requireNonNull(looper, "missing Looper"); + Objects.requireNonNull(context, "missing Context"); + // iface and binder validity checked by caller + + // Build narrow set of NetworkCapabilities, useful only for testing + NetworkCapabilities nc = new NetworkCapabilities(); + nc.clearAll(); // Remove default capabilities. + nc.addTransportType(NetworkCapabilities.TRANSPORT_TEST); + nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + nc.setNetworkSpecifier(new TestNetworkSpecifier(iface)); + nc.setAdministratorUids(administratorUids); + if (!isMetered) { + nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + } + + // Build LinkProperties + if (lp == null) { + lp = new LinkProperties(); + } else { + lp = new LinkProperties(lp); + // Use LinkAddress(es) from the interface itself to minimize how much the caller + // is trusted. + lp.setLinkAddresses(new ArrayList<>()); + } + lp.setInterfaceName(iface); + + // Find the currently assigned addresses, and add them to LinkProperties + boolean allowIPv4 = false, allowIPv6 = false; + NetworkInterface netIntf = NetworkInterface.getByName(iface); + Objects.requireNonNull(netIntf, "No such network interface found: " + netIntf); + + for (InterfaceAddress intfAddr : netIntf.getInterfaceAddresses()) { + lp.addLinkAddress( + new LinkAddress(intfAddr.getAddress(), intfAddr.getNetworkPrefixLength())); + + if (intfAddr.getAddress() instanceof Inet6Address) { + allowIPv6 |= !intfAddr.getAddress().isLinkLocalAddress(); + } else if (intfAddr.getAddress() instanceof Inet4Address) { + allowIPv4 = true; + } + } + + // Add global routes (but as non-default, non-internet providing network) + if (allowIPv4) { + lp.addRoute(new RouteInfo(new IpPrefix( + NetworkStackConstants.IPV4_ADDR_ANY, 0), null, iface)); + } + if (allowIPv6) { + lp.addRoute(new RouteInfo(new IpPrefix( + NetworkStackConstants.IPV6_ADDR_ANY, 0), null, iface)); + } + + final TestNetworkAgent agent = new TestNetworkAgent(context, looper, nc, lp, + new NetworkAgentConfig.Builder().build(), callingUid, binder, + mNetworkProvider); + agent.register(); + agent.markConnected(); + return agent; + } + + /** + * Sets up a Network with extremely limited privileges, guarded by the MANAGE_TEST_NETWORKS + * permission. + * + * <p>This method provides a Network that is useful only for testing. + */ + @Override + public void setupTestNetwork( + @NonNull String iface, + @Nullable LinkProperties lp, + boolean isMetered, + @NonNull int[] administratorUids, + @NonNull IBinder binder) { + enforceTestNetworkPermissions(mContext); + + Objects.requireNonNull(iface, "missing Iface"); + Objects.requireNonNull(binder, "missing IBinder"); + + if (!(iface.startsWith(INetd.IPSEC_INTERFACE_PREFIX) + || iface.startsWith(TEST_TUN_PREFIX))) { + throw new IllegalArgumentException( + "Cannot create network for non ipsec, non-testtun interface"); + } + + try { + final long token = Binder.clearCallingIdentity(); + try { + PermissionUtils.enforceNetworkStackPermission(mContext); + NetdUtils.setInterfaceUp(mNetd, iface); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Synchronize all accesses to mTestNetworkTracker to prevent the case where: + // 1. TestNetworkAgent successfully binds to death of binder + // 2. Before it is added to the mTestNetworkTracker, binder dies, binderDied() is called + // (on a different thread) + // 3. This thread is pre-empted, put() is called after remove() + synchronized (mTestNetworkTracker) { + TestNetworkAgent agent = + registerTestNetworkAgent( + mHandler.getLooper(), + mContext, + iface, + lp, + isMetered, + Binder.getCallingUid(), + administratorUids, + binder); + + mTestNetworkTracker.put(agent.getNetwork().getNetId(), agent); + } + } catch (SocketException e) { + throw new UncheckedIOException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** Teardown a test network */ + @Override + public void teardownTestNetwork(int netId) { + enforceTestNetworkPermissions(mContext); + + final TestNetworkAgent agent; + synchronized (mTestNetworkTracker) { + agent = mTestNetworkTracker.get(netId); + } + + if (agent == null) { + return; // Already torn down + } else if (agent.mUid != Binder.getCallingUid()) { + throw new SecurityException("Attempted to modify other user's test networks"); + } + + // Safe to be called multiple times. + agent.teardown(); + } + + private static final String PERMISSION_NAME = + android.Manifest.permission.MANAGE_TEST_NETWORKS; + + public static void enforceTestNetworkPermissions(@NonNull Context context) { + context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService"); + } +} diff --git a/service/src/com/android/server/connectivity/AutodestructReference.java b/service/src/com/android/server/connectivity/AutodestructReference.java new file mode 100644 index 0000000000000000000000000000000000000000..009a43e5828502a1712d49f2d3b54cba0adba3c2 --- /dev/null +++ b/service/src/com/android/server/connectivity/AutodestructReference.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.server.connectivity; + +import android.annotation.NonNull; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * A ref that autodestructs at the first usage of it. + * @param <T> The type of the held object + * @hide + */ +public class AutodestructReference<T> { + private final AtomicReference<T> mHeld; + public AutodestructReference(@NonNull T obj) { + if (null == obj) throw new NullPointerException("Autodestruct reference to null"); + mHeld = new AtomicReference<>(obj); + } + + /** Get the ref and destruct it. NPE if already destructed. */ + @NonNull + public T getAndDestroy() { + final T obj = mHeld.getAndSet(null); + if (null == obj) throw new NullPointerException("Already autodestructed"); + return obj; + } +} diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java new file mode 100644 index 0000000000000000000000000000000000000000..05b12bad5589156500d3ce2cac462a451cfc67c8 --- /dev/null +++ b/service/src/com/android/server/connectivity/DnsManager.java @@ -0,0 +1,494 @@ +/* + * 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.server.connectivity; + +import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MAX_SAMPLES; +import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MIN_SAMPLES; +import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS; +import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS; + +import android.annotation.NonNull; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.ConnectivitySettingsManager; +import android.net.IDnsResolver; +import android.net.InetAddresses; +import android.net.LinkProperties; +import android.net.Network; +import android.net.ResolverOptionsParcel; +import android.net.ResolverParamsParcel; +import android.net.Uri; +import android.net.shared.PrivateDnsConfig; +import android.os.Binder; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Encapsulate the management of DNS settings for networks. + * + * This class it NOT designed for concurrent access. Furthermore, all non-static + * methods MUST be called from ConnectivityService's thread. However, an exceptional + * case is getPrivateDnsConfig(Network) which is exclusively for + * ConnectivityService#dumpNetworkDiagnostics() on a random binder thread. + * + * [ Private DNS ] + * The code handling Private DNS is spread across several components, but this + * seems like the least bad place to collect all the observations. + * + * Private DNS handling and updating occurs in response to several different + * events. Each is described here with its corresponding intended handling. + * + * [A] Event: A new network comes up. + * Mechanics: + * [1] ConnectivityService gets notifications from NetworkAgents. + * [2] in updateNetworkInfo(), the first time the NetworkAgent goes into + * into CONNECTED state, the Private DNS configuration is retrieved, + * programmed, and strict mode hostname resolution (if applicable) is + * enqueued in NetworkAgent's NetworkMonitor, via a call to + * handlePerNetworkPrivateDnsConfig(). + * [3] Re-resolution of strict mode hostnames that fail to return any + * IP addresses happens inside NetworkMonitor; it sends itself a + * delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff + * schedule. + * [4] Successfully resolved hostnames are sent to ConnectivityService + * inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved + * IP addresses are programmed into netd via: + * + * updatePrivateDns() -> updateDnses() + * + * both of which make calls into DnsManager. + * [5] Upon a successful hostname resolution NetworkMonitor initiates a + * validation attempt in the form of a lookup for a one-time hostname + * that uses Private DNS. + * + * [B] Event: Private DNS settings are changed. + * Mechanics: + * [1] ConnectivityService gets notifications from its SettingsObserver. + * [2] handlePrivateDnsSettingsChanged() is called, which calls + * handlePerNetworkPrivateDnsConfig() and the process proceeds + * as if from A.3 above. + * + * [C] Event: An application calls ConnectivityManager#reportBadNetwork(). + * Mechanics: + * [1] NetworkMonitor is notified and initiates a reevaluation, which + * always bypasses Private DNS. + * [2] Once completed, NetworkMonitor checks if strict mode is in operation + * and if so enqueues another evaluation of Private DNS, as if from + * step A.5 above. + * + * @hide + */ +public class DnsManager { + private static final String TAG = DnsManager.class.getSimpleName(); + private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig(); + + /* Defaults for resolver parameters. */ + private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800; + private static final int DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25; + private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8; + private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64; + + /** + * Get PrivateDnsConfig. + */ + public static PrivateDnsConfig getPrivateDnsConfig(Context context) { + final int mode = ConnectivitySettingsManager.getPrivateDnsMode(context); + + final boolean useTls = mode != PRIVATE_DNS_MODE_OFF; + + if (PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mode) { + final String specifier = getStringSetting(context.getContentResolver(), + PRIVATE_DNS_SPECIFIER); + return new PrivateDnsConfig(specifier, null); + } + + return new PrivateDnsConfig(useTls); + } + + public static Uri[] getPrivateDnsSettingsUris() { + return new Uri[]{ + Settings.Global.getUriFor(PRIVATE_DNS_DEFAULT_MODE), + Settings.Global.getUriFor(PRIVATE_DNS_MODE), + Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER), + }; + } + + public static class PrivateDnsValidationUpdate { + public final int netId; + public final InetAddress ipAddress; + public final String hostname; + // Refer to IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_*. + public final int validationResult; + + public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress, + String hostname, int validationResult) { + this.netId = netId; + this.ipAddress = ipAddress; + this.hostname = hostname; + this.validationResult = validationResult; + } + } + + private static class PrivateDnsValidationStatuses { + enum ValidationStatus { + IN_PROGRESS, + FAILED, + SUCCEEDED + } + + // Validation statuses of <hostname, ipAddress> pairs for a single netId + // Caution : not thread-safe. As mentioned in the top file comment, all + // methods of this class must only be called on ConnectivityService's thread. + private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap; + + private PrivateDnsValidationStatuses() { + mValidationMap = new HashMap<>(); + } + + private boolean hasValidatedServer() { + for (ValidationStatus status : mValidationMap.values()) { + if (status == ValidationStatus.SUCCEEDED) { + return true; + } + } + return false; + } + + private void updateTrackedDnses(String[] ipAddresses, String hostname) { + Set<Pair<String, InetAddress>> latestDnses = new HashSet<>(); + for (String ipAddress : ipAddresses) { + try { + latestDnses.add(new Pair(hostname, + InetAddresses.parseNumericAddress(ipAddress))); + } catch (IllegalArgumentException e) {} + } + // Remove <hostname, ipAddress> pairs that should not be tracked. + for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it = + mValidationMap.entrySet().iterator(); it.hasNext(); ) { + Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next(); + if (!latestDnses.contains(entry.getKey())) { + it.remove(); + } + } + // Add new <hostname, ipAddress> pairs that should be tracked. + for (Pair<String, InetAddress> p : latestDnses) { + if (!mValidationMap.containsKey(p)) { + mValidationMap.put(p, ValidationStatus.IN_PROGRESS); + } + } + } + + private void updateStatus(PrivateDnsValidationUpdate update) { + Pair<String, InetAddress> p = new Pair(update.hostname, + update.ipAddress); + if (!mValidationMap.containsKey(p)) { + return; + } + if (update.validationResult == VALIDATION_RESULT_SUCCESS) { + mValidationMap.put(p, ValidationStatus.SUCCEEDED); + } else if (update.validationResult == VALIDATION_RESULT_FAILURE) { + mValidationMap.put(p, ValidationStatus.FAILED); + } else { + Log.e(TAG, "Unknown private dns validation operation=" + + update.validationResult); + } + } + + private LinkProperties fillInValidatedPrivateDns(LinkProperties lp) { + lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST); + mValidationMap.forEach((key, value) -> { + if (value == ValidationStatus.SUCCEEDED) { + lp.addValidatedPrivateDnsServer(key.second); + } + }); + return lp; + } + } + + private final Context mContext; + private final ContentResolver mContentResolver; + private final IDnsResolver mDnsResolver; + private final ConcurrentHashMap<Integer, PrivateDnsConfig> mPrivateDnsMap; + // TODO: Replace the Map with SparseArrays. + private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap; + private final Map<Integer, LinkProperties> mLinkPropertiesMap; + private final Map<Integer, int[]> mTransportsMap; + + private int mSampleValidity; + private int mSuccessThreshold; + private int mMinSamples; + private int mMaxSamples; + + public DnsManager(Context ctx, IDnsResolver dnsResolver) { + mContext = ctx; + mContentResolver = mContext.getContentResolver(); + mDnsResolver = dnsResolver; + mPrivateDnsMap = new ConcurrentHashMap<>(); + mPrivateDnsValidationMap = new HashMap<>(); + mLinkPropertiesMap = new HashMap<>(); + mTransportsMap = new HashMap<>(); + + // TODO: Create and register ContentObservers to track every setting + // used herein, posting messages to respond to changes. + } + + public PrivateDnsConfig getPrivateDnsConfig() { + return getPrivateDnsConfig(mContext); + } + + public void removeNetwork(Network network) { + mPrivateDnsMap.remove(network.getNetId()); + mPrivateDnsValidationMap.remove(network.getNetId()); + mTransportsMap.remove(network.getNetId()); + mLinkPropertiesMap.remove(network.getNetId()); + } + + // This is exclusively called by ConnectivityService#dumpNetworkDiagnostics() which + // is not on the ConnectivityService handler thread. + public PrivateDnsConfig getPrivateDnsConfig(@NonNull Network network) { + return mPrivateDnsMap.getOrDefault(network.getNetId(), PRIVATE_DNS_OFF); + } + + public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) { + Log.w(TAG, "updatePrivateDns(" + network + ", " + cfg + ")"); + return (cfg != null) + ? mPrivateDnsMap.put(network.getNetId(), cfg) + : mPrivateDnsMap.remove(network.getNetId()); + } + + public void updatePrivateDnsStatus(int netId, LinkProperties lp) { + // Use the PrivateDnsConfig data pushed to this class instance + // from ConnectivityService. + final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId, + PRIVATE_DNS_OFF); + + final boolean useTls = privateDnsCfg.useTls; + final PrivateDnsValidationStatuses statuses = + useTls ? mPrivateDnsValidationMap.get(netId) : null; + final boolean validated = (null != statuses) && statuses.hasValidatedServer(); + final boolean strictMode = privateDnsCfg.inStrictMode(); + final String tlsHostname = strictMode ? privateDnsCfg.hostname : null; + final boolean usingPrivateDns = strictMode || validated; + + lp.setUsePrivateDns(usingPrivateDns); + lp.setPrivateDnsServerName(tlsHostname); + if (usingPrivateDns && null != statuses) { + statuses.fillInValidatedPrivateDns(lp); + } else { + lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST); + } + } + + public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) { + final PrivateDnsValidationStatuses statuses = mPrivateDnsValidationMap.get(update.netId); + if (statuses == null) return; + statuses.updateStatus(update); + } + + /** + * When creating a new network or transport types are changed in a specific network, + * transport types are always saved to a hashMap before update dns config. + * When destroying network, the specific network will be removed from the hashMap. + * The hashMap is always accessed on the same thread. + */ + public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) { + mTransportsMap.put(netId, transportTypes); + sendDnsConfigurationForNetwork(netId); + } + + /** + * When {@link LinkProperties} are changed in a specific network, they are + * always saved to a hashMap before update dns config. + * When destroying network, the specific network will be removed from the hashMap. + * The hashMap is always accessed on the same thread. + */ + public void noteDnsServersForNetwork(int netId, @NonNull LinkProperties lp) { + mLinkPropertiesMap.put(netId, lp); + sendDnsConfigurationForNetwork(netId); + } + + /** + * Send dns configuration parameters to resolver for a given network. + */ + public void sendDnsConfigurationForNetwork(int netId) { + final LinkProperties lp = mLinkPropertiesMap.get(netId); + final int[] transportTypes = mTransportsMap.get(netId); + if (lp == null || transportTypes == null) return; + updateParametersSettings(); + final ResolverParamsParcel paramsParcel = new ResolverParamsParcel(); + + // We only use the PrivateDnsConfig data pushed to this class instance + // from ConnectivityService because it works in coordination with + // NetworkMonitor to decide which networks need validation and runs the + // blocking calls to resolve Private DNS strict mode hostnames. + // + // At this time we do not attempt to enable Private DNS on non-Internet + // networks like IMS. + final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId, + PRIVATE_DNS_OFF); + final boolean useTls = privateDnsCfg.useTls; + final boolean strictMode = privateDnsCfg.inStrictMode(); + + paramsParcel.netId = netId; + paramsParcel.sampleValiditySeconds = mSampleValidity; + paramsParcel.successThreshold = mSuccessThreshold; + paramsParcel.minSamples = mMinSamples; + paramsParcel.maxSamples = mMaxSamples; + paramsParcel.servers = makeStrings(lp.getDnsServers()); + paramsParcel.domains = getDomainStrings(lp.getDomains()); + paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : ""; + paramsParcel.tlsServers = + strictMode ? makeStrings( + Arrays.stream(privateDnsCfg.ips) + .filter((ip) -> lp.isReachable(ip)) + .collect(Collectors.toList())) + : useTls ? paramsParcel.servers // Opportunistic + : new String[0]; // Off + paramsParcel.resolverOptions = new ResolverOptionsParcel(); + paramsParcel.transportTypes = transportTypes; + // Prepare to track the validation status of the DNS servers in the + // resolver config when private DNS is in opportunistic or strict mode. + if (useTls) { + if (!mPrivateDnsValidationMap.containsKey(netId)) { + mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses()); + } + mPrivateDnsValidationMap.get(netId).updateTrackedDnses(paramsParcel.tlsServers, + paramsParcel.tlsName); + } else { + mPrivateDnsValidationMap.remove(netId); + } + + Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, " + + "%d, %d, %s, %s)", paramsParcel.netId, Arrays.toString(paramsParcel.servers), + Arrays.toString(paramsParcel.domains), paramsParcel.sampleValiditySeconds, + paramsParcel.successThreshold, paramsParcel.minSamples, + paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec, + paramsParcel.retryCount, paramsParcel.tlsName, + Arrays.toString(paramsParcel.tlsServers))); + + try { + mDnsResolver.setResolverConfiguration(paramsParcel); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error setting DNS configuration: " + e); + return; + } + } + + /** + * Flush DNS caches and events work before boot has completed. + */ + public void flushVmDnsCache() { + /* + * Tell the VMs to toss their DNS caches + */ + final Intent intent = new Intent(ConnectivityManager.ACTION_CLEAR_DNS_CACHE); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + /* + * Connectivity events can happen before boot has completed ... + */ + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendBroadcastAsUser(intent, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private void updateParametersSettings() { + mSampleValidity = getIntSetting( + DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS, + DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS); + if (mSampleValidity < 0 || mSampleValidity > 65535) { + Log.w(TAG, "Invalid sampleValidity=" + mSampleValidity + ", using default=" + + DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS); + mSampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS; + } + + mSuccessThreshold = getIntSetting( + DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT, + DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT); + if (mSuccessThreshold < 0 || mSuccessThreshold > 100) { + Log.w(TAG, "Invalid successThreshold=" + mSuccessThreshold + ", using default=" + + DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT); + mSuccessThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT; + } + + mMinSamples = getIntSetting(DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES); + mMaxSamples = getIntSetting(DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES); + if (mMinSamples < 0 || mMinSamples > mMaxSamples || mMaxSamples > 64) { + Log.w(TAG, "Invalid sample count (min, max)=(" + mMinSamples + ", " + mMaxSamples + + "), using default=(" + DNS_RESOLVER_DEFAULT_MIN_SAMPLES + ", " + + DNS_RESOLVER_DEFAULT_MAX_SAMPLES + ")"); + mMinSamples = DNS_RESOLVER_DEFAULT_MIN_SAMPLES; + mMaxSamples = DNS_RESOLVER_DEFAULT_MAX_SAMPLES; + } + } + + private int getIntSetting(String which, int dflt) { + return Settings.Global.getInt(mContentResolver, which, dflt); + } + + /** + * Create a string array of host addresses from a collection of InetAddresses + * + * @param addrs a Collection of InetAddresses + * @return an array of Strings containing their host addresses + */ + private String[] makeStrings(Collection<InetAddress> addrs) { + String[] result = new String[addrs.size()]; + int i = 0; + for (InetAddress addr : addrs) { + result[i++] = addr.getHostAddress(); + } + return result; + } + + private static String getStringSetting(ContentResolver cr, String which) { + return Settings.Global.getString(cr, which); + } + + private static String[] getDomainStrings(String domains) { + return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" "); + } +} diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java new file mode 100644 index 0000000000000000000000000000000000000000..fbfa7a18c9d85e2a0725022a4c00bbfef0f6da69 --- /dev/null +++ b/service/src/com/android/server/connectivity/FullScore.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkScore.KEEP_CONNECTED_NONE; +import static android.net.NetworkScore.POLICY_EXITING; +import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY; +import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkScore; +import android.net.NetworkScore.KeepConnectedReason; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.StringJoiner; + +/** + * This class represents how desirable a network is. + * + * FullScore is very similar to NetworkScore, but it contains the bits that are managed + * by ConnectivityService. This provides static guarantee that all users must know whether + * they are handling a score that had the CS-managed bits set. + */ +public class FullScore { + // This will be removed soon. Do *NOT* depend on it for any new code that is not part of + // a migration. + private final int mLegacyInt; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"POLICY_"}, value = { + POLICY_IS_VALIDATED, + POLICY_IS_VPN, + POLICY_EVER_USER_SELECTED, + POLICY_ACCEPT_UNVALIDATED, + POLICY_IS_UNMETERED + }) + public @interface Policy { + } + + // Agent-managed policies are in NetworkScore. They start from 1. + // CS-managed policies, counting from 63 downward + // This network is validated. CS-managed because the source of truth is in NetworkCapabilities. + /** @hide */ + public static final int POLICY_IS_VALIDATED = 63; + + // This is a VPN and behaves as one for scoring purposes. + /** @hide */ + public static final int POLICY_IS_VPN = 62; + + // This network has been selected by the user manually from settings or a 3rd party app + // at least once. {@see NetworkAgentConfig#explicitlySelected}. + /** @hide */ + public static final int POLICY_EVER_USER_SELECTED = 61; + + // The user has indicated in UI that this network should be used even if it doesn't + // validate. {@see NetworkAgentConfig#acceptUnvalidated}. + /** @hide */ + public static final int POLICY_ACCEPT_UNVALIDATED = 60; + + // This network is unmetered. {@see NetworkCapabilities.NET_CAPABILITY_NOT_METERED}. + /** @hide */ + public static final int POLICY_IS_UNMETERED = 59; + + // This network is invincible. This is useful for offers until there is an API to listen + // to requests. + /** @hide */ + public static final int POLICY_IS_INVINCIBLE = 58; + + // This network has been validated at least once since it was connected, but not explicitly + // avoided in UI. + // TODO : remove setAvoidUnvalidated and instead disconnect the network when the user + // chooses to move away from this network, and remove this flag. + /** @hide */ + public static final int POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD = 57; + + // To help iterate when printing + @VisibleForTesting + static final int MIN_CS_MANAGED_POLICY = POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD; + @VisibleForTesting + static final int MAX_CS_MANAGED_POLICY = POLICY_IS_VALIDATED; + + // Mask for policies in NetworkScore. This should have all bits managed by NetworkScore set + // and all bits managed by FullScore unset. As bits are handled from 0 up in NetworkScore and + // from 63 down in FullScore, cut at the 32nd bit for simplicity, but change this if some day + // there are more than 32 bits handled on either side. + // YIELD_TO_BAD_WIFI is temporarily handled by ConnectivityService, but the factory is still + // allowed to set it, so that it's possible to transition from handling it in CS to handling + // it in the factory. + private static final long EXTERNAL_POLICIES_MASK = 0x00000000FFFFFFFFL; + + @VisibleForTesting + static @NonNull String policyNameOf(final int policy) { + switch (policy) { + case POLICY_IS_VALIDATED: return "IS_VALIDATED"; + case POLICY_IS_VPN: return "IS_VPN"; + case POLICY_EVER_USER_SELECTED: return "EVER_USER_SELECTED"; + case POLICY_ACCEPT_UNVALIDATED: return "ACCEPT_UNVALIDATED"; + case POLICY_IS_UNMETERED: return "IS_UNMETERED"; + case POLICY_YIELD_TO_BAD_WIFI: return "YIELD_TO_BAD_WIFI"; + case POLICY_TRANSPORT_PRIMARY: return "TRANSPORT_PRIMARY"; + case POLICY_EXITING: return "EXITING"; + case POLICY_IS_INVINCIBLE: return "INVINCIBLE"; + case POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD: return "EVER_VALIDATED"; + } + throw new IllegalArgumentException("Unknown policy : " + policy); + } + + // Bitmask of all the policies applied to this score. + private final long mPolicies; + + private final int mKeepConnectedReason; + + FullScore(final int legacyInt, final long policies, + @KeepConnectedReason final int keepConnectedReason) { + mLegacyInt = legacyInt; + mPolicies = policies; + mKeepConnectedReason = keepConnectedReason; + } + + /** + * Given a score supplied by the NetworkAgent and CS-managed objects, produce a full score. + * + * @param score the score supplied by the agent + * @param caps the NetworkCapabilities of the network + * @param config the NetworkAgentConfig of the network + * @param everValidated whether this network has ever validated + * @param yieldToBadWiFi whether this network yields to a previously validated wifi gone bad + * @return a FullScore that is appropriate to use for ranking. + */ + // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the + // telephony factory, so that it depends on the carrier. For now this is handled by + // connectivity for backward compatibility. + public static FullScore fromNetworkScore(@NonNull final NetworkScore score, + @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config, + final boolean everValidated, final boolean yieldToBadWiFi) { + return withPolicies(score.getLegacyInt(), score.getPolicies(), + score.getKeepConnectedReason(), + caps.hasCapability(NET_CAPABILITY_VALIDATED), + caps.hasTransport(TRANSPORT_VPN), + caps.hasCapability(NET_CAPABILITY_NOT_METERED), + everValidated, + config.explicitlySelected, + config.acceptUnvalidated, + yieldToBadWiFi, + false /* invincible */); // only prospective scores can be invincible + } + + /** + * Given a score supplied by a NetworkProvider, produce a prospective score for an offer. + * + * NetworkOffers have score filters that are compared to the scores of actual networks + * to see if they could possibly beat the current satisfier. Some things the agent can't + * know in advance ; a good example is the validation bit – some networks will validate, + * others won't. For comparison purposes, assume the best, so all possibly beneficial + * networks will be brought up. + * + * @param score the score supplied by the agent for this offer + * @param caps the capabilities supplied by the agent for this offer + * @return a FullScore appropriate for comparing to actual network's scores. + */ + public static FullScore makeProspectiveScore(@NonNull final NetworkScore score, + @NonNull final NetworkCapabilities caps) { + // If the network offers Internet access, it may validate. + final boolean mayValidate = caps.hasCapability(NET_CAPABILITY_INTERNET); + // VPN transports are known in advance. + final boolean vpn = caps.hasTransport(TRANSPORT_VPN); + // Prospective scores are always unmetered, because unmetered networks are stronger + // than metered networks, and it's not known in advance whether the network is metered. + final boolean unmetered = true; + // If the offer may validate, then it should be considered to have validated at some point + final boolean everValidated = mayValidate; + // The network hasn't been chosen by the user (yet, at least). + final boolean everUserSelected = false; + // Don't assume the user will accept unvalidated connectivity. + final boolean acceptUnvalidated = false; + // Don't assume clinging to bad wifi + final boolean yieldToBadWiFi = false; + // A prospective score is invincible if the legacy int in the filter is over the maximum + // score. + final boolean invincible = score.getLegacyInt() > NetworkRanker.LEGACY_INT_MAX; + return withPolicies(score.getLegacyInt(), score.getPolicies(), KEEP_CONNECTED_NONE, + mayValidate, vpn, unmetered, everValidated, everUserSelected, acceptUnvalidated, + yieldToBadWiFi, invincible); + } + + /** + * Return a new score given updated caps and config. + * + * @param caps the NetworkCapabilities of the network + * @param config the NetworkAgentConfig of the network + * @return a score with the policies from the arguments reset + */ + // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the + // telephony factory, so that it depends on the carrier. For now this is handled by + // connectivity for backward compatibility. + public FullScore mixInScore(@NonNull final NetworkCapabilities caps, + @NonNull final NetworkAgentConfig config, + final boolean everValidated, + final boolean yieldToBadWifi) { + return withPolicies(mLegacyInt, mPolicies, mKeepConnectedReason, + caps.hasCapability(NET_CAPABILITY_VALIDATED), + caps.hasTransport(TRANSPORT_VPN), + caps.hasCapability(NET_CAPABILITY_NOT_METERED), + everValidated, + config.explicitlySelected, + config.acceptUnvalidated, + yieldToBadWifi, + false /* invincible */); // only prospective scores can be invincible + } + + // TODO : this shouldn't manage bad wifi avoidance – instead this should be done by the + // telephony factory, so that it depends on the carrier. For now this is handled by + // connectivity for backward compatibility. + private static FullScore withPolicies(@NonNull final int legacyInt, + final long externalPolicies, + @KeepConnectedReason final int keepConnectedReason, + final boolean isValidated, + final boolean isVpn, + final boolean isUnmetered, + final boolean everValidated, + final boolean everUserSelected, + final boolean acceptUnvalidated, + final boolean yieldToBadWiFi, + final boolean invincible) { + return new FullScore(legacyInt, (externalPolicies & EXTERNAL_POLICIES_MASK) + | (isValidated ? 1L << POLICY_IS_VALIDATED : 0) + | (isVpn ? 1L << POLICY_IS_VPN : 0) + | (isUnmetered ? 1L << POLICY_IS_UNMETERED : 0) + | (everValidated ? 1L << POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD : 0) + | (everUserSelected ? 1L << POLICY_EVER_USER_SELECTED : 0) + | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0) + | (yieldToBadWiFi ? 1L << POLICY_YIELD_TO_BAD_WIFI : 0) + | (invincible ? 1L << POLICY_IS_INVINCIBLE : 0), + keepConnectedReason); + } + + /** + * Returns this score but validated. + */ + public FullScore asValidated() { + return new FullScore(mLegacyInt, mPolicies | (1L << POLICY_IS_VALIDATED), + mKeepConnectedReason); + } + + /** + * For backward compatibility, get the legacy int. + * This will be removed before S is published. + */ + public int getLegacyInt() { + return getLegacyInt(false /* pretendValidated */); + } + + public int getLegacyIntAsValidated() { + return getLegacyInt(true /* pretendValidated */); + } + + // TODO : remove these two constants + // Penalty applied to scores of Networks that have not been validated. + private static final int UNVALIDATED_SCORE_PENALTY = 40; + + // Score for a network that can be used unvalidated + private static final int ACCEPT_UNVALIDATED_NETWORK_SCORE = 100; + + private int getLegacyInt(boolean pretendValidated) { + // If the user has chosen this network at least once, give it the maximum score when + // checking to pretend it's validated, or if it doesn't need to validate because the + // user said to use it even if it doesn't validate. + // This ensures that networks that have been selected in UI are not torn down before the + // user gets a chance to prefer it when a higher-scoring network (e.g., Ethernet) is + // available. + if (hasPolicy(POLICY_EVER_USER_SELECTED) + && (hasPolicy(POLICY_ACCEPT_UNVALIDATED) || pretendValidated)) { + return ACCEPT_UNVALIDATED_NETWORK_SCORE; + } + + int score = mLegacyInt; + // Except for VPNs, networks are subject to a penalty for not being validated. + // Apply the penalty unless the network is a VPN, or it's validated or pretending to be. + if (!hasPolicy(POLICY_IS_VALIDATED) && !pretendValidated && !hasPolicy(POLICY_IS_VPN)) { + score -= UNVALIDATED_SCORE_PENALTY; + } + if (score < 0) score = 0; + return score; + } + + /** + * @return whether this score has a particular policy. + */ + @VisibleForTesting + public boolean hasPolicy(final int policy) { + return 0 != (mPolicies & (1L << policy)); + } + + /** + * Returns the keep-connected reason, or KEEP_CONNECTED_NONE. + */ + public int getKeepConnectedReason() { + return mKeepConnectedReason; + } + + // Example output : + // Score(50 ; Policies : EVER_USER_SELECTED&IS_VALIDATED) + @Override + public String toString() { + final StringJoiner sj = new StringJoiner( + "&", // delimiter + "Score(" + mLegacyInt + " ; KeepConnected : " + mKeepConnectedReason + + " ; Policies : ", // prefix + ")"); // suffix + for (int i = NetworkScore.MIN_AGENT_MANAGED_POLICY; + i <= NetworkScore.MAX_AGENT_MANAGED_POLICY; ++i) { + if (hasPolicy(i)) sj.add(policyNameOf(i)); + } + for (int i = MIN_CS_MANAGED_POLICY; i <= MAX_CS_MANAGED_POLICY; ++i) { + if (hasPolicy(i)) sj.add(policyNameOf(i)); + } + return sj.toString(); + } +} diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..7d922a4de9a1cc1aa0fe8d16a3b2e64bccdae042 --- /dev/null +++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java @@ -0,0 +1,761 @@ +/* + * 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 com.android.server.connectivity; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.NattSocketKeepalive.NATT_PORT; +import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE; +import static android.net.SocketKeepalive.BINDER_DIED; +import static android.net.SocketKeepalive.DATA_RECEIVED; +import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES; +import static android.net.SocketKeepalive.ERROR_INVALID_INTERVAL; +import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; +import static android.net.SocketKeepalive.ERROR_INVALID_NETWORK; +import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET; +import static android.net.SocketKeepalive.ERROR_NO_SUCH_SLOT; +import static android.net.SocketKeepalive.ERROR_STOP_REASON_UNINITIALIZED; +import static android.net.SocketKeepalive.ERROR_UNSUPPORTED; +import static android.net.SocketKeepalive.MAX_INTERVAL_SEC; +import static android.net.SocketKeepalive.MIN_INTERVAL_SEC; +import static android.net.SocketKeepalive.NO_KEEPALIVE; +import static android.net.SocketKeepalive.SUCCESS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.ConnectivityResources; +import android.net.ISocketKeepaliveCallback; +import android.net.InetAddresses; +import android.net.InvalidPacketException; +import android.net.KeepalivePacketData; +import android.net.NattKeepalivePacketData; +import android.net.NetworkAgent; +import android.net.SocketKeepalive.InvalidSocketException; +import android.net.TcpKeepalivePacketData; +import android.net.util.KeepaliveUtils; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; +import android.util.Pair; + +import com.android.connectivity.resources.R; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.HexDump; +import com.android.net.module.util.IpUtils; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +/** + * Manages socket keepalive requests. + * + * Provides methods to stop and start keepalive requests, and keeps track of keepalives across all + * networks. This class is tightly coupled to ConnectivityService. It is not thread-safe and its + * handle* methods must be called only from the ConnectivityService handler thread. + */ +public class KeepaliveTracker { + + private static final String TAG = "KeepaliveTracker"; + private static final boolean DBG = false; + + public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD; + + /** Keeps track of keepalive requests. */ + private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives = + new HashMap<> (); + private final Handler mConnectivityServiceHandler; + @NonNull + private final TcpKeepaliveController mTcpController; + @NonNull + private final Context mContext; + + // Supported keepalive count for each transport type, can be configured through + // config_networkSupportedKeepaliveCount. For better error handling, use + // {@link getSupportedKeepalivesForNetworkCapabilities} instead of direct access. + @NonNull + private final int[] mSupportedKeepalives; + + // Reserved privileged keepalive slots per transport. Caller's permission will be enforced if + // the number of remaining keepalive slots is less than or equal to the threshold. + private final int mReservedPrivilegedSlots; + + // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if + // the number of remaining keepalive slots is less than or equal to the threshold. + private final int mAllowedUnprivilegedSlotsForUid; + + public KeepaliveTracker(Context context, Handler handler) { + mConnectivityServiceHandler = handler; + mTcpController = new TcpKeepaliveController(handler); + mContext = context; + mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext); + + final ConnectivityResources res = new ConnectivityResources(mContext); + mReservedPrivilegedSlots = res.get().getInteger( + R.integer.config_reservedPrivilegedKeepaliveSlots); + mAllowedUnprivilegedSlotsForUid = res.get().getInteger( + R.integer.config_allowedUnprivilegedKeepalivePerUid); + } + + /** + * Tracks information about a socket keepalive. + * + * All information about this keepalive is known at construction time except the slot number, + * which is only returned when the hardware has successfully started the keepalive. + */ + class KeepaliveInfo implements IBinder.DeathRecipient { + // Bookkeeping data. + private final ISocketKeepaliveCallback mCallback; + private final int mUid; + private final int mPid; + private final boolean mPrivileged; + private final NetworkAgentInfo mNai; + private final int mType; + private final FileDescriptor mFd; + + public static final int TYPE_NATT = 1; + public static final int TYPE_TCP = 2; + + // Keepalive slot. A small integer that identifies this keepalive among the ones handled + // by this network. + private int mSlot = NO_KEEPALIVE; + + // Packet data. + private final KeepalivePacketData mPacket; + private final int mInterval; + + // Whether the keepalive is started or not. The initial state is NOT_STARTED. + private static final int NOT_STARTED = 1; + private static final int STARTING = 2; + private static final int STARTED = 3; + private static final int STOPPING = 4; + private int mStartedState = NOT_STARTED; + private int mStopReason = ERROR_STOP_REASON_UNINITIALIZED; + + KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback, + @NonNull NetworkAgentInfo nai, + @NonNull KeepalivePacketData packet, + int interval, + int type, + @Nullable FileDescriptor fd) throws InvalidSocketException { + mCallback = callback; + mPid = Binder.getCallingPid(); + mUid = Binder.getCallingUid(); + mPrivileged = (PERMISSION_GRANTED == mContext.checkPermission(PERMISSION, mPid, mUid)); + + mNai = nai; + mPacket = packet; + mInterval = interval; + mType = type; + + // For SocketKeepalive, a dup of fd is kept in mFd so the source port from which the + // keepalives are sent cannot be reused by another app even if the fd gets closed by + // the user. A null is acceptable here for backward compatibility of PacketKeepalive + // API. + try { + if (fd != null) { + mFd = Os.dup(fd); + } else { + Log.d(TAG, toString() + " calls with null fd"); + if (!mPrivileged) { + throw new SecurityException( + "null fd is not allowed for unprivileged access."); + } + if (mType == TYPE_TCP) { + throw new IllegalArgumentException( + "null fd is not allowed for tcp socket keepalives."); + } + mFd = null; + } + } catch (ErrnoException e) { + Log.e(TAG, "Cannot dup fd: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + } + } + + public NetworkAgentInfo getNai() { + return mNai; + } + + private String startedStateString(final int state) { + switch (state) { + case NOT_STARTED : return "NOT_STARTED"; + case STARTING : return "STARTING"; + case STARTED : return "STARTED"; + case STOPPING : return "STOPPING"; + } + throw new IllegalArgumentException("Unknown state"); + } + + public String toString() { + return "KeepaliveInfo [" + + " type=" + mType + + " network=" + mNai.network + + " startedState=" + startedStateString(mStartedState) + + " " + + IpUtils.addressAndPortToString(mPacket.getSrcAddress(), mPacket.getSrcPort()) + + "->" + + IpUtils.addressAndPortToString(mPacket.getDstAddress(), mPacket.getDstPort()) + + " interval=" + mInterval + + " uid=" + mUid + " pid=" + mPid + " privileged=" + mPrivileged + + " packetData=" + HexDump.toHexString(mPacket.getPacket()) + + " ]"; + } + + /** Called when the application process is killed. */ + public void binderDied() { + stop(BINDER_DIED); + } + + void unlinkDeathRecipient() { + if (mCallback != null) { + mCallback.asBinder().unlinkToDeath(this, 0); + } + } + + private int checkNetworkConnected() { + if (!mNai.networkInfo.isConnectedOrConnecting()) { + return ERROR_INVALID_NETWORK; + } + return SUCCESS; + } + + private int checkSourceAddress() { + // Check that we have the source address. + for (InetAddress address : mNai.linkProperties.getAddresses()) { + if (address.equals(mPacket.getSrcAddress())) { + return SUCCESS; + } + } + return ERROR_INVALID_IP_ADDRESS; + } + + private int checkInterval() { + if (mInterval < MIN_INTERVAL_SEC || mInterval > MAX_INTERVAL_SEC) { + return ERROR_INVALID_INTERVAL; + } + return SUCCESS; + } + + private int checkPermission() { + final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai); + if (networkKeepalives == null) { + return ERROR_INVALID_NETWORK; + } + + if (mPrivileged) return SUCCESS; + + final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities( + mSupportedKeepalives, mNai.networkCapabilities); + + int takenUnprivilegedSlots = 0; + for (final KeepaliveInfo ki : networkKeepalives.values()) { + if (!ki.mPrivileged) ++takenUnprivilegedSlots; + } + if (takenUnprivilegedSlots > supported - mReservedPrivilegedSlots) { + return ERROR_INSUFFICIENT_RESOURCES; + } + + // Count unprivileged keepalives for the same uid across networks. + int unprivilegedCountSameUid = 0; + for (final HashMap<Integer, KeepaliveInfo> kaForNetwork : mKeepalives.values()) { + for (final KeepaliveInfo ki : kaForNetwork.values()) { + if (ki.mUid == mUid) { + unprivilegedCountSameUid++; + } + } + } + if (unprivilegedCountSameUid > mAllowedUnprivilegedSlotsForUid) { + return ERROR_INSUFFICIENT_RESOURCES; + } + return SUCCESS; + } + + private int checkLimit() { + final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai); + if (networkKeepalives == null) { + return ERROR_INVALID_NETWORK; + } + final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities( + mSupportedKeepalives, mNai.networkCapabilities); + if (supported == 0) return ERROR_UNSUPPORTED; + if (networkKeepalives.size() > supported) return ERROR_INSUFFICIENT_RESOURCES; + return SUCCESS; + } + + private int isValid() { + synchronized (mNai) { + int error = checkInterval(); + if (error == SUCCESS) error = checkLimit(); + if (error == SUCCESS) error = checkPermission(); + if (error == SUCCESS) error = checkNetworkConnected(); + if (error == SUCCESS) error = checkSourceAddress(); + return error; + } + } + + void start(int slot) { + mSlot = slot; + int error = isValid(); + if (error == SUCCESS) { + Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.toShortString()); + switch (mType) { + case TYPE_NATT: + final NattKeepalivePacketData nattData = (NattKeepalivePacketData) mPacket; + mNai.onAddNattKeepalivePacketFilter(slot, nattData); + mNai.onStartNattSocketKeepalive(slot, mInterval, nattData); + break; + case TYPE_TCP: + try { + mTcpController.startSocketMonitor(mFd, this, mSlot); + } catch (InvalidSocketException e) { + handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET); + return; + } + final TcpKeepalivePacketData tcpData = (TcpKeepalivePacketData) mPacket; + mNai.onAddTcpKeepalivePacketFilter(slot, tcpData); + // TODO: check result from apf and notify of failure as needed. + mNai.onStartTcpSocketKeepalive(slot, mInterval, tcpData); + break; + default: + Log.wtf(TAG, "Starting keepalive with unknown type: " + mType); + handleStopKeepalive(mNai, mSlot, error); + return; + } + mStartedState = STARTING; + } else { + handleStopKeepalive(mNai, mSlot, error); + return; + } + } + + void stop(int reason) { + int uid = Binder.getCallingUid(); + if (uid != mUid && uid != Process.SYSTEM_UID) { + if (DBG) { + Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network); + } + } + // Ignore the case when the network disconnects immediately after stop() has been + // called and the keepalive code is waiting for the response from the modem. This + // might happen when the caller listens for a lower-layer network disconnect + // callback and stop the keepalive at that time. But the stop() races with the + // stop() generated in ConnectivityService network disconnection code path. + if (mStartedState == STOPPING && reason == ERROR_INVALID_NETWORK) return; + + // Store the reason of stopping, and report it after the keepalive is fully stopped. + if (mStopReason != ERROR_STOP_REASON_UNINITIALIZED) { + throw new IllegalStateException("Unexpected stop reason: " + mStopReason); + } + mStopReason = reason; + Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.toShortString() + + ": " + reason); + switch (mStartedState) { + case NOT_STARTED: + // Remove the reference of the keepalive that meet error before starting, + // e.g. invalid parameter. + cleanupStoppedKeepalive(mNai, mSlot); + break; + default: + mStartedState = STOPPING; + switch (mType) { + case TYPE_TCP: + mTcpController.stopSocketMonitor(mSlot); + // fall through + case TYPE_NATT: + mNai.onStopSocketKeepalive(mSlot); + mNai.onRemoveKeepalivePacketFilter(mSlot); + break; + default: + Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType); + } + } + + // Close the duplicated fd that maintains the lifecycle of socket whenever + // keepalive is running. + if (mFd != null) { + try { + Os.close(mFd); + } catch (ErrnoException e) { + // This should not happen since system server controls the lifecycle of fd when + // keepalive offload is running. + Log.wtf(TAG, "Error closing fd for keepalive " + mSlot + ": " + e); + } + } + } + + void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) { + handleStopKeepalive(mNai, mSlot, socketKeepaliveReason); + } + } + + void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) { + if (DBG) Log.w(TAG, "Sending onError(" + error + ") callback"); + try { + cb.onError(error); + } catch (RemoteException e) { + Log.w(TAG, "Discarded onError(" + error + ") callback"); + } + } + + private int findFirstFreeSlot(NetworkAgentInfo nai) { + HashMap networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives == null) { + networkKeepalives = new HashMap<Integer, KeepaliveInfo>(); + mKeepalives.put(nai, networkKeepalives); + } + + // Find the lowest-numbered free slot. Slot numbers start from 1, because that's what two + // separate chipset implementations independently came up with. + int slot; + for (slot = 1; slot <= networkKeepalives.size(); slot++) { + if (networkKeepalives.get(slot) == null) { + return slot; + } + } + return slot; + } + + public void handleStartKeepalive(Message message) { + KeepaliveInfo ki = (KeepaliveInfo) message.obj; + NetworkAgentInfo nai = ki.getNai(); + int slot = findFirstFreeSlot(nai); + mKeepalives.get(nai).put(slot, ki); + ki.start(slot); + } + + public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) { + final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives != null) { + final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values()); + for (KeepaliveInfo ki : kalist) { + ki.stop(reason); + // Clean up keepalives since the network agent is disconnected and unable to pass + // back asynchronous result of stop(). + cleanupStoppedKeepalive(nai, ki.mSlot); + } + } + } + + public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) { + final String networkName = NetworkAgentInfo.toShortString(nai); + HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives == null) { + Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + networkName); + return; + } + KeepaliveInfo ki = networkKeepalives.get(slot); + if (ki == null) { + Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + networkName); + return; + } + ki.stop(reason); + // Clean up keepalives will be done as a result of calling ki.stop() after the slots are + // freed. + } + + private void cleanupStoppedKeepalive(NetworkAgentInfo nai, int slot) { + final String networkName = NetworkAgentInfo.toShortString(nai); + HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives == null) { + Log.e(TAG, "Attempt to remove keepalive on nonexistent network " + networkName); + return; + } + KeepaliveInfo ki = networkKeepalives.get(slot); + if (ki == null) { + Log.e(TAG, "Attempt to remove nonexistent keepalive " + slot + " on " + networkName); + return; + } + + // Remove the keepalive from hash table so the slot can be considered available when reusing + // it. + networkKeepalives.remove(slot); + Log.d(TAG, "Remove keepalive " + slot + " on " + networkName + ", " + + networkKeepalives.size() + " remains."); + if (networkKeepalives.isEmpty()) { + mKeepalives.remove(nai); + } + + // Notify app that the keepalive is stopped. + final int reason = ki.mStopReason; + if (reason == SUCCESS) { + try { + ki.mCallback.onStopped(); + } catch (RemoteException e) { + Log.w(TAG, "Discarded onStop callback: " + reason); + } + } else if (reason == DATA_RECEIVED) { + try { + ki.mCallback.onDataReceived(); + } catch (RemoteException e) { + Log.w(TAG, "Discarded onDataReceived callback: " + reason); + } + } else if (reason == ERROR_STOP_REASON_UNINITIALIZED) { + throw new IllegalStateException("Unexpected stop reason: " + reason); + } else if (reason == ERROR_NO_SUCH_SLOT) { + throw new IllegalStateException("No such slot: " + reason); + } else { + notifyErrorCallback(ki.mCallback, reason); + } + + ki.unlinkDeathRecipient(); + } + + public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) { + HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives != null) { + ArrayList<Pair<Integer, Integer>> invalidKeepalives = new ArrayList<>(); + for (int slot : networkKeepalives.keySet()) { + int error = networkKeepalives.get(slot).isValid(); + if (error != SUCCESS) { + invalidKeepalives.add(Pair.create(slot, error)); + } + } + for (Pair<Integer, Integer> slotAndError: invalidKeepalives) { + handleStopKeepalive(nai, slotAndError.first, slotAndError.second); + } + } + } + + /** Handle keepalive events from lower layer. */ + public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) { + KeepaliveInfo ki = null; + try { + ki = mKeepalives.get(nai).get(slot); + } catch(NullPointerException e) {} + if (ki == null) { + Log.e(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason + + " for unknown keepalive " + slot + " on " + nai.toShortString()); + return; + } + + // This can be called in a number of situations : + // - startedState is STARTING. + // - reason is SUCCESS => go to STARTED. + // - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive. + // - startedState is STARTED. + // - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive. + // - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive. + // The control is not supposed to ever come here if the state is NOT_STARTED. This is + // because in NOT_STARTED state, the code will switch to STARTING before sending messages + // to start, and the only way to NOT_STARTED is this function, through the edges outlined + // above : in all cases, keepalive gets stopped and can't restart without going into + // STARTING as messages are ordered. This also depends on the hardware processing the + // messages in order. + // TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an + // option. + if (KeepaliveInfo.STARTING == ki.mStartedState) { + if (SUCCESS == reason) { + // Keepalive successfully started. + Log.d(TAG, "Started keepalive " + slot + " on " + nai.toShortString()); + ki.mStartedState = KeepaliveInfo.STARTED; + try { + ki.mCallback.onStarted(slot); + } catch (RemoteException e) { + Log.w(TAG, "Discarded onStarted(" + slot + ") callback"); + } + } else { + Log.d(TAG, "Failed to start keepalive " + slot + " on " + nai.toShortString() + + ": " + reason); + // The message indicated some error trying to start: do call handleStopKeepalive. + handleStopKeepalive(nai, slot, reason); + } + } else if (KeepaliveInfo.STOPPING == ki.mStartedState) { + // The message indicated result of stopping : clean up keepalive slots. + Log.d(TAG, "Stopped keepalive " + slot + " on " + nai.toShortString() + + " stopped: " + reason); + ki.mStartedState = KeepaliveInfo.NOT_STARTED; + cleanupStoppedKeepalive(nai, slot); + } else { + Log.wtf(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason + + " for keepalive in wrong state: " + ki.toString()); + } + } + + /** + * Called when requesting that keepalives be started on a IPsec NAT-T socket. See + * {@link android.net.SocketKeepalive}. + **/ + public void startNattKeepalive(@Nullable NetworkAgentInfo nai, + @Nullable FileDescriptor fd, + int intervalSeconds, + @NonNull ISocketKeepaliveCallback cb, + @NonNull String srcAddrString, + int srcPort, + @NonNull String dstAddrString, + int dstPort) { + if (nai == null) { + notifyErrorCallback(cb, ERROR_INVALID_NETWORK); + return; + } + + InetAddress srcAddress, dstAddress; + try { + srcAddress = InetAddresses.parseNumericAddress(srcAddrString); + dstAddress = InetAddresses.parseNumericAddress(dstAddrString); + } catch (IllegalArgumentException e) { + notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS); + return; + } + + KeepalivePacketData packet; + try { + packet = NattKeepalivePacketData.nattKeepalivePacket( + srcAddress, srcPort, dstAddress, NATT_PORT); + } catch (InvalidPacketException e) { + notifyErrorCallback(cb, e.getError()); + return; + } + KeepaliveInfo ki = null; + try { + ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds, + KeepaliveInfo.TYPE_NATT, fd); + } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { + Log.e(TAG, "Fail to construct keepalive", e); + notifyErrorCallback(cb, ERROR_INVALID_SOCKET); + return; + } + Log.d(TAG, "Created keepalive: " + ki.toString()); + mConnectivityServiceHandler.obtainMessage( + NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget(); + } + + /** + * Called by ConnectivityService to start TCP keepalive on a file descriptor. + * + * In order to offload keepalive for application correctly, sequence number, ack number and + * other fields are needed to form the keepalive packet. Thus, this function synchronously + * puts the socket into repair mode to get the necessary information. After the socket has been + * put into repair mode, the application cannot access the socket until reverted to normal. + * + * See {@link android.net.SocketKeepalive}. + **/ + public void startTcpKeepalive(@Nullable NetworkAgentInfo nai, + @NonNull FileDescriptor fd, + int intervalSeconds, + @NonNull ISocketKeepaliveCallback cb) { + if (nai == null) { + notifyErrorCallback(cb, ERROR_INVALID_NETWORK); + return; + } + + final TcpKeepalivePacketData packet; + try { + packet = TcpKeepaliveController.getTcpKeepalivePacket(fd); + } catch (InvalidSocketException e) { + notifyErrorCallback(cb, e.error); + return; + } catch (InvalidPacketException e) { + notifyErrorCallback(cb, e.getError()); + return; + } + KeepaliveInfo ki = null; + try { + ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds, + KeepaliveInfo.TYPE_TCP, fd); + } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) { + Log.e(TAG, "Fail to construct keepalive e=" + e); + notifyErrorCallback(cb, ERROR_INVALID_SOCKET); + return; + } + Log.d(TAG, "Created keepalive: " + ki.toString()); + mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget(); + } + + /** + * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is + * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the + * resource index bound to the {@link UdpEncapsulationSocket} when creating by + * {@link com.android.server.IpSecService} to verify whether the given + * {@link UdpEncapsulationSocket} is legitimate. + **/ + public void startNattKeepalive(@Nullable NetworkAgentInfo nai, + @Nullable FileDescriptor fd, + int resourceId, + int intervalSeconds, + @NonNull ISocketKeepaliveCallback cb, + @NonNull String srcAddrString, + @NonNull String dstAddrString, + int dstPort) { + // Ensure that the socket is created by IpSecService. + if (!isNattKeepaliveSocketValid(fd, resourceId)) { + notifyErrorCallback(cb, ERROR_INVALID_SOCKET); + } + + // Get src port to adopt old API. + int srcPort = 0; + try { + final SocketAddress srcSockAddr = Os.getsockname(fd); + srcPort = ((InetSocketAddress) srcSockAddr).getPort(); + } catch (ErrnoException e) { + notifyErrorCallback(cb, ERROR_INVALID_SOCKET); + } + + // Forward request to old API. + startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort, + dstAddrString, dstPort); + } + + /** + * Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid. + **/ + public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) { + // TODO: 1. confirm whether the fd is called from system api or created by IpSecService. + // 2. If the fd is created from the system api, check that it's bounded. And + // call dup to keep the fd open. + // 3. If the fd is created from IpSecService, check if the resource ID is valid. And + // hold the resource needed in IpSecService. + if (null == fd) { + return false; + } + return true; + } + + public void dump(IndentingPrintWriter pw) { + pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives)); + pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots); + pw.println("Allowed Unprivileged keepalives per uid: " + mAllowedUnprivilegedSlotsForUid); + pw.println("Socket keepalives:"); + pw.increaseIndent(); + for (NetworkAgentInfo nai : mKeepalives.keySet()) { + pw.println(nai.toShortString()); + pw.increaseIndent(); + for (int slot : mKeepalives.get(nai).keySet()) { + KeepaliveInfo ki = mKeepalives.get(nai).get(slot); + pw.println(slot + ": " + ki.toString()); + } + pw.decreaseIndent(); + } + pw.decreaseIndent(); + } +} diff --git a/service/src/com/android/server/connectivity/LingerMonitor.java b/service/src/com/android/server/connectivity/LingerMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..032612c6f093a4408a1e18659c650f3a1c6a9e13 --- /dev/null +++ b/service/src/com/android/server/connectivity/LingerMonitor.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.ConnectivityManager.NETID_UNSET; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.ConnectivityResources; +import android.net.NetworkCapabilities; +import android.os.SystemClock; +import android.os.UserHandle; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + +import com.android.connectivity.resources.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.MessageUtils; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; + +import java.util.Arrays; +import java.util.HashMap; + +/** + * Class that monitors default network linger events and possibly notifies the user of network + * switches. + * + * This class is not thread-safe and all its methods must be called on the ConnectivityService + * handler thread. + */ +public class LingerMonitor { + + private static final boolean DBG = true; + private static final boolean VDBG = false; + private static final String TAG = LingerMonitor.class.getSimpleName(); + + public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3; + public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS; + + private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap(); + @VisibleForTesting + public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName( + "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity")); + + @VisibleForTesting + public static final int NOTIFY_TYPE_NONE = 0; + public static final int NOTIFY_TYPE_NOTIFICATION = 1; + public static final int NOTIFY_TYPE_TOAST = 2; + + private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames( + new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" }); + + private final Context mContext; + final Resources mResources; + private final NetworkNotificationManager mNotifier; + private final int mDailyLimit; + private final long mRateLimitMillis; + + private long mFirstNotificationMillis; + private long mLastNotificationMillis; + private int mNotificationCounter; + + /** Current notifications. Maps the netId we switched away from to the netId we switched to. */ + private final SparseIntArray mNotifications = new SparseIntArray(); + + /** Whether we ever notified that we switched away from a particular network. */ + private final SparseBooleanArray mEverNotified = new SparseBooleanArray(); + + public LingerMonitor(Context context, NetworkNotificationManager notifier, + int dailyLimit, long rateLimitMillis) { + mContext = context; + mResources = new ConnectivityResources(mContext).get(); + mNotifier = notifier; + mDailyLimit = dailyLimit; + mRateLimitMillis = rateLimitMillis; + // Ensure that (now - mLastNotificationMillis) >= rateLimitMillis at first + mLastNotificationMillis = -rateLimitMillis; + } + + private static HashMap<String, Integer> makeTransportToNameMap() { + SparseArray<String> numberToName = MessageUtils.findMessageNames( + new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" }); + HashMap<String, Integer> nameToNumber = new HashMap<>(); + for (int i = 0; i < numberToName.size(); i++) { + // MessageUtils will fail to initialize if there are duplicate constant values, so there + // are no duplicates here. + nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i)); + } + return nameToNumber; + } + + private static boolean hasTransport(NetworkAgentInfo nai, int transport) { + return nai.networkCapabilities.hasTransport(transport); + } + + private int getNotificationSource(NetworkAgentInfo toNai) { + for (int i = 0; i < mNotifications.size(); i++) { + if (mNotifications.valueAt(i) == toNai.network.getNetId()) { + return mNotifications.keyAt(i); + } + } + return NETID_UNSET; + } + + private boolean everNotified(NetworkAgentInfo nai) { + return mEverNotified.get(nai.network.getNetId(), false); + } + + @VisibleForTesting + public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + // TODO: Evaluate moving to CarrierConfigManager. + String[] notifySwitches = mResources.getStringArray(R.array.config_networkNotifySwitches); + + if (VDBG) { + Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches)); + } + + for (String notifySwitch : notifySwitches) { + if (TextUtils.isEmpty(notifySwitch)) continue; + String[] transports = notifySwitch.split("-", 2); + if (transports.length != 2) { + Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch); + continue; + } + int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]); + int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]); + if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) { + return true; + } + } + + return false; + } + + private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + mNotifier.showNotification(fromNai.network.getNetId(), NotificationType.NETWORK_SWITCH, + fromNai, toNai, createNotificationIntent(), true); + } + + @VisibleForTesting + protected PendingIntent createNotificationIntent() { + return PendingIntent.getActivity( + mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + CELLULAR_SETTINGS, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + + // Removes any notification that was put up as a result of switching to nai. + private void maybeStopNotifying(NetworkAgentInfo nai) { + int fromNetId = getNotificationSource(nai); + if (fromNetId != NETID_UNSET) { + mNotifications.delete(fromNetId); + mNotifier.clearNotification(fromNetId); + // Toasts can't be deleted. + } + } + + // Notify the user of a network switch using a notification or a toast. + private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) { + int notifyType = mResources.getInteger(R.integer.config_networkNotifySwitchType); + if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) { + notifyType = NOTIFY_TYPE_TOAST; + } + + if (VDBG) { + Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType)); + } + + switch (notifyType) { + case NOTIFY_TYPE_NONE: + return; + case NOTIFY_TYPE_NOTIFICATION: + showNotification(fromNai, toNai); + break; + case NOTIFY_TYPE_TOAST: + mNotifier.showToast(fromNai, toNai); + break; + default: + Log.e(TAG, "Unknown notify type " + notifyType); + return; + } + + if (DBG) { + Log.d(TAG, "Notifying switch from=" + fromNai.toShortString() + + " to=" + toNai.toShortString() + + " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")")); + } + + mNotifications.put(fromNai.network.getNetId(), toNai.network.getNetId()); + mEverNotified.put(fromNai.network.getNetId(), true); + } + + /** + * Put up or dismiss a notification or toast for of a change in the default network if needed. + * + * Putting up a notification when switching from no network to some network is not supported + * and as such this method can't be called with a null |fromNai|. It can be called with a + * null |toNai| if there isn't a default network any more. + * + * @param fromNai switching from this NAI + * @param toNai switching to this NAI + */ + // The default network changed from fromNai to toNai due to a change in score. + public void noteLingerDefaultNetwork(@NonNull final NetworkAgentInfo fromNai, + @Nullable final NetworkAgentInfo toNai) { + if (VDBG) { + Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString() + + " everValidated=" + fromNai.everValidated + + " lastValidated=" + fromNai.lastValidated + + " to=" + toNai.toShortString()); + } + + // If we are currently notifying the user because the device switched to fromNai, now that + // we are switching away from it we should remove the notification. This includes the case + // where we switch back to toNai because its score improved again (e.g., because it regained + // Internet access). + maybeStopNotifying(fromNai); + + // If the network was simply lost (either because it disconnected or because it stopped + // being the default with no replacement), then don't show a notification. + if (null == toNai) return; + + // If this network never validated, don't notify. Otherwise, we could do things like: + // + // 1. Unvalidated wifi connects. + // 2. Unvalidated mobile data connects. + // 3. Cell validates, and we show a notification. + // or: + // 1. User connects to wireless printer. + // 2. User turns on cellular data. + // 3. We show a notification. + if (!fromNai.everValidated) return; + + // If this network is a captive portal, don't notify. This cannot happen on initial connect + // to a captive portal, because the everValidated check above will fail. However, it can + // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that + // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification. + // We don't want to overwrite that notification with this one; the user has already been + // notified, and of the two, the captive portal notification is the more useful one because + // it allows the user to sign in to the captive portal. In this case, display a toast + // in addition to the captive portal notification. + // + // Note that if the network we switch to is already up when the captive portal reappears, + // this won't work because NetworkMonitor tells ConnectivityService that the network is + // unvalidated (causing a switch) before asking it to show the sign in notification. In this + // case, the toast won't show and we'll only display the sign in notification. This is the + // best we can do at this time. + boolean forceToast = fromNai.networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL); + + // Only show the notification once, in order to avoid irritating the user every time. + // TODO: should we do this? + if (everNotified(fromNai)) { + if (VDBG) { + Log.d(TAG, "Not notifying handover from " + fromNai.toShortString() + + ", already notified"); + } + return; + } + + // Only show the notification if we switched away because a network became unvalidated, not + // because its score changed. + // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes + // unvalidated. + if (fromNai.lastValidated) return; + + if (!isNotificationEnabled(fromNai, toNai)) return; + + final long now = SystemClock.elapsedRealtime(); + if (isRateLimited(now) || isAboveDailyLimit(now)) return; + + notify(fromNai, toNai, forceToast); + } + + public void noteDisconnect(NetworkAgentInfo nai) { + mNotifications.delete(nai.network.getNetId()); + mEverNotified.delete(nai.network.getNetId()); + maybeStopNotifying(nai); + // No need to cancel notifications on nai: NetworkMonitor does that on disconnect. + } + + private boolean isRateLimited(long now) { + final long millisSinceLast = now - mLastNotificationMillis; + if (millisSinceLast < mRateLimitMillis) { + return true; + } + mLastNotificationMillis = now; + return false; + } + + private boolean isAboveDailyLimit(long now) { + if (mFirstNotificationMillis == 0) { + mFirstNotificationMillis = now; + } + final long millisSinceFirst = now - mFirstNotificationMillis; + if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) { + mNotificationCounter = 0; + mFirstNotificationMillis = 0; + } + if (mNotificationCounter >= mDailyLimit) { + return true; + } + mNotificationCounter++; + return false; + } +} diff --git a/service/src/com/android/server/connectivity/MockableSystemProperties.java b/service/src/com/android/server/connectivity/MockableSystemProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..a25b89ac039abb7228f39d3b7322ebf855c93c7a --- /dev/null +++ b/service/src/com/android/server/connectivity/MockableSystemProperties.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.os.SystemProperties; + +public class MockableSystemProperties { + + public String get(String key) { + return SystemProperties.get(key); + } + + public int getInt(String key, int def) { + return SystemProperties.getInt(key, def); + } + + public boolean getBoolean(String key, boolean def) { + return SystemProperties.getBoolean(key, def); + } +} diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java new file mode 100644 index 0000000000000000000000000000000000000000..c66a280f2b028ad8d9ad5cbac1abc4b4186039f1 --- /dev/null +++ b/service/src/com/android/server/connectivity/Nat464Xlat.java @@ -0,0 +1,523 @@ +/* + * 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 com.android.server.connectivity; + +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import static com.android.net.module.util.CollectionUtils.contains; + +import android.annotation.NonNull; +import android.net.ConnectivityManager; +import android.net.IDnsResolver; +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.NetworkInfo; +import android.net.RouteInfo; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.NetworkStackConstants; +import com.android.server.ConnectivityService; + +import java.net.Inet6Address; +import java.util.Objects; + +/** + * Class to manage a 464xlat CLAT daemon. Nat464Xlat is not thread safe and should be manipulated + * from a consistent and unique thread context. It is the responsibility of ConnectivityService to + * call into this class from its own Handler thread. + * + * @hide + */ +public class Nat464Xlat { + private static final String TAG = Nat464Xlat.class.getSimpleName(); + + // This must match the interface prefix in clatd.c. + private static final String CLAT_PREFIX = "v4-"; + + // The network types on which we will start clatd, + // allowing clat only on networks for which we can support IPv6-only. + private static final int[] NETWORK_TYPES = { + ConnectivityManager.TYPE_MOBILE, + ConnectivityManager.TYPE_WIFI, + ConnectivityManager.TYPE_ETHERNET, + }; + + // The network states in which running clatd is supported. + private static final NetworkInfo.State[] NETWORK_STATES = { + NetworkInfo.State.CONNECTED, + NetworkInfo.State.SUSPENDED, + }; + + private final IDnsResolver mDnsResolver; + private final INetd mNetd; + + // The network we're running on, and its type. + private final NetworkAgentInfo mNetwork; + + private enum State { + IDLE, // start() not called. Base iface and stacked iface names are null. + DISCOVERING, // same as IDLE, except prefix discovery in progress. + STARTING, // start() called. Base iface and stacked iface names are known. + RUNNING, // start() called, and the stacked iface is known to be up. + } + + /** + * NAT64 prefix currently in use. Only valid in STARTING or RUNNING states. + * Used, among other things, to avoid updates when switching from a prefix learned from one + * source (e.g., RA) to the same prefix learned from another source (e.g., RA). + */ + private IpPrefix mNat64PrefixInUse; + /** NAT64 prefix (if any) discovered from DNS via RFC 7050. */ + private IpPrefix mNat64PrefixFromDns; + /** NAT64 prefix (if any) learned from the network via RA. */ + private IpPrefix mNat64PrefixFromRa; + private String mBaseIface; + private String mIface; + private Inet6Address mIPv6Address; + private State mState = State.IDLE; + + private boolean mEnableClatOnCellular; + private boolean mPrefixDiscoveryRunning; + + public Nat464Xlat(NetworkAgentInfo nai, INetd netd, IDnsResolver dnsResolver, + ConnectivityService.Dependencies deps) { + mDnsResolver = dnsResolver; + mNetd = netd; + mNetwork = nai; + mEnableClatOnCellular = deps.getCellular464XlatEnabled(); + } + + /** + * Whether to attempt 464xlat on this network. This is true for an IPv6-only network that is + * currently connected and where the NetworkAgent has not disabled 464xlat. It is the signal to + * enable NAT64 prefix discovery. + * + * @param nai the NetworkAgentInfo corresponding to the network. + * @return true if the network requires clat, false otherwise. + */ + @VisibleForTesting + protected boolean requiresClat(NetworkAgentInfo nai) { + // TODO: migrate to NetworkCapabilities.TRANSPORT_*. + final boolean supported = contains(NETWORK_TYPES, nai.networkInfo.getType()); + final boolean connected = contains(NETWORK_STATES, nai.networkInfo.getState()); + + // Only run clat on networks that have a global IPv6 address and don't have a native IPv4 + // address. + LinkProperties lp = nai.linkProperties; + final boolean isIpv6OnlyNetwork = (lp != null) && lp.hasGlobalIpv6Address() + && !lp.hasIpv4Address(); + + // If the network tells us it doesn't use clat, respect that. + final boolean skip464xlat = (nai.netAgentConfig() != null) + && nai.netAgentConfig().skip464xlat; + + return supported && connected && isIpv6OnlyNetwork && !skip464xlat + && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR) + ? isCellular464XlatEnabled() : true); + } + + /** + * Whether the clat demon should be started on this network now. This is true if requiresClat is + * true and a NAT64 prefix has been discovered. + * + * @param nai the NetworkAgentInfo corresponding to the network. + * @return true if the network should start clat, false otherwise. + */ + @VisibleForTesting + protected boolean shouldStartClat(NetworkAgentInfo nai) { + LinkProperties lp = nai.linkProperties; + return requiresClat(nai) && lp != null && lp.getNat64Prefix() != null; + } + + /** + * @return true if clatd has been started and has not yet stopped. + * A true result corresponds to internal states STARTING and RUNNING. + */ + public boolean isStarted() { + return (mState == State.STARTING || mState == State.RUNNING); + } + + /** + * @return true if clatd has been started but the stacked interface is not yet up. + */ + public boolean isStarting() { + return mState == State.STARTING; + } + + /** + * @return true if clatd has been started and the stacked interface is up. + */ + public boolean isRunning() { + return mState == State.RUNNING; + } + + /** + * Start clatd, register this Nat464Xlat as a network observer for the stacked interface, + * and set internal state. + */ + private void enterStartingState(String baseIface) { + mNat64PrefixInUse = selectNat64Prefix(); + String addrStr = null; + try { + addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e); + } + mIface = CLAT_PREFIX + baseIface; + mBaseIface = baseIface; + mState = State.STARTING; + try { + mIPv6Address = (Inet6Address) InetAddresses.parseNumericAddress(addrStr); + } catch (ClassCastException | IllegalArgumentException | NullPointerException e) { + Log.e(TAG, "Invalid IPv6 address " + addrStr); + } + if (mPrefixDiscoveryRunning && !isPrefixDiscoveryNeeded()) { + stopPrefixDiscovery(); + } + if (!mPrefixDiscoveryRunning) { + setPrefix64(mNat64PrefixInUse); + } + } + + /** + * Enter running state just after getting confirmation that the stacked interface is up, and + * turn ND offload off if on WiFi. + */ + private void enterRunningState() { + mState = State.RUNNING; + } + + /** + * Unregister as a base observer for the stacked interface, and clear internal state. + */ + private void leaveStartedState() { + mNat64PrefixInUse = null; + mIface = null; + mBaseIface = null; + + if (!mPrefixDiscoveryRunning) { + setPrefix64(null); + } + + if (isPrefixDiscoveryNeeded()) { + if (!mPrefixDiscoveryRunning) { + startPrefixDiscovery(); + } + mState = State.DISCOVERING; + } else { + stopPrefixDiscovery(); + mState = State.IDLE; + } + } + + @VisibleForTesting + protected void start() { + if (isStarted()) { + Log.e(TAG, "startClat: already started"); + return; + } + + String baseIface = mNetwork.linkProperties.getInterfaceName(); + if (baseIface == null) { + Log.e(TAG, "startClat: Can't start clat on null interface"); + return; + } + // TODO: should we only do this if mNetd.clatdStart() succeeds? + Log.i(TAG, "Starting clatd on " + baseIface); + enterStartingState(baseIface); + } + + @VisibleForTesting + protected void stop() { + if (!isStarted()) { + Log.e(TAG, "stopClat: already stopped"); + return; + } + + Log.i(TAG, "Stopping clatd on " + mBaseIface); + try { + mNetd.clatdStop(mBaseIface); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e); + } + + String iface = mIface; + boolean wasRunning = isRunning(); + + // Change state before updating LinkProperties. handleUpdateLinkProperties ends up calling + // fixupLinkProperties, and if at that time the state is still RUNNING, fixupLinkProperties + // would wrongly inform ConnectivityService that there is still a stacked interface. + leaveStartedState(); + + if (wasRunning) { + LinkProperties lp = new LinkProperties(mNetwork.linkProperties); + lp.removeStackedLink(iface); + mNetwork.connService().handleUpdateLinkProperties(mNetwork, lp); + } + } + + private void startPrefixDiscovery() { + try { + mDnsResolver.startPrefix64Discovery(getNetId()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error starting prefix discovery on netId " + getNetId() + ": " + e); + } + mPrefixDiscoveryRunning = true; + } + + private void stopPrefixDiscovery() { + try { + mDnsResolver.stopPrefix64Discovery(getNetId()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error stopping prefix discovery on netId " + getNetId() + ": " + e); + } + mPrefixDiscoveryRunning = false; + } + + private boolean isPrefixDiscoveryNeeded() { + // If there is no NAT64 prefix in the RA, prefix discovery is always needed. It cannot be + // stopped after it succeeds, because stopping it will cause netd to report that the prefix + // has been removed, and that will cause us to stop clatd. + return requiresClat(mNetwork) && mNat64PrefixFromRa == null; + } + + private void setPrefix64(IpPrefix prefix) { + final String prefixString = (prefix != null) ? prefix.toString() : ""; + try { + mDnsResolver.setPrefix64(getNetId(), prefixString); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error setting NAT64 prefix on netId " + getNetId() + " to " + + prefix + ": " + e); + } + } + + private void maybeHandleNat64PrefixChange() { + final IpPrefix newPrefix = selectNat64Prefix(); + if (!Objects.equals(mNat64PrefixInUse, newPrefix)) { + Log.d(TAG, "NAT64 prefix changed from " + mNat64PrefixInUse + " to " + + newPrefix); + stop(); + // It's safe to call update here, even though this method is called from update, because + // stop() is guaranteed to have moved out of STARTING and RUNNING, which are the only + // states in which this method can be called. + update(); + } + } + + /** + * Starts/stops NAT64 prefix discovery and clatd as necessary. + */ + public void update() { + // TODO: turn this class into a proper StateMachine. http://b/126113090 + switch (mState) { + case IDLE: + if (isPrefixDiscoveryNeeded()) { + startPrefixDiscovery(); // Enters DISCOVERING state. + mState = State.DISCOVERING; + } else if (requiresClat(mNetwork)) { + start(); // Enters STARTING state. + } + break; + + case DISCOVERING: + if (shouldStartClat(mNetwork)) { + // NAT64 prefix detected. Start clatd. + start(); // Enters STARTING state. + return; + } + if (!requiresClat(mNetwork)) { + // IPv4 address added. Go back to IDLE state. + stopPrefixDiscovery(); + mState = State.IDLE; + return; + } + break; + + case STARTING: + case RUNNING: + // NAT64 prefix removed, or IPv4 address added. + // Stop clatd and go back into DISCOVERING or idle. + if (!shouldStartClat(mNetwork)) { + stop(); + break; + } + // Only necessary while clat is actually started. + maybeHandleNat64PrefixChange(); + break; + } + } + + /** + * Picks a NAT64 prefix to use. Always prefers the prefix from the RA if one is received from + * both RA and DNS, because the prefix in the RA has better security and updatability, and will + * almost always be received first anyway. + * + * Any network that supports legacy hosts will support discovering the DNS64 prefix via DNS as + * well. If the prefix from the RA is withdrawn, fall back to that for reliability purposes. + */ + private IpPrefix selectNat64Prefix() { + return mNat64PrefixFromRa != null ? mNat64PrefixFromRa : mNat64PrefixFromDns; + } + + public void setNat64PrefixFromRa(IpPrefix prefix) { + mNat64PrefixFromRa = prefix; + } + + public void setNat64PrefixFromDns(IpPrefix prefix) { + mNat64PrefixFromDns = prefix; + } + + /** + * Copies the stacked clat link in oldLp, if any, to the passed LinkProperties. + * This is necessary because the LinkProperties in mNetwork come from the transport layer, which + * has no idea that 464xlat is running on top of it. + */ + public void fixupLinkProperties(@NonNull LinkProperties oldLp, @NonNull LinkProperties lp) { + // This must be done even if clatd is not running, because otherwise shouldStartClat would + // never return true. + lp.setNat64Prefix(selectNat64Prefix()); + + if (!isRunning()) { + return; + } + if (lp.getAllInterfaceNames().contains(mIface)) { + return; + } + + Log.d(TAG, "clatd running, updating NAI for " + mIface); + for (LinkProperties stacked: oldLp.getStackedLinks()) { + if (Objects.equals(mIface, stacked.getInterfaceName())) { + lp.addStackedLink(stacked); + return; + } + } + } + + private LinkProperties makeLinkProperties(LinkAddress clatAddress) { + LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName(mIface); + + // Although the clat interface is a point-to-point tunnel, we don't + // point the route directly at the interface because some apps don't + // understand routes without gateways (see, e.g., http://b/9597256 + // http://b/9597516). Instead, set the next hop of the route to the + // clat IPv4 address itself (for those apps, it doesn't matter what + // the IP of the gateway is, only that there is one). + RouteInfo ipv4Default = new RouteInfo( + new LinkAddress(NetworkStackConstants.IPV4_ADDR_ANY, 0), + clatAddress.getAddress(), mIface); + stacked.addRoute(ipv4Default); + stacked.addLinkAddress(clatAddress); + return stacked; + } + + private LinkAddress getLinkAddress(String iface) { + try { + final InterfaceConfigurationParcel config = mNetd.interfaceGetCfg(iface); + return new LinkAddress( + InetAddresses.parseNumericAddress(config.ipv4Addr), config.prefixLength); + } catch (IllegalArgumentException | RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error getting link properties: " + e); + return null; + } + } + + /** + * Adds stacked link on base link and transitions to RUNNING state. + */ + private void handleInterfaceLinkStateChanged(String iface, boolean up) { + // TODO: if we call start(), then stop(), then start() again, and the + // interfaceLinkStateChanged notification for the first start is delayed past the first + // stop, then the code becomes out of sync with system state and will behave incorrectly. + // + // This is not trivial to fix because: + // 1. It is not guaranteed that start() will eventually result in the interface coming up, + // because there could be an error starting clat (e.g., if the interface goes down before + // the packet socket can be bound). + // 2. If start is called multiple times, there is nothing in the interfaceLinkStateChanged + // notification that says which start() call the interface was created by. + // + // Once this code is converted to StateMachine, it will be possible to use deferMessage to + // ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires, + // and possibly use a timeout (or provide some guarantees at the lower layer) to address #1. + if (!isStarting() || !up || !Objects.equals(mIface, iface)) { + return; + } + + LinkAddress clatAddress = getLinkAddress(iface); + if (clatAddress == null) { + Log.e(TAG, "clatAddress was null for stacked iface " + iface); + return; + } + + Log.i(TAG, String.format("interface %s is up, adding stacked link %s on top of %s", + mIface, mIface, mBaseIface)); + enterRunningState(); + LinkProperties lp = new LinkProperties(mNetwork.linkProperties); + lp.addStackedLink(makeLinkProperties(clatAddress)); + mNetwork.connService().handleUpdateLinkProperties(mNetwork, lp); + } + + /** + * Removes stacked link on base link and transitions to IDLE state. + */ + private void handleInterfaceRemoved(String iface) { + if (!Objects.equals(mIface, iface)) { + return; + } + if (!isRunning()) { + return; + } + + Log.i(TAG, "interface " + iface + " removed"); + // If we're running, and the interface was removed, then we didn't call stop(), and it's + // likely that clatd crashed. Ensure we call stop() so we can start clatd again. Calling + // stop() will also update LinkProperties, and if clatd crashed, the LinkProperties update + // will cause ConnectivityService to call start() again. + stop(); + } + + public void interfaceLinkStateChanged(String iface, boolean up) { + mNetwork.handler().post(() -> { handleInterfaceLinkStateChanged(iface, up); }); + } + + public void interfaceRemoved(String iface) { + mNetwork.handler().post(() -> handleInterfaceRemoved(iface)); + } + + @Override + public String toString() { + return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState; + } + + @VisibleForTesting + protected int getNetId() { + return mNetwork.network.getNetId(); + } + + @VisibleForTesting + protected boolean isCellular464XlatEnabled() { + return mEnableClatOnCellular; + } +} diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..18becd45f6d8b4e901b380bdde42474b66c2c52a --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java @@ -0,0 +1,1215 @@ +/* + * 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.server.connectivity; + +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.transportNamesOf; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.CaptivePortalData; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.INetworkAgent; +import android.net.INetworkAgentRegistry; +import android.net.INetworkMonitor; +import android.net.LinkProperties; +import android.net.NattKeepalivePacketData; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkMonitorManager; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.NetworkStateSnapshot; +import android.net.QosCallbackException; +import android.net.QosFilter; +import android.net.QosFilterParcelable; +import android.net.QosSession; +import android.net.TcpKeepalivePacketData; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemClock; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import com.android.internal.util.WakeupMessage; +import com.android.server.ConnectivityService; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * A bag class used by ConnectivityService for holding a collection of most recent + * information published by a particular NetworkAgent as well as the + * AsyncChannel/messenger for reaching that NetworkAgent and lists of NetworkRequests + * interested in using it. Default sort order is descending by score. + */ +// States of a network: +// -------------------- +// 1. registered, uncreated, disconnected, unvalidated +// This state is entered when a NetworkFactory registers a NetworkAgent in any state except +// the CONNECTED state. +// 2. registered, uncreated, connecting, unvalidated +// This state is entered when a registered NetworkAgent for a VPN network transitions to the +// CONNECTING state (TODO: go through this state for every network, not just VPNs). +// ConnectivityService will tell netd to create the network early in order to add extra UID +// routing rules referencing the netID. These rules need to be in place before the network is +// connected to avoid racing against client apps trying to connect to a half-setup network. +// 3. registered, uncreated, connected, unvalidated +// This state is entered when a registered NetworkAgent transitions to the CONNECTED state. +// ConnectivityService will tell netd to create the network if it was not already created, and +// immediately transition to state #4. +// 4. registered, created, connected, unvalidated +// If this network can satisfy the default NetworkRequest, then NetworkMonitor will +// probe for Internet connectivity. +// If this network cannot satisfy the default NetworkRequest, it will immediately be +// transitioned to state #5. +// A network may remain in this state if NetworkMonitor fails to find Internet connectivity, +// for example: +// a. a captive portal is present, or +// b. a WiFi router whose Internet backhaul is down, or +// c. a wireless connection stops transfering packets temporarily (e.g. device is in elevator +// or tunnel) but does not disconnect from the AP/cell tower, or +// d. a stand-alone device offering a WiFi AP without an uplink for configuration purposes. +// 5. registered, created, connected, validated +// +// The device's default network connection: +// ---------------------------------------- +// Networks in states #4 and #5 may be used as a device's default network connection if they +// satisfy the default NetworkRequest. +// A network, that satisfies the default NetworkRequest, in state #5 should always be chosen +// in favor of a network, that satisfies the default NetworkRequest, in state #4. +// When deciding between two networks, that both satisfy the default NetworkRequest, to select +// for the default network connection, the one with the higher score should be chosen. +// +// When a network disconnects: +// --------------------------- +// If a network's transport disappears, for example: +// a. WiFi turned off, or +// b. cellular data turned off, or +// c. airplane mode is turned on, or +// d. a wireless connection disconnects from AP/cell tower entirely (e.g. device is out of range +// of AP for an extended period of time, or switches to another AP without roaming) +// then that network can transition from any state (#1-#5) to unregistered. This happens by +// the transport disconnecting their NetworkAgent's AsyncChannel with ConnectivityManager. +// ConnectivityService also tells netd to destroy the network. +// +// When ConnectivityService disconnects a network: +// ----------------------------------------------- +// If a network is just connected, ConnectivityService will think it will be used soon, but might +// not be used. Thus, a 5s timer will be held to prevent the network being torn down immediately. +// This "nascent" state is implemented by the "lingering" logic below without relating to any +// request, and is used in some cases where network requests race with network establishment. The +// nascent state ends when the 5-second timer fires, or as soon as the network satisfies a +// request, whichever is earlier. In this state, the network is considered in the background. +// +// If a network has no chance of satisfying any requests (even if it were to become validated +// and enter state #5), ConnectivityService will disconnect the NetworkAgent's AsyncChannel. +// +// If the network was satisfying a foreground NetworkRequest (i.e. had been the highest scoring that +// satisfied the NetworkRequest's constraints), but is no longer the highest scoring network for any +// foreground NetworkRequest, then there will be a 30s pause to allow network communication to be +// wrapped up rather than abruptly terminated. During this pause the network is said to be +// "lingering". During this pause if the network begins satisfying a foreground NetworkRequest, +// ConnectivityService will cancel the future disconnection of the NetworkAgent's AsyncChannel, and +// the network is no longer considered "lingering". After the linger timer expires, if the network +// is satisfying one or more background NetworkRequests it is kept up in the background. If it is +// not, ConnectivityService disconnects the NetworkAgent's AsyncChannel. +public class NetworkAgentInfo implements Comparable<NetworkAgentInfo>, NetworkRanker.Scoreable { + + @NonNull public NetworkInfo networkInfo; + // This Network object should always be used if possible, so as to encourage reuse of the + // enclosed socket factory and connection pool. Avoid creating other Network objects. + // This Network object is always valid. + @NonNull public final Network network; + @NonNull public LinkProperties linkProperties; + // This should only be modified by ConnectivityService, via setNetworkCapabilities(). + // TODO: make this private with a getter. + @NonNull public NetworkCapabilities networkCapabilities; + @NonNull public final NetworkAgentConfig networkAgentConfig; + + // Underlying networks declared by the agent. Only set if supportsUnderlyingNetworks is true. + // The networks in this list might be declared by a VPN app using setUnderlyingNetworks and are + // not guaranteed to be current or correct, or even to exist. + // + // This array is read and iterated on multiple threads with no locking so its contents must + // never be modified. When the list of networks changes, replace with a new array, on the + // handler thread. + public @Nullable volatile Network[] declaredUnderlyingNetworks; + + // The capabilities originally announced by the NetworkAgent, regardless of any capabilities + // that were added or removed due to this network's underlying networks. + // Only set if #supportsUnderlyingNetworks is true. + public @Nullable NetworkCapabilities declaredCapabilities; + + // Indicates if netd has been told to create this Network. From this point on the appropriate + // routing rules are setup and routes are added so packets can begin flowing over the Network. + // This is a sticky bit; once set it is never cleared. + public boolean created; + // Set to true after the first time this network is marked as CONNECTED. Once set, the network + // shows up in API calls, is able to satisfy NetworkRequests and can become the default network. + // This is a sticky bit; once set it is never cleared. + public boolean everConnected; + // Set to true if this Network successfully passed validation or if it did not satisfy the + // default NetworkRequest in which case validation will not be attempted. + // This is a sticky bit; once set it is never cleared even if future validation attempts fail. + public boolean everValidated; + + // The result of the last validation attempt on this network (true if validated, false if not). + public boolean lastValidated; + + // If true, becoming unvalidated will lower the network's score. This is only meaningful if the + // system is configured not to do this for certain networks, e.g., if the + // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via + // Settings.Global.NETWORK_AVOID_BAD_WIFI. + public boolean avoidUnvalidated; + + // Whether a captive portal was ever detected on this network. + // This is a sticky bit; once set it is never cleared. + public boolean everCaptivePortalDetected; + + // Whether a captive portal was found during the last network validation attempt. + public boolean lastCaptivePortalDetected; + + // Set to true when partial connectivity was detected. + public boolean partialConnectivity; + + // Delay between when the network is disconnected and when the native network is destroyed. + public int teardownDelayMs; + + // Captive portal info of the network from RFC8908, if any. + // Obtained by ConnectivityService and merged into NetworkAgent-provided information. + public CaptivePortalData capportApiData; + + // The UID of the remote entity that created this Network. + public final int creatorUid; + + // Network agent portal info of the network, if any. This information is provided from + // non-RFC8908 sources, such as Wi-Fi Passpoint, which can provide information such as Venue + // URL, Terms & Conditions URL, and network friendly name. + public CaptivePortalData networkAgentPortalData; + + // Networks are lingered when they become unneeded as a result of their NetworkRequests being + // satisfied by a higher-scoring network. so as to allow communication to wrap up before the + // network is taken down. This usually only happens to the default network. Lingering ends with + // either the linger timeout expiring and the network being taken down, or the network + // satisfying a request again. + public static class InactivityTimer implements Comparable<InactivityTimer> { + public final int requestId; + public final long expiryMs; + + public InactivityTimer(int requestId, long expiryMs) { + this.requestId = requestId; + this.expiryMs = expiryMs; + } + public boolean equals(Object o) { + if (!(o instanceof InactivityTimer)) return false; + InactivityTimer other = (InactivityTimer) o; + return (requestId == other.requestId) && (expiryMs == other.expiryMs); + } + public int hashCode() { + return Objects.hash(requestId, expiryMs); + } + public int compareTo(InactivityTimer other) { + return (expiryMs != other.expiryMs) ? + Long.compare(expiryMs, other.expiryMs) : + Integer.compare(requestId, other.requestId); + } + public String toString() { + return String.format("%s, expires %dms", requestId, + expiryMs - SystemClock.elapsedRealtime()); + } + } + + /** + * Inform ConnectivityService that the network LINGER period has + * expired. + * obj = this NetworkAgentInfo + */ + public static final int EVENT_NETWORK_LINGER_COMPLETE = 1001; + + /** + * Inform ConnectivityService that the agent is half-connected. + * arg1 = ARG_AGENT_SUCCESS or ARG_AGENT_FAILURE + * obj = NetworkAgentInfo + * @hide + */ + public static final int EVENT_AGENT_REGISTERED = 1002; + + /** + * Inform ConnectivityService that the agent was disconnected. + * obj = NetworkAgentInfo + * @hide + */ + public static final int EVENT_AGENT_DISCONNECTED = 1003; + + /** + * Argument for EVENT_AGENT_HALF_CONNECTED indicating failure. + */ + public static final int ARG_AGENT_FAILURE = 0; + + /** + * Argument for EVENT_AGENT_HALF_CONNECTED indicating success. + */ + public static final int ARG_AGENT_SUCCESS = 1; + + // How long this network should linger for. + private int mLingerDurationMs; + + // All inactivity timers for this network, sorted by expiry time. A timer is added whenever + // a request is moved to a network with a better score, regardless of whether the network is or + // was lingering or not. An inactivity timer is also added when a network connects + // without immediately satisfying any requests. + // TODO: determine if we can replace this with a smaller or unsorted data structure. (e.g., + // SparseLongArray) combined with the timestamp of when the last timer is scheduled to fire. + private final SortedSet<InactivityTimer> mInactivityTimers = new TreeSet<>(); + + // For fast lookups. Indexes into mInactivityTimers by request ID. + private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>(); + + // Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of + // whether the network is inactive or not. Always set to the expiry of the mInactivityTimers + // that expires last. When the timer fires, all inactivity state is cleared, and if the network + // has no requests, it is torn down. + private WakeupMessage mInactivityMessage; + + // Inactivity expiry. Holds the expiry time of the inactivity timer, or 0 if the timer is not + // armed. + private long mInactivityExpiryMs; + + // Whether the network is inactive or not. Must be maintained separately from the above because + // it depends on the state of other networks and requests, which only ConnectivityService knows. + // (Example: we don't linger a network if it would become the best for a NetworkRequest if it + // validated). + private boolean mInactive; + + // This represents the quality of the network. As opposed to NetworkScore, FullScore includes + // the ConnectivityService-managed bits. + private FullScore mScore; + + // The list of NetworkRequests being satisfied by this Network. + private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>(); + + // How many of the satisfied requests are actual requests and not listens. + private int mNumRequestNetworkRequests = 0; + + // How many of the satisfied requests are of type BACKGROUND_REQUEST. + private int mNumBackgroundNetworkRequests = 0; + + // The last ConnectivityReport made available for this network. This value is only null before a + // report is generated. Once non-null, it will never be null again. + @Nullable private ConnectivityReport mConnectivityReport; + + public final INetworkAgent networkAgent; + // Only accessed from ConnectivityService handler thread + private final AgentDeathMonitor mDeathMonitor = new AgentDeathMonitor(); + + public final int factorySerialNumber; + + // Used by ConnectivityService to keep track of 464xlat. + public final Nat464Xlat clatd; + + // Set after asynchronous creation of the NetworkMonitor. + private volatile NetworkMonitorManager mNetworkMonitor; + + private static final String TAG = ConnectivityService.class.getSimpleName(); + private static final boolean VDBG = false; + private final ConnectivityService mConnService; + private final Context mContext; + private final Handler mHandler; + private final QosCallbackTracker mQosCallbackTracker; + + public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info, + @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc, + @NonNull NetworkScore score, Context context, + Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd, + IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid, + int lingerDurationMs, QosCallbackTracker qosCallbackTracker, + ConnectivityService.Dependencies deps) { + Objects.requireNonNull(net); + Objects.requireNonNull(info); + Objects.requireNonNull(lp); + Objects.requireNonNull(nc); + Objects.requireNonNull(context); + Objects.requireNonNull(config); + Objects.requireNonNull(qosCallbackTracker); + networkAgent = na; + network = net; + networkInfo = info; + linkProperties = lp; + networkCapabilities = nc; + networkAgentConfig = config; + mConnService = connService; + setScore(score); // uses members connService, networkCapabilities and networkAgentConfig + clatd = new Nat464Xlat(this, netd, dnsResolver, deps); + mContext = context; + mHandler = handler; + this.factorySerialNumber = factorySerialNumber; + this.creatorUid = creatorUid; + mLingerDurationMs = lingerDurationMs; + mQosCallbackTracker = qosCallbackTracker; + } + + private class AgentDeathMonitor implements IBinder.DeathRecipient { + @Override + public void binderDied() { + notifyDisconnected(); + } + } + + /** + * Notify the NetworkAgent that it was registered, and should be unregistered if it dies. + * + * Must be called from the ConnectivityService handler thread. A NetworkAgent can only be + * registered once. + */ + public void notifyRegistered() { + try { + networkAgent.asBinder().linkToDeath(mDeathMonitor, 0); + networkAgent.onRegistered(new NetworkAgentMessageHandler(mHandler)); + } catch (RemoteException e) { + Log.e(TAG, "Error registering NetworkAgent", e); + maybeUnlinkDeathMonitor(); + mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_FAILURE, 0, this) + .sendToTarget(); + return; + } + + mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_SUCCESS, 0, this).sendToTarget(); + } + + /** + * Disconnect the NetworkAgent. Must be called from the ConnectivityService handler thread. + */ + public void disconnect() { + try { + networkAgent.onDisconnected(); + } catch (RemoteException e) { + Log.i(TAG, "Error disconnecting NetworkAgent", e); + // Fall through: it's fine if the remote has died + } + + notifyDisconnected(); + maybeUnlinkDeathMonitor(); + } + + private void maybeUnlinkDeathMonitor() { + try { + networkAgent.asBinder().unlinkToDeath(mDeathMonitor, 0); + } catch (NoSuchElementException e) { + // Was not linked: ignore + } + } + + private void notifyDisconnected() { + // Note this may be called multiple times if ConnectivityService disconnects while the + // NetworkAgent also dies. ConnectivityService ignores disconnects of already disconnected + // agents. + mHandler.obtainMessage(EVENT_AGENT_DISCONNECTED, this).sendToTarget(); + } + + /** + * Notify the NetworkAgent that bandwidth update was requested. + */ + public void onBandwidthUpdateRequested() { + try { + networkAgent.onBandwidthUpdateRequested(); + } catch (RemoteException e) { + Log.e(TAG, "Error sending bandwidth update request event", e); + } + } + + /** + * Notify the NetworkAgent that validation status has changed. + */ + public void onValidationStatusChanged(int validationStatus, @Nullable String captivePortalUrl) { + try { + networkAgent.onValidationStatusChanged(validationStatus, captivePortalUrl); + } catch (RemoteException e) { + Log.e(TAG, "Error sending validation status change event", e); + } + } + + /** + * Notify the NetworkAgent that the acceptUnvalidated setting should be saved. + */ + public void onSaveAcceptUnvalidated(boolean acceptUnvalidated) { + try { + networkAgent.onSaveAcceptUnvalidated(acceptUnvalidated); + } catch (RemoteException e) { + Log.e(TAG, "Error sending accept unvalidated event", e); + } + } + + /** + * Notify the NetworkAgent that NATT socket keepalive should be started. + */ + public void onStartNattSocketKeepalive(int slot, int intervalDurationMs, + @NonNull NattKeepalivePacketData packetData) { + try { + networkAgent.onStartNattSocketKeepalive(slot, intervalDurationMs, packetData); + } catch (RemoteException e) { + Log.e(TAG, "Error sending NATT socket keepalive start event", e); + } + } + + /** + * Notify the NetworkAgent that TCP socket keepalive should be started. + */ + public void onStartTcpSocketKeepalive(int slot, int intervalDurationMs, + @NonNull TcpKeepalivePacketData packetData) { + try { + networkAgent.onStartTcpSocketKeepalive(slot, intervalDurationMs, packetData); + } catch (RemoteException e) { + Log.e(TAG, "Error sending TCP socket keepalive start event", e); + } + } + + /** + * Notify the NetworkAgent that socket keepalive should be stopped. + */ + public void onStopSocketKeepalive(int slot) { + try { + networkAgent.onStopSocketKeepalive(slot); + } catch (RemoteException e) { + Log.e(TAG, "Error sending TCP socket keepalive stop event", e); + } + } + + /** + * Notify the NetworkAgent that signal strength thresholds should be updated. + */ + public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) { + try { + networkAgent.onSignalStrengthThresholdsUpdated(thresholds); + } catch (RemoteException e) { + Log.e(TAG, "Error sending signal strength thresholds event", e); + } + } + + /** + * Notify the NetworkAgent that automatic reconnect should be prevented. + */ + public void onPreventAutomaticReconnect() { + try { + networkAgent.onPreventAutomaticReconnect(); + } catch (RemoteException e) { + Log.e(TAG, "Error sending prevent automatic reconnect event", e); + } + } + + /** + * Notify the NetworkAgent that a NATT keepalive packet filter should be added. + */ + public void onAddNattKeepalivePacketFilter(int slot, + @NonNull NattKeepalivePacketData packetData) { + try { + networkAgent.onAddNattKeepalivePacketFilter(slot, packetData); + } catch (RemoteException e) { + Log.e(TAG, "Error sending add NATT keepalive packet filter event", e); + } + } + + /** + * Notify the NetworkAgent that a TCP keepalive packet filter should be added. + */ + public void onAddTcpKeepalivePacketFilter(int slot, + @NonNull TcpKeepalivePacketData packetData) { + try { + networkAgent.onAddTcpKeepalivePacketFilter(slot, packetData); + } catch (RemoteException e) { + Log.e(TAG, "Error sending add TCP keepalive packet filter event", e); + } + } + + /** + * Notify the NetworkAgent that a keepalive packet filter should be removed. + */ + public void onRemoveKeepalivePacketFilter(int slot) { + try { + networkAgent.onRemoveKeepalivePacketFilter(slot); + } catch (RemoteException e) { + Log.e(TAG, "Error sending remove keepalive packet filter event", e); + } + } + + /** + * Notify the NetworkAgent that the qos filter should be registered against the given qos + * callback id. + */ + public void onQosFilterCallbackRegistered(final int qosCallbackId, + final QosFilter qosFilter) { + try { + networkAgent.onQosFilterCallbackRegistered(qosCallbackId, + new QosFilterParcelable(qosFilter)); + } catch (final RemoteException e) { + Log.e(TAG, "Error registering a qos callback id against a qos filter", e); + } + } + + /** + * Notify the NetworkAgent that the given qos callback id should be unregistered. + */ + public void onQosCallbackUnregistered(final int qosCallbackId) { + try { + networkAgent.onQosCallbackUnregistered(qosCallbackId); + } catch (RemoteException e) { + Log.e(TAG, "Error unregistering a qos callback id", e); + } + } + + /** + * Notify the NetworkAgent that the network is successfully connected. + */ + public void onNetworkCreated() { + try { + networkAgent.onNetworkCreated(); + } catch (RemoteException e) { + Log.e(TAG, "Error sending network created event", e); + } + } + + /** + * Notify the NetworkAgent that the native network has been destroyed. + */ + public void onNetworkDestroyed() { + try { + networkAgent.onNetworkDestroyed(); + } catch (RemoteException e) { + Log.e(TAG, "Error sending network destroyed event", e); + } + } + + // TODO: consider moving out of NetworkAgentInfo into its own class + private class NetworkAgentMessageHandler extends INetworkAgentRegistry.Stub { + private final Handler mHandler; + + private NetworkAgentMessageHandler(Handler handler) { + mHandler = handler; + } + + @Override + public void sendNetworkCapabilities(@NonNull NetworkCapabilities nc) { + Objects.requireNonNull(nc); + mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED, + new Pair<>(NetworkAgentInfo.this, nc)).sendToTarget(); + } + + @Override + public void sendLinkProperties(@NonNull LinkProperties lp) { + Objects.requireNonNull(lp); + mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED, + new Pair<>(NetworkAgentInfo.this, lp)).sendToTarget(); + } + + @Override + public void sendNetworkInfo(@NonNull NetworkInfo info) { + Objects.requireNonNull(info); + mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_INFO_CHANGED, + new Pair<>(NetworkAgentInfo.this, info)).sendToTarget(); + } + + @Override + public void sendScore(@NonNull final NetworkScore score) { + mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED, + new Pair<>(NetworkAgentInfo.this, score)).sendToTarget(); + } + + @Override + public void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial) { + mHandler.obtainMessage(NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED, + explicitlySelected ? 1 : 0, acceptPartial ? 1 : 0, + new Pair<>(NetworkAgentInfo.this, null)).sendToTarget(); + } + + @Override + public void sendSocketKeepaliveEvent(int slot, int reason) { + mHandler.obtainMessage(NetworkAgent.EVENT_SOCKET_KEEPALIVE, + slot, reason, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget(); + } + + @Override + public void sendUnderlyingNetworks(@Nullable List<Network> networks) { + mHandler.obtainMessage(NetworkAgent.EVENT_UNDERLYING_NETWORKS_CHANGED, + new Pair<>(NetworkAgentInfo.this, networks)).sendToTarget(); + } + + @Override + public void sendEpsQosSessionAvailable(final int qosCallbackId, final QosSession session, + final EpsBearerQosSessionAttributes attributes) { + mQosCallbackTracker.sendEventEpsQosSessionAvailable(qosCallbackId, session, attributes); + } + + @Override + public void sendNrQosSessionAvailable(final int qosCallbackId, final QosSession session, + final NrQosSessionAttributes attributes) { + mQosCallbackTracker.sendEventNrQosSessionAvailable(qosCallbackId, session, attributes); + } + + @Override + public void sendQosSessionLost(final int qosCallbackId, final QosSession session) { + mQosCallbackTracker.sendEventQosSessionLost(qosCallbackId, session); + } + + @Override + public void sendQosCallbackError(final int qosCallbackId, + @QosCallbackException.ExceptionType final int exceptionType) { + mQosCallbackTracker.sendEventQosCallbackError(qosCallbackId, exceptionType); + } + + @Override + public void sendTeardownDelayMs(int teardownDelayMs) { + mHandler.obtainMessage(NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED, + teardownDelayMs, 0, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget(); + } + + @Override + public void sendLingerDuration(final int durationMs) { + mHandler.obtainMessage(NetworkAgent.EVENT_LINGER_DURATION_CHANGED, + new Pair<>(NetworkAgentInfo.this, durationMs)).sendToTarget(); + } + } + + /** + * Inform NetworkAgentInfo that a new NetworkMonitor was created. + */ + public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) { + mNetworkMonitor = new NetworkMonitorManager(networkMonitor); + } + + /** + * Set the NetworkCapabilities on this NetworkAgentInfo. Also attempts to notify NetworkMonitor + * of the new capabilities, if NetworkMonitor has been created. + * + * <p>If {@link NetworkMonitor#notifyNetworkCapabilitiesChanged(NetworkCapabilities)} fails, + * the exception is logged but not reported to callers. + * + * @return the old capabilities of this network. + */ + @NonNull public synchronized NetworkCapabilities getAndSetNetworkCapabilities( + @NonNull final NetworkCapabilities nc) { + final NetworkCapabilities oldNc = networkCapabilities; + networkCapabilities = nc; + mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(), + yieldToBadWiFi()); + final NetworkMonitorManager nm = mNetworkMonitor; + if (nm != null) { + nm.notifyNetworkCapabilitiesChanged(nc); + } + return oldNc; + } + + private boolean yieldToBadWiFi() { + // Only cellular networks yield to bad wifi + return networkCapabilities.hasTransport(TRANSPORT_CELLULAR) && !mConnService.avoidBadWifi(); + } + + public ConnectivityService connService() { + return mConnService; + } + + public NetworkAgentConfig netAgentConfig() { + return networkAgentConfig; + } + + public Handler handler() { + return mHandler; + } + + public Network network() { + return network; + } + + /** + * Get the NetworkMonitorManager in this NetworkAgentInfo. + * + * <p>This will be null before {@link #onNetworkMonitorCreated(INetworkMonitor)} is called. + */ + public NetworkMonitorManager networkMonitor() { + return mNetworkMonitor; + } + + // Functions for manipulating the requests satisfied by this network. + // + // These functions must only called on ConnectivityService's main thread. + + private static final boolean ADD = true; + private static final boolean REMOVE = false; + + private void updateRequestCounts(boolean add, NetworkRequest request) { + int delta = add ? +1 : -1; + switch (request.type) { + case REQUEST: + mNumRequestNetworkRequests += delta; + break; + + case BACKGROUND_REQUEST: + mNumRequestNetworkRequests += delta; + mNumBackgroundNetworkRequests += delta; + break; + + case LISTEN: + case LISTEN_FOR_BEST: + case TRACK_DEFAULT: + case TRACK_SYSTEM_DEFAULT: + break; + + case NONE: + default: + Log.wtf(TAG, "Unhandled request type " + request.type); + break; + } + } + + /** + * Add {@code networkRequest} to this network as it's satisfied by this network. + * @return true if {@code networkRequest} was added or false if {@code networkRequest} was + * already present. + */ + public boolean addRequest(NetworkRequest networkRequest) { + NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId); + if (existing == networkRequest) return false; + if (existing != null) { + // Should only happen if the requestId wraps. If that happens lots of other things will + // be broken as well. + Log.wtf(TAG, String.format("Duplicate requestId for %s and %s on %s", + networkRequest, existing, toShortString())); + updateRequestCounts(REMOVE, existing); + } + mNetworkRequests.put(networkRequest.requestId, networkRequest); + updateRequestCounts(ADD, networkRequest); + return true; + } + + /** + * Remove the specified request from this network. + */ + public void removeRequest(int requestId) { + NetworkRequest existing = mNetworkRequests.get(requestId); + if (existing == null) return; + updateRequestCounts(REMOVE, existing); + mNetworkRequests.remove(requestId); + if (existing.isRequest()) { + unlingerRequest(existing.requestId); + } + } + + /** + * Returns whether this network is currently satisfying the request with the specified ID. + */ + public boolean isSatisfyingRequest(int id) { + return mNetworkRequests.get(id) != null; + } + + /** + * Returns the request at the specified position in the list of requests satisfied by this + * network. + */ + public NetworkRequest requestAt(int index) { + return mNetworkRequests.valueAt(index); + } + + /** + * Returns the number of requests currently satisfied by this network for which + * {@link android.net.NetworkRequest#isRequest} returns {@code true}. + */ + public int numRequestNetworkRequests() { + return mNumRequestNetworkRequests; + } + + /** + * Returns the number of requests currently satisfied by this network of type + * {@link android.net.NetworkRequest.Type.BACKGROUND_REQUEST}. + */ + public int numBackgroundNetworkRequests() { + return mNumBackgroundNetworkRequests; + } + + /** + * Returns the number of foreground requests currently satisfied by this network. + */ + public int numForegroundNetworkRequests() { + return mNumRequestNetworkRequests - mNumBackgroundNetworkRequests; + } + + /** + * Returns the number of requests of any type currently satisfied by this network. + */ + public int numNetworkRequests() { + return mNetworkRequests.size(); + } + + /** + * Returns whether the network is a background network. A network is a background network if it + * does not have the NET_CAPABILITY_FOREGROUND capability, which implies it is satisfying no + * foreground request, is not lingering (i.e. kept for a while after being outscored), and is + * not a speculative network (i.e. kept pending validation when validation would have it + * outscore another foreground network). That implies it is being kept up by some background + * request (otherwise it would be torn down), maybe the mobile always-on request. + */ + public boolean isBackgroundNetwork() { + return !isVPN() && numForegroundNetworkRequests() == 0 && mNumBackgroundNetworkRequests > 0 + && !isLingering(); + } + + // Does this network satisfy request? + public boolean satisfies(NetworkRequest request) { + return created && + request.networkCapabilities.satisfiedByNetworkCapabilities(networkCapabilities); + } + + public boolean satisfiesImmutableCapabilitiesOf(NetworkRequest request) { + return created && + request.networkCapabilities.satisfiedByImmutableNetworkCapabilities( + networkCapabilities); + } + + /** Whether this network is a VPN. */ + public boolean isVPN() { + return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); + } + + /** Whether this network might have underlying networks. Currently only true for VPNs. */ + public boolean supportsUnderlyingNetworks() { + return isVPN(); + } + + // Caller must not mutate. This method is called frequently and making a defensive copy + // would be too expensive. This is used by NetworkRanker.Scoreable, so it can be compared + // against other scoreables. + @Override public NetworkCapabilities getCapsNoCopy() { + return networkCapabilities; + } + + // NetworkRanker.Scoreable + @Override public FullScore getScore() { + return mScore; + } + + // Get the current score for this Network. This may be modified from what the + // NetworkAgent sent, as it has modifiers applied to it. + public int getCurrentScore() { + return mScore.getLegacyInt(); + } + + // Get the current score for this Network as if it was validated. This may be modified from + // what the NetworkAgent sent, as it has modifiers applied to it. + public int getCurrentScoreAsValidated() { + return mScore.getLegacyIntAsValidated(); + } + + /** + * Mix-in the ConnectivityService-managed bits in the score. + */ + public void setScore(final NetworkScore score) { + mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig, + everValidatedForYield(), yieldToBadWiFi()); + } + + /** + * Update the ConnectivityService-managed bits in the score. + * + * Call this after updating the network agent config. + */ + public void updateScoreForNetworkAgentUpdate() { + mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, + everValidatedForYield(), yieldToBadWiFi()); + } + + private boolean everValidatedForYield() { + return everValidated && !avoidUnvalidated; + } + + /** + * Returns a Scoreable identical to this NAI, but validated. + * + * This is useful to probe what scoring would be if this network validated, to know + * whether to provisionally keep a network that may or may not validate. + * + * @return a Scoreable identical to this NAI, but validated. + */ + public NetworkRanker.Scoreable getValidatedScoreable() { + return new NetworkRanker.Scoreable() { + @Override public FullScore getScore() { + return mScore.asValidated(); + } + + @Override public NetworkCapabilities getCapsNoCopy() { + return networkCapabilities; + } + }; + } + + /** + * Return a {@link NetworkStateSnapshot} for this network. + */ + @NonNull + public NetworkStateSnapshot getNetworkStateSnapshot() { + synchronized (this) { + // Network objects are outwardly immutable so there is no point in duplicating. + // Duplicating also precludes sharing socket factories and connection pools. + final String subscriberId = (networkAgentConfig != null) + ? networkAgentConfig.subscriberId : null; + return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities), + new LinkProperties(linkProperties), subscriberId, networkInfo.getType()); + } + } + + /** + * Sets the specified requestId to linger on this network for the specified time. Called by + * ConnectivityService when any request is moved to another network with a higher score, or + * when a network is newly created. + * + * @param requestId The requestId of the request that no longer need to be served by this + * network. Or {@link NetworkRequest.REQUEST_ID_NONE} if this is the + * {@code InactivityTimer} for a newly created network. + */ + // TODO: Consider creating a dedicated function for nascent network, e.g. start/stopNascent. + public void lingerRequest(int requestId, long now, long duration) { + if (mInactivityTimerForRequest.get(requestId) != null) { + // Cannot happen. Once a request is lingering on a particular network, we cannot + // re-linger it unless that network becomes the best for that request again, in which + // case we should have unlingered it. + Log.wtf(TAG, toShortString() + ": request " + requestId + " already lingered"); + } + final long expiryMs = now + duration; + InactivityTimer timer = new InactivityTimer(requestId, expiryMs); + if (VDBG) Log.d(TAG, "Adding InactivityTimer " + timer + " to " + toShortString()); + mInactivityTimers.add(timer); + mInactivityTimerForRequest.put(requestId, timer); + } + + /** + * Sets the specified requestId to linger on this network for the timeout set when + * initializing or modified by {@link #setLingerDuration(int)}. Called by + * ConnectivityService when any request is moved to another network with a higher score. + * + * @param requestId The requestId of the request that no longer need to be served by this + * network. + * @param now current system timestamp obtained by {@code SystemClock.elapsedRealtime}. + */ + public void lingerRequest(int requestId, long now) { + lingerRequest(requestId, now, mLingerDurationMs); + } + + /** + * Cancel lingering. Called by ConnectivityService when a request is added to this network. + * Returns true if the given requestId was lingering on this network, false otherwise. + */ + public boolean unlingerRequest(int requestId) { + InactivityTimer timer = mInactivityTimerForRequest.get(requestId); + if (timer != null) { + if (VDBG) { + Log.d(TAG, "Removing InactivityTimer " + timer + " from " + toShortString()); + } + mInactivityTimers.remove(timer); + mInactivityTimerForRequest.remove(requestId); + return true; + } + return false; + } + + public long getInactivityExpiry() { + return mInactivityExpiryMs; + } + + public void updateInactivityTimer() { + long newExpiry = mInactivityTimers.isEmpty() ? 0 : mInactivityTimers.last().expiryMs; + if (newExpiry == mInactivityExpiryMs) return; + + // Even if we're going to reschedule the timer, cancel it first. This is because the + // semantics of WakeupMessage guarantee that if cancel is called then the alarm will + // never call its callback (handleLingerComplete), even if it has already fired. + // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage + // has already been dispatched, rescheduling to some time in the future won't stop it + // from calling its callback immediately. + if (mInactivityMessage != null) { + mInactivityMessage.cancel(); + mInactivityMessage = null; + } + + if (newExpiry > 0) { + // If the newExpiry timestamp is in the past, the wakeup message will fire immediately. + mInactivityMessage = new WakeupMessage( + mContext, mHandler, + "NETWORK_LINGER_COMPLETE." + network.getNetId() /* cmdName */, + EVENT_NETWORK_LINGER_COMPLETE /* cmd */, + 0 /* arg1 (unused) */, 0 /* arg2 (unused) */, + this /* obj (NetworkAgentInfo) */); + mInactivityMessage.schedule(newExpiry); + } + + mInactivityExpiryMs = newExpiry; + } + + public void setInactive() { + mInactive = true; + } + + public void unsetInactive() { + mInactive = false; + } + + public boolean isInactive() { + return mInactive; + } + + public boolean isLingering() { + return mInactive && !isNascent(); + } + + /** + * Set the linger duration for this NAI. + * @param durationMs The new linger duration, in milliseconds. + */ + public void setLingerDuration(final int durationMs) { + final long diff = durationMs - mLingerDurationMs; + final ArrayList<InactivityTimer> newTimers = new ArrayList<>(); + for (final InactivityTimer timer : mInactivityTimers) { + if (timer.requestId == NetworkRequest.REQUEST_ID_NONE) { + // Don't touch nascent timer, re-add as is. + newTimers.add(timer); + } else { + newTimers.add(new InactivityTimer(timer.requestId, timer.expiryMs + diff)); + } + } + mInactivityTimers.clear(); + mInactivityTimers.addAll(newTimers); + updateInactivityTimer(); + mLingerDurationMs = durationMs; + } + + /** + * Return whether the network satisfies no request, but is still being kept up + * because it has just connected less than + * {@code ConnectivityService#DEFAULT_NASCENT_DELAY_MS}ms ago and is thus still considered + * nascent. Note that nascent mechanism uses inactivity timer which isn't + * associated with a request. Thus, use {@link NetworkRequest#REQUEST_ID_NONE} to identify it. + * + */ + public boolean isNascent() { + return mInactive && mInactivityTimers.size() == 1 + && mInactivityTimers.first().requestId == NetworkRequest.REQUEST_ID_NONE; + } + + public void clearInactivityState() { + if (mInactivityMessage != null) { + mInactivityMessage.cancel(); + mInactivityMessage = null; + } + mInactivityTimers.clear(); + mInactivityTimerForRequest.clear(); + // Sets mInactivityExpiryMs, cancels and nulls out mInactivityMessage. + updateInactivityTimer(); + mInactive = false; + } + + public void dumpInactivityTimers(PrintWriter pw) { + for (InactivityTimer timer : mInactivityTimers) { + pw.println(timer); + } + } + + /** + * Sets the most recent ConnectivityReport for this network. + * + * <p>This should only be called from the ConnectivityService thread. + * + * @hide + */ + public void setConnectivityReport(@NonNull ConnectivityReport connectivityReport) { + mConnectivityReport = connectivityReport; + } + + /** + * Returns the most recent ConnectivityReport for this network, or null if none have been + * reported yet. + * + * <p>This should only be called from the ConnectivityService thread. + * + * @hide + */ + @Nullable + public ConnectivityReport getConnectivityReport() { + return mConnectivityReport; + } + + // TODO: Print shorter members first and only print the boolean variable which value is true + // to improve readability. + public String toString() { + return "NetworkAgentInfo{" + + "network{" + network + "} handle{" + network.getNetworkHandle() + "} ni{" + + networkInfo.toShortString() + "} " + + mScore + " " + + (isNascent() ? " nascent" : (isLingering() ? " lingering" : "")) + + (everValidated ? " everValidated" : "") + + (lastValidated ? " lastValidated" : "") + + (partialConnectivity ? " partialConnectivity" : "") + + (everCaptivePortalDetected ? " everCaptivePortal" : "") + + (lastCaptivePortalDetected ? " isCaptivePortal" : "") + + (networkAgentConfig.explicitlySelected ? " explicitlySelected" : "") + + (networkAgentConfig.acceptUnvalidated ? " acceptUnvalidated" : "") + + (networkAgentConfig.acceptPartialConnectivity ? " acceptPartialConnectivity" : "") + + (clatd.isStarted() ? " clat{" + clatd + "} " : "") + + (declaredUnderlyingNetworks != null + ? " underlying{" + Arrays.toString(declaredUnderlyingNetworks) + "}" : "") + + " lp{" + linkProperties + "}" + + " nc{" + networkCapabilities + "}" + + "}"; + } + + /** + * Show a short string representing a Network. + * + * This is often not enough for debugging purposes for anything complex, but the full form + * is very long and hard to read, so this is useful when there isn't a lot of ambiguity. + * This represents the network with something like "[100 WIFI|VPN]" or "[108 MOBILE]". + */ + public String toShortString() { + return "[" + network.getNetId() + " " + + transportNamesOf(networkCapabilities.getTransportTypes()) + "]"; + } + + // Enables sorting in descending order of score. + @Override + public int compareTo(NetworkAgentInfo other) { + return other.getCurrentScore() - getCurrentScore(); + } + + /** + * Null-guarding version of NetworkAgentInfo#toShortString() + */ + @NonNull + public static String toShortString(@Nullable final NetworkAgentInfo nai) { + return null != nai ? nai.toShortString() : "[null]"; + } +} diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java new file mode 100644 index 0000000000000000000000000000000000000000..2e51be39bfae397eb293a6fb9c56a4df7572de52 --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java @@ -0,0 +1,757 @@ +/* + * 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 com.android.server.connectivity; + +import static android.system.OsConstants.*; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.InetAddresses; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.RouteInfo; +import android.net.TrafficStats; +import android.net.shared.PrivateDnsConfig; +import android.net.util.NetworkConstants; +import android.os.SystemClock; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructTimeval; +import android.text.TextUtils; +import android.util.Pair; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.NetworkStackConstants; + +import libcore.io.IoUtils; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * NetworkDiagnostics + * + * A simple class to diagnose network connectivity fundamentals. Current + * checks performed are: + * - ICMPv4/v6 echo requests for all routers + * - ICMPv4/v6 echo requests for all DNS servers + * - DNS UDP queries to all DNS servers + * + * Currently unimplemented checks include: + * - report ARP/ND data about on-link neighbors + * - DNS TCP queries to all DNS servers + * - HTTP DIRECT and PROXY checks + * - port 443 blocking/TLS intercept checks + * - QUIC reachability checks + * - MTU checks + * + * The supplied timeout bounds the entire diagnostic process. Each specific + * check class must implement this upper bound on measurements in whichever + * manner is most appropriate and effective. + * + * @hide + */ +public class NetworkDiagnostics { + private static final String TAG = "NetworkDiagnostics"; + + private static final InetAddress TEST_DNS4 = InetAddresses.parseNumericAddress("8.8.8.8"); + private static final InetAddress TEST_DNS6 = InetAddresses.parseNumericAddress( + "2001:4860:4860::8888"); + + // For brevity elsewhere. + private static final long now() { + return SystemClock.elapsedRealtime(); + } + + // Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>. + // Should be a member of DnsUdpCheck, but "compiler says no". + public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED }; + + private final Network mNetwork; + private final LinkProperties mLinkProperties; + private final PrivateDnsConfig mPrivateDnsCfg; + private final Integer mInterfaceIndex; + + private final long mTimeoutMs; + private final long mStartTime; + private final long mDeadlineTime; + + // A counter, initialized to the total number of measurements, + // so callers can wait for completion. + private final CountDownLatch mCountDownLatch; + + public class Measurement { + private static final String SUCCEEDED = "SUCCEEDED"; + private static final String FAILED = "FAILED"; + + private boolean succeeded; + + // Package private. TODO: investigate better encapsulation. + String description = ""; + long startTime; + long finishTime; + String result = ""; + Thread thread; + + public boolean checkSucceeded() { return succeeded; } + + void recordSuccess(String msg) { + maybeFixupTimes(); + succeeded = true; + result = SUCCEEDED + ": " + msg; + if (mCountDownLatch != null) { + mCountDownLatch.countDown(); + } + } + + void recordFailure(String msg) { + maybeFixupTimes(); + succeeded = false; + result = FAILED + ": " + msg; + if (mCountDownLatch != null) { + mCountDownLatch.countDown(); + } + } + + private void maybeFixupTimes() { + // Allows the caller to just set success/failure and not worry + // about also setting the correct finishing time. + if (finishTime == 0) { finishTime = now(); } + + // In cases where, for example, a failure has occurred before the + // measurement even began, fixup the start time to reflect as much. + if (startTime == 0) { startTime = finishTime; } + } + + @Override + public String toString() { + return description + ": " + result + " (" + (finishTime - startTime) + "ms)"; + } + } + + private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>(); + private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks = + new HashMap<>(); + private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>(); + private final Map<InetAddress, Measurement> mDnsTlsChecks = new HashMap<>(); + private final String mDescription; + + + public NetworkDiagnostics(Network network, LinkProperties lp, + @NonNull PrivateDnsConfig privateDnsCfg, long timeoutMs) { + mNetwork = network; + mLinkProperties = lp; + mPrivateDnsCfg = privateDnsCfg; + mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName()); + mTimeoutMs = timeoutMs; + mStartTime = now(); + mDeadlineTime = mStartTime + mTimeoutMs; + + // Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity. + // We are free to modify mLinkProperties with impunity because ConnectivityService passes us + // a copy and not the original object. It's easier to do it this way because we don't need + // to check whether the LinkProperties already contains these DNS servers because + // LinkProperties#addDnsServer checks for duplicates. + if (mLinkProperties.isReachable(TEST_DNS4)) { + mLinkProperties.addDnsServer(TEST_DNS4); + } + // TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any + // DNS servers for which isReachable() is false, but since this is diagnostic code, be extra + // careful. + if (mLinkProperties.hasGlobalIpv6Address() || mLinkProperties.hasIpv6DefaultRoute()) { + mLinkProperties.addDnsServer(TEST_DNS6); + } + + for (RouteInfo route : mLinkProperties.getRoutes()) { + if (route.hasGateway()) { + InetAddress gateway = route.getGateway(); + prepareIcmpMeasurement(gateway); + if (route.isIPv6Default()) { + prepareExplicitSourceIcmpMeasurements(gateway); + } + } + } + for (InetAddress nameserver : mLinkProperties.getDnsServers()) { + prepareIcmpMeasurement(nameserver); + prepareDnsMeasurement(nameserver); + + // Unlike the DnsResolver which doesn't do certificate validation in opportunistic mode, + // DoT probes to the DNS servers will fail if certificate validation fails. + prepareDnsTlsMeasurement(null /* hostname */, nameserver); + } + + for (InetAddress tlsNameserver : mPrivateDnsCfg.ips) { + // Reachability check is necessary since when resolving the strict mode hostname, + // NetworkMonitor always queries for both A and AAAA records, even if the network + // is IPv4-only or IPv6-only. + if (mLinkProperties.isReachable(tlsNameserver)) { + // If there are IPs, there must have been a name that resolved to them. + prepareDnsTlsMeasurement(mPrivateDnsCfg.hostname, tlsNameserver); + } + } + + mCountDownLatch = new CountDownLatch(totalMeasurementCount()); + + startMeasurements(); + + mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}" + + " index{" + mInterfaceIndex + "}" + + " network{" + mNetwork + "}" + + " nethandle{" + mNetwork.getNetworkHandle() + "}"; + } + + private static Integer getInterfaceIndex(String ifname) { + try { + NetworkInterface ni = NetworkInterface.getByName(ifname); + return ni.getIndex(); + } catch (NullPointerException | SocketException e) { + return null; + } + } + + private static String socketAddressToString(@NonNull SocketAddress sockAddr) { + // The default toString() implementation is not the prettiest. + InetSocketAddress inetSockAddr = (InetSocketAddress) sockAddr; + InetAddress localAddr = inetSockAddr.getAddress(); + return String.format( + (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"), + localAddr.getHostAddress(), inetSockAddr.getPort()); + } + + private void prepareIcmpMeasurement(InetAddress target) { + if (!mIcmpChecks.containsKey(target)) { + Measurement measurement = new Measurement(); + measurement.thread = new Thread(new IcmpCheck(target, measurement)); + mIcmpChecks.put(target, measurement); + } + } + + private void prepareExplicitSourceIcmpMeasurements(InetAddress target) { + for (LinkAddress l : mLinkProperties.getLinkAddresses()) { + InetAddress source = l.getAddress(); + if (source instanceof Inet6Address && l.isGlobalPreferred()) { + Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target); + if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) { + Measurement measurement = new Measurement(); + measurement.thread = new Thread(new IcmpCheck(source, target, measurement)); + mExplicitSourceIcmpChecks.put(srcTarget, measurement); + } + } + } + } + + private void prepareDnsMeasurement(InetAddress target) { + if (!mDnsUdpChecks.containsKey(target)) { + Measurement measurement = new Measurement(); + measurement.thread = new Thread(new DnsUdpCheck(target, measurement)); + mDnsUdpChecks.put(target, measurement); + } + } + + private void prepareDnsTlsMeasurement(@Nullable String hostname, @NonNull InetAddress target) { + // This might overwrite an existing entry in mDnsTlsChecks, because |target| can be an IP + // address configured by the network as well as an IP address learned by resolving the + // strict mode DNS hostname. If the entry is overwritten, the overwritten measurement + // thread will not execute. + Measurement measurement = new Measurement(); + measurement.thread = new Thread(new DnsTlsCheck(hostname, target, measurement)); + mDnsTlsChecks.put(target, measurement); + } + + private int totalMeasurementCount() { + return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size() + + mDnsTlsChecks.size(); + } + + private void startMeasurements() { + for (Measurement measurement : mIcmpChecks.values()) { + measurement.thread.start(); + } + for (Measurement measurement : mExplicitSourceIcmpChecks.values()) { + measurement.thread.start(); + } + for (Measurement measurement : mDnsUdpChecks.values()) { + measurement.thread.start(); + } + for (Measurement measurement : mDnsTlsChecks.values()) { + measurement.thread.start(); + } + } + + public void waitForMeasurements() { + try { + mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) {} + } + + public List<Measurement> getMeasurements() { + // TODO: Consider moving waitForMeasurements() in here to minimize the + // chance of caller errors. + + ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount()); + + // Sort measurements IPv4 first. + for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry : + mExplicitSourceIcmpChecks.entrySet()) { + if (entry.getKey().first instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + + // IPv6 measurements second. + for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry : + mExplicitSourceIcmpChecks.entrySet()) { + if (entry.getKey().first instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + + return measurements; + } + + public void dump(IndentingPrintWriter pw) { + pw.println(TAG + ":" + mDescription); + final long unfinished = mCountDownLatch.getCount(); + if (unfinished > 0) { + // This can't happen unless a caller forgets to call waitForMeasurements() + // or a measurement isn't implemented to correctly honor the timeout. + pw.println("WARNING: countdown wait incomplete: " + + unfinished + " unfinished measurements"); + } + + pw.increaseIndent(); + + String prefix; + for (Measurement m : getMeasurements()) { + prefix = m.checkSucceeded() ? "." : "F"; + pw.println(prefix + " " + m.toString()); + } + + pw.decreaseIndent(); + } + + + private class SimpleSocketCheck implements Closeable { + protected final InetAddress mSource; // Usually null. + protected final InetAddress mTarget; + protected final int mAddressFamily; + protected final Measurement mMeasurement; + protected FileDescriptor mFileDescriptor; + protected SocketAddress mSocketAddress; + + protected SimpleSocketCheck( + InetAddress source, InetAddress target, Measurement measurement) { + mMeasurement = measurement; + + if (target instanceof Inet6Address) { + Inet6Address targetWithScopeId = null; + if (target.isLinkLocalAddress() && mInterfaceIndex != null) { + try { + targetWithScopeId = Inet6Address.getByAddress( + null, target.getAddress(), mInterfaceIndex); + } catch (UnknownHostException e) { + mMeasurement.recordFailure(e.toString()); + } + } + mTarget = (targetWithScopeId != null) ? targetWithScopeId : target; + mAddressFamily = AF_INET6; + } else { + mTarget = target; + mAddressFamily = AF_INET; + } + + // We don't need to check the scope ID here because we currently only do explicit-source + // measurements from global IPv6 addresses. + mSource = source; + } + + protected SimpleSocketCheck(InetAddress target, Measurement measurement) { + this(null, target, measurement); + } + + protected void setupSocket( + int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort) + throws ErrnoException, IOException { + final int oldTag = TrafficStats.getAndSetThreadStatsTag( + NetworkStackConstants.TAG_SYSTEM_PROBE); + try { + mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol); + } finally { + // TODO: The tag should remain set until all traffic is sent and received. + // Consider tagging the socket after the measurement thread is started. + TrafficStats.setThreadStatsTag(oldTag); + } + // Setting SNDTIMEO is purely for defensive purposes. + Os.setsockoptTimeval(mFileDescriptor, + SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout)); + Os.setsockoptTimeval(mFileDescriptor, + SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout)); + // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability. + mNetwork.bindSocket(mFileDescriptor); + if (mSource != null) { + Os.bind(mFileDescriptor, mSource, 0); + } + Os.connect(mFileDescriptor, mTarget, dstPort); + mSocketAddress = Os.getsockname(mFileDescriptor); + } + + protected boolean ensureMeasurementNecessary() { + if (mMeasurement.finishTime == 0) return false; + + // Countdown latch was not decremented when the measurement failed during setup. + mCountDownLatch.countDown(); + return true; + } + + @Override + public void close() { + IoUtils.closeQuietly(mFileDescriptor); + } + } + + + private class IcmpCheck extends SimpleSocketCheck implements Runnable { + private static final int TIMEOUT_SEND = 100; + private static final int TIMEOUT_RECV = 300; + private static final int PACKET_BUFSIZE = 512; + private final int mProtocol; + private final int mIcmpType; + + public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) { + super(source, target, measurement); + + if (mAddressFamily == AF_INET6) { + mProtocol = IPPROTO_ICMPV6; + mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE; + mMeasurement.description = "ICMPv6"; + } else { + mProtocol = IPPROTO_ICMP; + mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE; + mMeasurement.description = "ICMPv4"; + } + + mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}"; + } + + public IcmpCheck(InetAddress target, Measurement measurement) { + this(null, target, measurement); + } + + @Override + public void run() { + if (ensureMeasurementNecessary()) return; + + try { + setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0); + } catch (ErrnoException | IOException e) { + mMeasurement.recordFailure(e.toString()); + return; + } + mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}"; + + // Build a trivial ICMP packet. + final byte[] icmpPacket = { + (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header + }; + + int count = 0; + mMeasurement.startTime = now(); + while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) { + count++; + icmpPacket[icmpPacket.length - 1] = (byte) count; + try { + Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length); + } catch (ErrnoException | InterruptedIOException e) { + mMeasurement.recordFailure(e.toString()); + break; + } + + try { + ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); + Os.read(mFileDescriptor, reply); + // TODO: send a few pings back to back to guesstimate packet loss. + mMeasurement.recordSuccess("1/" + count); + break; + } catch (ErrnoException | InterruptedIOException e) { + continue; + } + } + if (mMeasurement.finishTime == 0) { + mMeasurement.recordFailure("0/" + count); + } + + close(); + } + } + + + private class DnsUdpCheck extends SimpleSocketCheck implements Runnable { + private static final int TIMEOUT_SEND = 100; + private static final int TIMEOUT_RECV = 500; + private static final int RR_TYPE_A = 1; + private static final int RR_TYPE_AAAA = 28; + private static final int PACKET_BUFSIZE = 512; + + protected final Random mRandom = new Random(); + + // Should be static, but the compiler mocks our puny, human attempts at reason. + protected String responseCodeStr(int rcode) { + try { + return DnsResponseCode.values()[rcode].toString(); + } catch (IndexOutOfBoundsException e) { + return String.valueOf(rcode); + } + } + + protected final int mQueryType; + + public DnsUdpCheck(InetAddress target, Measurement measurement) { + super(target, measurement); + + // TODO: Ideally, query the target for both types regardless of address family. + if (mAddressFamily == AF_INET6) { + mQueryType = RR_TYPE_AAAA; + } else { + mQueryType = RR_TYPE_A; + } + + mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}"; + } + + @Override + public void run() { + if (ensureMeasurementNecessary()) return; + + try { + setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV, + NetworkConstants.DNS_SERVER_PORT); + } catch (ErrnoException | IOException e) { + mMeasurement.recordFailure(e.toString()); + return; + } + + // This needs to be fixed length so it can be dropped into the pre-canned packet. + final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000); + appendDnsToMeasurementDescription(sixRandomDigits, mSocketAddress); + + // Build a trivial DNS packet. + final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits); + + int count = 0; + mMeasurement.startTime = now(); + while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) { + count++; + try { + Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length); + } catch (ErrnoException | InterruptedIOException e) { + mMeasurement.recordFailure(e.toString()); + break; + } + + try { + ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE); + Os.read(mFileDescriptor, reply); + // TODO: more correct and detailed evaluation of the response, + // possibly adding the returned IP address(es) to the output. + final String rcodeStr = (reply.limit() > 3) + ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f) + : ""; + mMeasurement.recordSuccess("1/" + count + rcodeStr); + break; + } catch (ErrnoException | InterruptedIOException e) { + continue; + } + } + if (mMeasurement.finishTime == 0) { + mMeasurement.recordFailure("0/" + count); + } + + close(); + } + + protected byte[] getDnsQueryPacket(String sixRandomDigits) { + byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII); + return new byte[] { + (byte) mRandom.nextInt(), (byte) mRandom.nextInt(), // [0-1] query ID + 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD). + 0, 1, // [4-5] QDCOUNT (number of queries) + 0, 0, // [6-7] ANCOUNT (number of answers) + 0, 0, // [8-9] NSCOUNT (number of name server records) + 0, 0, // [10-11] ARCOUNT (number of additional records) + 17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5], + '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's', + 6, 'm', 'e', 't', 'r', 'i', 'c', + 7, 'g', 's', 't', 'a', 't', 'i', 'c', + 3, 'c', 'o', 'm', + 0, // null terminator of FQDN (root TLD) + 0, (byte) mQueryType, // QTYPE + 0, 1 // QCLASS, set to 1 = IN (Internet) + }; + } + + protected void appendDnsToMeasurementDescription( + String sixRandomDigits, SocketAddress sockAddr) { + mMeasurement.description += " src{" + socketAddressToString(sockAddr) + "}" + + " qtype{" + mQueryType + "}" + + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}"; + } + } + + // TODO: Have it inherited from SimpleSocketCheck, and separate common DNS helpers out of + // DnsUdpCheck. + private class DnsTlsCheck extends DnsUdpCheck { + private static final int TCP_CONNECT_TIMEOUT_MS = 2500; + private static final int TCP_TIMEOUT_MS = 2000; + private static final int DNS_TLS_PORT = 853; + private static final int DNS_HEADER_SIZE = 12; + + private final String mHostname; + + public DnsTlsCheck(@Nullable String hostname, @NonNull InetAddress target, + @NonNull Measurement measurement) { + super(target, measurement); + + mHostname = hostname; + mMeasurement.description = "DNS TLS dst{" + mTarget.getHostAddress() + "} hostname{" + + (mHostname == null ? "" : mHostname) + "}"; + } + + private SSLSocket setupSSLSocket() throws IOException { + // A TrustManager will be created and initialized with a KeyStore containing system + // CaCerts. During SSL handshake, it will be used to validate the certificates from + // the server. + SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket(); + sslSocket.setSoTimeout(TCP_TIMEOUT_MS); + + if (!TextUtils.isEmpty(mHostname)) { + // Set SNI. + final List<SNIServerName> names = + Collections.singletonList(new SNIHostName(mHostname)); + SSLParameters params = sslSocket.getSSLParameters(); + params.setServerNames(names); + sslSocket.setSSLParameters(params); + } + + mNetwork.bindSocket(sslSocket); + return sslSocket; + } + + private void sendDoTProbe(@Nullable SSLSocket sslSocket) throws IOException { + final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000); + final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits); + + mMeasurement.startTime = now(); + sslSocket.connect(new InetSocketAddress(mTarget, DNS_TLS_PORT), TCP_CONNECT_TIMEOUT_MS); + + // Synchronous call waiting for the TLS handshake complete. + sslSocket.startHandshake(); + appendDnsToMeasurementDescription(sixRandomDigits, sslSocket.getLocalSocketAddress()); + + final DataOutputStream output = new DataOutputStream(sslSocket.getOutputStream()); + output.writeShort(dnsPacket.length); + output.write(dnsPacket, 0, dnsPacket.length); + + final DataInputStream input = new DataInputStream(sslSocket.getInputStream()); + final int replyLength = Short.toUnsignedInt(input.readShort()); + final byte[] reply = new byte[replyLength]; + int bytesRead = 0; + while (bytesRead < replyLength) { + bytesRead += input.read(reply, bytesRead, replyLength - bytesRead); + } + + if (bytesRead > DNS_HEADER_SIZE && bytesRead == replyLength) { + mMeasurement.recordSuccess("1/1 " + responseCodeStr((int) (reply[3]) & 0x0f)); + } else { + mMeasurement.recordFailure("1/1 Read " + bytesRead + " bytes while expected to be " + + replyLength + " bytes"); + } + } + + @Override + public void run() { + if (ensureMeasurementNecessary()) return; + + // No need to restore the tag, since this thread is only used for this measurement. + TrafficStats.getAndSetThreadStatsTag(NetworkStackConstants.TAG_SYSTEM_PROBE); + + try (SSLSocket sslSocket = setupSSLSocket()) { + sendDoTProbe(sslSocket); + } catch (IOException e) { + mMeasurement.recordFailure(e.toString()); + } + } + } +} diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3dc79c51d8a95a47b8d95164a6ae18706146a905 --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java @@ -0,0 +1,404 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.net.ConnectivityResources; +import android.net.NetworkSpecifier; +import android.net.TelephonyNetworkSpecifier; +import android.net.wifi.WifiInfo; +import android.os.UserHandle; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.widget.Toast; + +import com.android.connectivity.resources.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; + +public class NetworkNotificationManager { + + + public static enum NotificationType { + LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET), + NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH), + NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET), + PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY), + SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN), + PRIVATE_DNS_BROKEN(SystemMessage.NOTE_NETWORK_PRIVATE_DNS_BROKEN); + + public final int eventId; + + NotificationType(int eventId) { + this.eventId = eventId; + Holder.sIdToTypeMap.put(eventId, this); + } + + private static class Holder { + private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>(); + } + + public static NotificationType getFromId(int id) { + return Holder.sIdToTypeMap.get(id); + } + }; + + private static final String TAG = NetworkNotificationManager.class.getSimpleName(); + private static final boolean DBG = true; + + // Notification channels used by ConnectivityService mainline module, it should be aligned with + // SystemNotificationChannels so the channels are the same as the ones used as the system + // server. + public static final String NOTIFICATION_CHANNEL_NETWORK_STATUS = "NETWORK_STATUS"; + public static final String NOTIFICATION_CHANNEL_NETWORK_ALERTS = "NETWORK_ALERTS"; + + // The context is for the current user (system server) + private final Context mContext; + private final ConnectivityResources mResources; + private final TelephonyManager mTelephonyManager; + // The notification manager is created from a context for User.ALL, so notifications + // will be sent to all users. + private final NotificationManager mNotificationManager; + // Tracks the types of notifications managed by this instance, from creation to cancellation. + private final SparseIntArray mNotificationTypeMap; + + public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) { + mContext = c; + mTelephonyManager = t; + mNotificationManager = + (NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */) + .getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationTypeMap = new SparseIntArray(); + mResources = new ConnectivityResources(mContext); + } + + @VisibleForTesting + protected static int approximateTransportType(NetworkAgentInfo nai) { + return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai); + } + + // TODO: deal more gracefully with multi-transport networks. + private static int getFirstTransportType(NetworkAgentInfo nai) { + // TODO: The range is wrong, the safer and correct way is to change the range from + // MIN_TRANSPORT to MAX_TRANSPORT. + for (int i = 0; i < 64; i++) { + if (nai.networkCapabilities.hasTransport(i)) return i; + } + return -1; + } + + private String getTransportName(final int transportType) { + String[] networkTypes = mResources.get().getStringArray(R.array.network_switch_type_name); + try { + return networkTypes[transportType]; + } catch (IndexOutOfBoundsException e) { + return mResources.get().getString(R.string.network_switch_type_name_unknown); + } + } + + private static int getIcon(int transportType) { + return (transportType == TRANSPORT_WIFI) + ? R.drawable.stat_notify_wifi_in_range // TODO: Distinguish ! from ?. + : R.drawable.stat_notify_rssi_in_range; + } + + /** + * Show or hide network provisioning notifications. + * + * We use notifications for two purposes: to notify that a network requires sign in + * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access + * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a + * particular network we can display the notification type that was most recently requested. + * So for example if a captive portal fails to reply within a few seconds of connecting, we + * might first display NO_INTERNET, and then when the captive portal check completes, display + * SIGN_IN. + * + * @param id an identifier that uniquely identifies this notification. This must match + * between show and hide calls. We use the NetID value but for legacy callers + * we concatenate the range of types with the range of NetIDs. + * @param notifyType the type of the notification. + * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET, + * or LOST_INTERNET notification, this is the network we're connecting to. For a + * NETWORK_SWITCH notification it's the network that we switched from. When this network + * disconnects the notification is removed. + * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null + * in all other cases. Only used to determine the text of the notification. + */ + public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai, + NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) { + final String tag = tagFor(id); + final int eventId = notifyType.eventId; + final int transportType; + final CharSequence name; + if (nai != null) { + transportType = approximateTransportType(nai); + final String extraInfo = nai.networkInfo.getExtraInfo(); + if (nai.linkProperties != null && nai.linkProperties.getCaptivePortalData() != null + && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData() + .getVenueFriendlyName())) { + name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName(); + } else { + name = TextUtils.isEmpty(extraInfo) + ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo; + } + // Only notify for Internet-capable networks. + if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return; + } else { + // Legacy notifications. + transportType = TRANSPORT_CELLULAR; + name = ""; + } + + // Clear any previous notification with lower priority, otherwise return. http://b/63676954. + // A new SIGN_IN notification with a new intent should override any existing one. + final int previousEventId = mNotificationTypeMap.get(id); + final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId); + if (priority(previousNotifyType) > priority(notifyType)) { + Log.d(TAG, String.format( + "ignoring notification %s for network %s with existing notification %s", + notifyType, id, previousNotifyType)); + return; + } + clearNotification(id); + + if (DBG) { + Log.d(TAG, String.format( + "showNotification tag=%s event=%s transport=%s name=%s highPriority=%s", + tag, nameOf(eventId), getTransportName(transportType), name, highPriority)); + } + + final Resources r = mResources.get(); + final CharSequence title; + final CharSequence details; + Icon icon = Icon.createWithResource( + mResources.getResourcesContext(), getIcon(transportType)); + if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) { + title = r.getString(R.string.wifi_no_internet, name); + details = r.getString(R.string.wifi_no_internet_detailed); + } else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) { + if (transportType == TRANSPORT_CELLULAR) { + title = r.getString(R.string.mobile_no_internet); + } else if (transportType == TRANSPORT_WIFI) { + title = r.getString(R.string.wifi_no_internet, name); + } else { + title = r.getString(R.string.other_networks_no_internet); + } + details = r.getString(R.string.private_dns_broken_detailed); + } else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY + && transportType == TRANSPORT_WIFI) { + title = r.getString(R.string.network_partial_connectivity, name); + details = r.getString(R.string.network_partial_connectivity_detailed); + } else if (notifyType == NotificationType.LOST_INTERNET && + transportType == TRANSPORT_WIFI) { + title = r.getString(R.string.wifi_no_internet, name); + details = r.getString(R.string.wifi_no_internet_detailed); + } else if (notifyType == NotificationType.SIGN_IN) { + switch (transportType) { + case TRANSPORT_WIFI: + title = r.getString(R.string.wifi_available_sign_in, 0); + details = r.getString(R.string.network_available_sign_in_detailed, name); + break; + case TRANSPORT_CELLULAR: + title = r.getString(R.string.network_available_sign_in, 0); + // TODO: Change this to pull from NetworkInfo once a printable + // name has been added to it + NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier(); + int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; + if (specifier instanceof TelephonyNetworkSpecifier) { + subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId(); + } + + details = mTelephonyManager.createForSubscriptionId(subId) + .getNetworkOperatorName(); + break; + default: + title = r.getString(R.string.network_available_sign_in, 0); + details = r.getString(R.string.network_available_sign_in_detailed, name); + break; + } + } else if (notifyType == NotificationType.NETWORK_SWITCH) { + String fromTransport = getTransportName(transportType); + String toTransport = getTransportName(approximateTransportType(switchToNai)); + title = r.getString(R.string.network_switch_metered, toTransport); + details = r.getString(R.string.network_switch_metered_detail, toTransport, + fromTransport); + } else if (notifyType == NotificationType.NO_INTERNET + || notifyType == NotificationType.PARTIAL_CONNECTIVITY) { + // NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks + // are sent, but they are not implemented yet. + return; + } else { + Log.wtf(TAG, "Unknown notification type " + notifyType + " on network transport " + + getTransportName(transportType)); + return; + } + // When replacing an existing notification for a given network, don't alert, just silently + // update the existing notification. Note that setOnlyAlertOnce() will only work for the + // same id, and the id used here is the NotificationType which is different in every type of + // notification. This is required because the notification metrics only track the ID but not + // the tag. + final boolean hasPreviousNotification = previousNotifyType != null; + final String channelId = (highPriority && !hasPreviousNotification) + ? NOTIFICATION_CHANNEL_NETWORK_ALERTS : NOTIFICATION_CHANNEL_NETWORK_STATUS; + Notification.Builder builder = new Notification.Builder(mContext, channelId) + .setWhen(System.currentTimeMillis()) + .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH) + .setSmallIcon(icon) + .setAutoCancel(true) + .setTicker(title) + .setColor(mContext.getColor(android.R.color.system_notification_accent_color)) + .setContentTitle(title) + .setContentIntent(intent) + .setLocalOnly(true) + .setOnlyAlertOnce(true) + // TODO: consider having action buttons to disconnect on the sign-in notification + // especially if it is ongoing + .setOngoing(notifyType == NotificationType.SIGN_IN + && r.getBoolean(R.bool.config_ongoingSignInNotification)); + + if (notifyType == NotificationType.NETWORK_SWITCH) { + builder.setStyle(new Notification.BigTextStyle().bigText(details)); + } else { + builder.setContentText(details); + } + + if (notifyType == NotificationType.SIGN_IN) { + builder.extend(new Notification.TvExtender().setChannelId(channelId)); + } + + Notification notification = builder.build(); + + mNotificationTypeMap.put(id, eventId); + try { + mNotificationManager.notify(tag, eventId, notification); + } catch (NullPointerException npe) { + Log.d(TAG, "setNotificationVisible: visible notificationManager error", npe); + } + } + + /** + * Clear the notification with the given id, only if it matches the given type. + */ + public void clearNotification(int id, NotificationType notifyType) { + final int previousEventId = mNotificationTypeMap.get(id); + final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId); + if (notifyType != previousNotifyType) { + return; + } + clearNotification(id); + } + + public void clearNotification(int id) { + if (mNotificationTypeMap.indexOfKey(id) < 0) { + return; + } + final String tag = tagFor(id); + final int eventId = mNotificationTypeMap.get(id); + if (DBG) { + Log.d(TAG, String.format("clearing notification tag=%s event=%s", tag, + nameOf(eventId))); + } + try { + mNotificationManager.cancel(tag, eventId); + } catch (NullPointerException npe) { + Log.d(TAG, String.format( + "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe); + } + mNotificationTypeMap.delete(id); + } + + /** + * Legacy provisioning notifications coming directly from DcTracker. + */ + public void setProvNotificationVisible(boolean visible, int id, String action) { + if (visible) { + // For legacy purposes, action is sent as the action + the phone ID from DcTracker. + // Split the string here and send the phone ID as an extra instead. + String[] splitAction = action.split(":"); + Intent intent = new Intent(splitAction[0]); + try { + intent.putExtra("provision.phone.id", Integer.parseInt(splitAction[1])); + } catch (NumberFormatException ignored) { } + PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE); + showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false); + } else { + clearNotification(id); + } + } + + public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) { + String fromTransport = getTransportName(approximateTransportType(fromNai)); + String toTransport = getTransportName(approximateTransportType(toNai)); + String text = mResources.get().getString( + R.string.network_switch_metered_toast, fromTransport, toTransport); + Toast.makeText(mContext, text, Toast.LENGTH_LONG).show(); + } + + @VisibleForTesting + static String tagFor(int id) { + return String.format("ConnectivityNotification:%d", id); + } + + @VisibleForTesting + static String nameOf(int eventId) { + NotificationType t = NotificationType.getFromId(eventId); + return (t != null) ? t.name() : "UNKNOWN"; + } + + /** + * A notification with a higher number will take priority over a notification with a lower + * number. + */ + private static int priority(NotificationType t) { + if (t == null) { + return 0; + } + switch (t) { + case SIGN_IN: + return 6; + case PARTIAL_CONNECTIVITY: + return 5; + case PRIVATE_DNS_BROKEN: + return 4; + case NO_INTERNET: + return 3; + case NETWORK_SWITCH: + return 2; + case LOST_INTERNET: + return 1; + default: + return 0; + } + } +} diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java new file mode 100644 index 0000000000000000000000000000000000000000..8285e7a8a3cb99ca299d8bff8453e45155418b10 --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkOffer.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.annotation.NonNull; +import android.net.INetworkOfferCallback; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.RemoteException; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents an offer made by a NetworkProvider to create a network if a need arises. + * + * This class contains the prospective score and capabilities of the network. The provider + * is not obligated to caps able to create a network satisfying this, nor to build a network + * with the exact score and/or capabilities passed ; after all, not all providers know in + * advance what a network will look like after it's connected. Instead, this is meant as a + * filter to limit requests sent to the provider by connectivity to those that this offer stands + * a chance to fulfill. + * + * @see NetworkProvider#offerNetwork. + * + * @hide + */ +public class NetworkOffer implements NetworkRanker.Scoreable { + @NonNull public final FullScore score; + @NonNull public final NetworkCapabilities caps; + @NonNull public final INetworkOfferCallback callback; + @NonNull public final int providerId; + // While this could, in principle, be deduced from the old values of the satisfying networks, + // doing so would add a lot of complexity and performance penalties. For each request, the + // ranker would have to run again to figure out if this offer used to be able to beat the + // previous satisfier to know if there is a change in whether this offer is now needed ; + // besides, there would be a need to handle an edge case when a new request comes online, + // where it's not satisfied before the first rematch, where starting to satisfy a request + // should not result in sending unneeded to this offer. This boolean, while requiring that + // the offers are only ever manipulated on the CS thread, is by far a simpler and + // economical solution. + private final Set<NetworkRequest> mCurrentlyNeeded = new HashSet<>(); + + public NetworkOffer(@NonNull final FullScore score, + @NonNull final NetworkCapabilities caps, + @NonNull final INetworkOfferCallback callback, + @NonNull final int providerId) { + this.score = Objects.requireNonNull(score); + this.caps = Objects.requireNonNull(caps); + this.callback = Objects.requireNonNull(callback); + this.providerId = providerId; + } + + /** + * Get the score filter of this offer + */ + @Override @NonNull public FullScore getScore() { + return score; + } + + /** + * Get the capabilities filter of this offer + */ + @Override @NonNull public NetworkCapabilities getCapsNoCopy() { + return caps; + } + + /** + * Tell the provider for this offer that the network is needed for a request. + * @param request the request for which the offer is needed + */ + public void onNetworkNeeded(@NonNull final NetworkRequest request) { + if (mCurrentlyNeeded.contains(request)) { + throw new IllegalStateException("Network already needed"); + } + mCurrentlyNeeded.add(request); + try { + callback.onNetworkNeeded(request); + } catch (final RemoteException e) { + // The provider is dead. It will be removed by the death recipient. + } + } + + /** + * Tell the provider for this offer that the network is no longer needed for this request. + * + * onNetworkNeeded will have been called with the same request before. + * + * @param request the request + */ + public void onNetworkUnneeded(@NonNull final NetworkRequest request) { + if (!mCurrentlyNeeded.contains(request)) { + throw new IllegalStateException("Network already unneeded"); + } + mCurrentlyNeeded.remove(request); + try { + callback.onNetworkUnneeded(request); + } catch (final RemoteException e) { + // The provider is dead. It will be removed by the death recipient. + } + } + + /** + * Returns whether this offer is currently needed for this request. + * @param request the request + * @return whether the offer is currently considered needed + */ + public boolean neededFor(@NonNull final NetworkRequest request) { + return mCurrentlyNeeded.contains(request); + } + + /** + * Migrate from, and take over, a previous offer. + * + * When an updated offer is sent from a provider, call this method on the new offer, passing + * the old one, to take over the state. + * + * @param previousOffer the previous offer + */ + public void migrateFrom(@NonNull final NetworkOffer previousOffer) { + if (!callback.asBinder().equals(previousOffer.callback.asBinder())) { + throw new IllegalArgumentException("Can only migrate from a previous version of" + + " the same offer"); + } + mCurrentlyNeeded.clear(); + mCurrentlyNeeded.addAll(previousOffer.mCurrentlyNeeded); + } + + @Override + public String toString() { + return "NetworkOffer [ Score " + score + " ]"; + } +} diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java new file mode 100644 index 0000000000000000000000000000000000000000..d7eb9c8a59b855afb16f2df693d6c4fac9f3715a --- /dev/null +++ b/service/src/com/android/server/connectivity/NetworkRanker.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkScore.POLICY_EXITING; +import static android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY; +import static android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI; + +import static com.android.net.module.util.CollectionUtils.filter; +import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED; +import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED; +import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD; +import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE; +import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED; +import static com.android.server.connectivity.FullScore.POLICY_IS_VPN; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; + +import com.android.net.module.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +/** + * A class that knows how to find the best network matching a request out of a list of networks. + */ +public class NetworkRanker { + // Historically the legacy ints have been 0~100 in principle (though the highest score in + // AOSP has always been 90). This is relied on by VPNs that send a legacy score of 101. + public static final int LEGACY_INT_MAX = 100; + + /** + * A class that can be scored against other scoreables. + */ + public interface Scoreable { + /** Get score of this scoreable */ + FullScore getScore(); + /** Get capabilities of this scoreable */ + NetworkCapabilities getCapsNoCopy(); + } + + private static final boolean USE_POLICY_RANKING = true; + + public NetworkRanker() { } + + /** + * Find the best network satisfying this request among the list of passed networks. + */ + @Nullable + public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request, + @NonNull final Collection<NetworkAgentInfo> nais, + @Nullable final NetworkAgentInfo currentSatisfier) { + final ArrayList<NetworkAgentInfo> candidates = filter(nais, nai -> nai.satisfies(request)); + if (candidates.size() == 1) return candidates.get(0); // Only one potential satisfier + if (candidates.size() <= 0) return null; // No network can satisfy this request + if (USE_POLICY_RANKING) { + return getBestNetworkByPolicy(candidates, currentSatisfier); + } else { + return getBestNetworkByLegacyInt(candidates); + } + } + + // Transport preference order, if it comes down to that. + private static final int[] PREFERRED_TRANSPORTS_ORDER = { TRANSPORT_ETHERNET, TRANSPORT_WIFI, + TRANSPORT_BLUETOOTH, TRANSPORT_CELLULAR }; + + // Function used to partition a list into two working areas depending on whether they + // satisfy a predicate. All items satisfying the predicate will be put in |positive|, all + // items that don't will be put in |negative|. + // This is useful in this file because many of the ranking checks will retain only networks that + // satisfy a predicate if any of them do, but keep them all if all of them do. Having working + // areas is uncustomary in Java, but this function is called in a fairly intensive manner + // and doing allocation quite that often might affect performance quite badly. + private static <T> void partitionInto(@NonNull final List<T> source, @NonNull Predicate<T> test, + @NonNull final List<T> positive, @NonNull final List<T> negative) { + positive.clear(); + negative.clear(); + for (final T item : source) { + if (test.test(item)) { + positive.add(item); + } else { + negative.add(item); + } + } + } + + private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) { + return candidate.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD) + && candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI); + } + + /** + * Apply the "yield to bad WiFi" policy. + * + * This function must run immediately after the validation policy. + * + * If any of the accepted networks has the "yield to bad WiFi" policy AND there are some + * bad WiFis in the rejected list, then move the networks with the policy to the rejected + * list. If this leaves no accepted network, then move the bad WiFis back to the accepted list. + * + * This function returns nothing, but will have updated accepted and rejected in-place. + * + * @param accepted networks accepted by the validation policy + * @param rejected networks rejected by the validation policy + */ + private <T extends Scoreable> void applyYieldToBadWifiPolicy(@NonNull ArrayList<T> accepted, + @NonNull ArrayList<T> rejected) { + if (!CollectionUtils.any(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) { + // No network with the policy : do nothing. + return; + } + if (!CollectionUtils.any(rejected, n -> isBadWiFi(n))) { + // No bad WiFi : do nothing. + return; + } + if (CollectionUtils.all(accepted, n -> n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI))) { + // All validated networks yield to bad WiFis : keep bad WiFis alongside with the + // yielders. This is important because the yielders need to be compared to the bad + // wifis by the following policies (e.g. exiting). + final ArrayList<T> acceptedYielders = new ArrayList<>(accepted); + final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected); + partitionInto(rejectedWithBadWiFis, n -> isBadWiFi(n), accepted, rejected); + accepted.addAll(acceptedYielders); + return; + } + // Only some of the validated networks yield to bad WiFi : keep only the ones who don't. + final ArrayList<T> acceptedWithYielders = new ArrayList<>(accepted); + partitionInto(acceptedWithYielders, n -> !n.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI), + accepted, rejected); + } + + /** + * Get the best network among a list of candidates according to policy. + * @param candidates the candidates + * @param currentSatisfier the current satisfier, or null if none + * @return the best network + */ + @Nullable public <T extends Scoreable> T getBestNetworkByPolicy( + @NonNull List<T> candidates, + @Nullable final T currentSatisfier) { + // Used as working areas. + final ArrayList<T> accepted = + new ArrayList<>(candidates.size() /* initialCapacity */); + final ArrayList<T> rejected = + new ArrayList<>(candidates.size() /* initialCapacity */); + + // The following tests will search for a network matching a given criterion. They all + // function the same way : if any network matches the criterion, drop from consideration + // all networks that don't. To achieve this, the tests below : + // 1. partition the list of remaining candidates into accepted and rejected networks. + // 2. if only one candidate remains, that's the winner : if accepted.size == 1 return [0] + // 3. if multiple remain, keep only the accepted networks and go on to the next criterion. + // Because the working areas will be wiped, a copy of the accepted networks needs to be + // made. + // 4. if none remain, the criterion did not help discriminate so keep them all. As an + // optimization, skip creating a new array and go on to the next criterion. + + // If a network is invincible, use it. + partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_INVINCIBLE), + accepted, rejected); + if (accepted.size() == 1) return accepted.get(0); + if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); + + // If there is a connected VPN, use it. + partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VPN), + accepted, rejected); + if (accepted.size() == 1) return accepted.get(0); + if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); + + // Selected & Accept-unvalidated policy : if any network has both of these, then don't + // choose one that doesn't. + partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED) + && nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED), + accepted, rejected); + if (accepted.size() == 1) return accepted.get(0); + if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); + + // If any network is validated (or should be accepted even if it's not validated), then + // don't choose one that isn't. + partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_VALIDATED) + || nai.getScore().hasPolicy(POLICY_ACCEPT_UNVALIDATED), + accepted, rejected); + // Yield to bad wifi policy : if any network has the "yield to bad WiFi" policy and + // there are bad WiFis connected, then accept the bad WiFis and reject the networks with + // the policy. + applyYieldToBadWifiPolicy(accepted, rejected); + if (accepted.size() == 1) return accepted.get(0); + if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); + + // If any network is not exiting, don't choose one that is. + partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_EXITING), + accepted, rejected); + if (accepted.size() == 1) return accepted.get(0); + if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); + + // TODO : If any network is unmetered, don't choose a metered network. + // This can't be implemented immediately because prospective networks are always + // considered unmetered because factories don't know if the network will be metered. + // Saying an unmetered network always beats a metered one would mean that when metered wifi + // is connected, the offer for telephony would beat WiFi but the actual metered network + // would lose, so we'd have an infinite loop where telephony would continually bring up + // a network that is immediately torn down. + // Fix this by getting the agent to tell connectivity whether the network they will + // bring up is metered. Cell knows that in advance, while WiFi has a good estimate and + // can revise it if the network later turns out to be metered. + // partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_IS_UNMETERED), + // accepted, rejected); + // if (accepted.size() == 1) return accepted.get(0); + // if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted); + + // If any network is for the default subscription, don't choose a network for another + // subscription with the same transport. + partitionInto(candidates, nai -> nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY), + accepted, rejected); + if (accepted.size() > 0) { + // Some networks are primary for their transport. For each transport, keep only the + // primary, but also keep all networks for which there isn't a primary (which are now + // in the |rejected| array). + // So for each primary network, remove from |rejected| all networks with the same + // transports as one of the primary networks. The remaining networks should be accepted. + for (final T defaultSubNai : accepted) { + final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes(); + rejected.removeIf( + nai -> Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes())); + } + // Now the |rejected| list contains networks with transports for which there isn't + // a primary network. Add them back to the candidates. + accepted.addAll(rejected); + candidates = new ArrayList<>(accepted); + } + if (1 == candidates.size()) return candidates.get(0); + // If there were no primary network, then candidates.size() > 0 because it didn't + // change from the previous result. If there were, it's guaranteed candidates.size() > 0 + // because accepted.size() > 0 above. + + // If some of the networks have a better transport than others, keep only the ones with + // the best transports. + for (final int transport : PREFERRED_TRANSPORTS_ORDER) { + partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport), + accepted, rejected); + if (accepted.size() == 1) return accepted.get(0); + if (accepted.size() > 0 && rejected.size() > 0) { + candidates = new ArrayList<>(accepted); + break; + } + } + + // At this point there are still multiple networks passing all the tests above. If any + // of them is the previous satisfier, keep it. + if (candidates.contains(currentSatisfier)) return currentSatisfier; + + // If there are still multiple options at this point but none of them is any of the + // transports above, it doesn't matter which is returned. They are all the same. + return candidates.get(0); + } + + // TODO : switch to the policy implementation and remove + // Almost equivalent to Collections.max(nais), but allows returning null if no network + // satisfies the request. + private NetworkAgentInfo getBestNetworkByLegacyInt( + @NonNull final Collection<NetworkAgentInfo> nais) { + NetworkAgentInfo bestNetwork = null; + int bestScore = Integer.MIN_VALUE; + for (final NetworkAgentInfo nai : nais) { + final int naiScore = nai.getCurrentScore(); + if (naiScore > bestScore) { + bestNetwork = nai; + bestScore = naiScore; + } + } + return bestNetwork; + } + + /** + * Returns whether a {@link Scoreable} has a chance to beat a champion network for a request. + * + * Offers are sent by network providers when they think they might be able to make a network + * with the characteristics contained in the offer. If the offer has no chance to beat + * the currently best network for a given request, there is no point in the provider spending + * power trying to find and bring up such a network. + * + * Note that having an offer up does not constitute a commitment from the provider part + * to be able to bring up a network with these characteristics, or a network at all for + * that matter. This is only used to save power by letting providers know when they can't + * beat a current champion. + * + * @param request The request to evaluate against. + * @param champion The currently best network for this request. + * @param contestant The offer. + * @return Whether the offer stands a chance to beat the champion. + */ + public boolean mightBeat(@NonNull final NetworkRequest request, + @Nullable final NetworkAgentInfo champion, + @NonNull final Scoreable contestant) { + // If this network can't even satisfy the request then it can't beat anything, not + // even an absence of network. It can't satisfy it anyway. + if (!request.canBeSatisfiedBy(contestant.getCapsNoCopy())) return false; + // If there is no satisfying network, then this network can beat, because some network + // is always better than no network. + if (null == champion) return true; + if (USE_POLICY_RANKING) { + // If there is no champion, the offer can always beat. + // Otherwise rank them. + final ArrayList<Scoreable> candidates = new ArrayList<>(); + candidates.add(champion); + candidates.add(contestant); + return contestant == getBestNetworkByPolicy(candidates, champion); + } else { + return mightBeatByLegacyInt(champion.getScore(), contestant); + } + } + + /** + * Returns whether a contestant might beat a champion according to the legacy int. + */ + private boolean mightBeatByLegacyInt(@Nullable final FullScore championScore, + @NonNull final Scoreable contestant) { + final int offerIntScore; + if (contestant.getCapsNoCopy().hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + // If the offer might have Internet access, then it might validate. + offerIntScore = contestant.getScore().getLegacyIntAsValidated(); + } else { + offerIntScore = contestant.getScore().getLegacyInt(); + } + return championScore.getLegacyInt() < offerIntScore; + } +} diff --git a/service/src/com/android/server/connectivity/OsCompat.java b/service/src/com/android/server/connectivity/OsCompat.java new file mode 100644 index 0000000000000000000000000000000000000000..57e3dcdf0d73fe48eaaab82783d98224c8f66d62 --- /dev/null +++ b/service/src/com/android/server/connectivity/OsCompat.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.system.ErrnoException; +import android.system.Os; + +import java.io.FileDescriptor; + +/** + * Compatibility utility for android.system.Os core platform APIs. + * + * Connectivity has access to such APIs, but they are not part of the module_current stubs yet + * (only core_current). Most stable core platform APIs are included manually in the connectivity + * build rules, but because Os is also part of the base java SDK that is earlier on the + * classpath, the extra core platform APIs are not seen. + * + * TODO (b/157639992, b/183097033): remove as soon as core_current is part of system_server_current + * @hide + */ +public class OsCompat { + // This value should be correct on all architectures supported by Android, but hardcoding ioctl + // numbers should be avoided. + /** + * @see android.system.OsConstants#TIOCOUTQ + */ + public static final int TIOCOUTQ = 0x5411; + + /** + * @see android.system.Os#getsockoptInt(FileDescriptor, int, int) + */ + public static int getsockoptInt(FileDescriptor fd, int level, int option) throws + ErrnoException { + try { + return (int) Os.class.getMethod( + "getsockoptInt", FileDescriptor.class, int.class, int.class) + .invoke(null, fd, level, option); + } catch (ReflectiveOperationException e) { + if (e.getCause() instanceof ErrnoException) { + throw (ErrnoException) e.getCause(); + } + throw new IllegalStateException("Error calling getsockoptInt", e); + } + } + + /** + * @see android.system.Os#ioctlInt(FileDescriptor, int) + */ + public static int ioctlInt(FileDescriptor fd, int cmd) throws + ErrnoException { + try { + return (int) Os.class.getMethod( + "ioctlInt", FileDescriptor.class, int.class).invoke(null, fd, cmd); + } catch (ReflectiveOperationException e) { + if (e.getCause() instanceof ErrnoException) { + throw (ErrnoException) e.getCause(); + } + throw new IllegalStateException("Error calling ioctlInt", e); + } + } +} diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..9bda59cc867e0c120fdc36e1c11a55fd11cf144c --- /dev/null +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java @@ -0,0 +1,829 @@ +/* + * 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.server.connectivity; + +import static android.Manifest.permission.CHANGE_NETWORK_STATE; +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.INTERNET; +import static android.Manifest.permission.NETWORK_STACK; +import static android.Manifest.permission.UPDATE_DEVICE_STATS; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.net.ConnectivitySettingsManager.APPS_ALLOWED_ON_RESTRICTED_NETWORKS; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.os.Process.INVALID_UID; +import static android.os.Process.SYSTEM_UID; + +import static com.android.net.module.util.CollectionUtils.toIntArray; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; +import android.net.ConnectivitySettingsManager; +import android.net.INetd; +import android.net.UidRange; +import android.net.Uri; +import android.os.Build; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.SystemConfigManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.system.OsConstants; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * A utility class to inform Netd of UID permisisons. + * Does a mass update at boot and then monitors for app install/remove. + * + * @hide + */ +public class PermissionMonitor { + private static final String TAG = "PermissionMonitor"; + private static final boolean DBG = true; + protected static final Boolean SYSTEM = Boolean.TRUE; + protected static final Boolean NETWORK = Boolean.FALSE; + private static final int VERSION_Q = Build.VERSION_CODES.Q; + + private final PackageManager mPackageManager; + private final UserManager mUserManager; + private final SystemConfigManager mSystemConfigManager; + private final INetd mNetd; + private final Dependencies mDeps; + private final Context mContext; + + @GuardedBy("this") + private final Set<UserHandle> mUsers = new HashSet<>(); + + // Keys are app uids. Values are true for SYSTEM permission and false for NETWORK permission. + @GuardedBy("this") + private final Map<Integer, Boolean> mApps = new HashMap<>(); + + // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges + // for apps under the VPN + @GuardedBy("this") + private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>(); + + // A set of appIds for apps across all users on the device. We track appIds instead of uids + // directly to reduce its size and also eliminate the need to update this set when user is + // added/removed. + @GuardedBy("this") + private final Set<Integer> mAllApps = new HashSet<>(); + + // A set of apps which are allowed to use restricted networks. These apps can't hold the + // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission because they can't be signature|privileged + // apps. However, these apps should still be able to use restricted networks under certain + // conditions (e.g. government app using emergency services). So grant netd system permission + // to uids whose package name is listed in APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting. + @GuardedBy("this") + private final Set<String> mAppsAllowedOnRestrictedNetworks = new ArraySet<>(); + + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + final Uri packageData = intent.getData(); + final String packageName = + packageData != null ? packageData.getSchemeSpecificPart() : null; + + if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { + onPackageAdded(packageName, uid); + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + onPackageRemoved(packageName, uid); + } else { + Log.wtf(TAG, "received unexpected intent: " + action); + } + } + }; + + /** + * Dependencies of PermissionMonitor, for injection in tests. + */ + @VisibleForTesting + public static class Dependencies { + /** + * Get device first sdk version. + */ + public int getDeviceFirstSdkInt() { + return Build.VERSION.DEVICE_INITIAL_SDK_INT; + } + + /** + * Get apps allowed to use restricted networks via ConnectivitySettingsManager. + */ + public Set<String> getAppsAllowedOnRestrictedNetworks(@NonNull Context context) { + return ConnectivitySettingsManager.getAppsAllowedOnRestrictedNetworks(context); + } + + /** + * Register ContentObserver for given Uri. + */ + public void registerContentObserver(@NonNull Context context, @NonNull Uri uri, + boolean notifyForDescendants, @NonNull ContentObserver observer) { + context.getContentResolver().registerContentObserver( + uri, notifyForDescendants, observer); + } + } + + public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd) { + this(context, netd, new Dependencies()); + } + + @VisibleForTesting + PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd, + @NonNull final Dependencies deps) { + mPackageManager = context.getPackageManager(); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mSystemConfigManager = context.getSystemService(SystemConfigManager.class); + mNetd = netd; + mDeps = deps; + mContext = context; + } + + // Intended to be called only once at startup, after the system is ready. Installs a broadcast + // receiver to monitor ongoing UID changes, so this shouldn't/needn't be called again. + public synchronized void startMonitoring() { + log("Monitoring"); + + final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */); + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + userAllContext.registerReceiver( + mIntentReceiver, intentFilter, null /* broadcastPermission */, + null /* scheduler */); + + // Register APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer + mDeps.registerContentObserver( + userAllContext, + Settings.Secure.getUriFor(APPS_ALLOWED_ON_RESTRICTED_NETWORKS), + false /* notifyForDescendants */, + new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + onSettingChanged(); + } + }); + + // Read APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting and update + // mAppsAllowedOnRestrictedNetworks. + updateAppsAllowedOnRestrictedNetworks(mDeps.getAppsAllowedOnRestrictedNetworks(mContext)); + + List<PackageInfo> apps = mPackageManager.getInstalledPackages(GET_PERMISSIONS + | MATCH_ANY_USER); + if (apps == null) { + loge("No apps"); + return; + } + + SparseIntArray netdPermsUids = new SparseIntArray(); + + for (PackageInfo app : apps) { + int uid = app.applicationInfo != null ? app.applicationInfo.uid : INVALID_UID; + if (uid < 0) { + continue; + } + mAllApps.add(UserHandle.getAppId(uid)); + + boolean isNetwork = hasNetworkPermission(app); + boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app); + + if (isNetwork || hasRestrictedPermission) { + Boolean permission = mApps.get(uid); + // If multiple packages share a UID (cf: android:sharedUserId) and ask for different + // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is). + if (permission == null || permission == NETWORK) { + mApps.put(uid, hasRestrictedPermission); + } + } + + //TODO: unify the management of the permissions into one codepath. + int otherNetdPerms = getNetdPermissionMask(app.requestedPermissions, + app.requestedPermissionsFlags); + netdPermsUids.put(uid, netdPermsUids.get(uid) | otherNetdPerms); + } + + mUsers.addAll(mUserManager.getUserHandles(true /* excludeDying */)); + + final SparseArray<String> netdPermToSystemPerm = new SparseArray<>(); + netdPermToSystemPerm.put(INetd.PERMISSION_INTERNET, INTERNET); + netdPermToSystemPerm.put(INetd.PERMISSION_UPDATE_DEVICE_STATS, UPDATE_DEVICE_STATS); + for (int i = 0; i < netdPermToSystemPerm.size(); i++) { + final int netdPermission = netdPermToSystemPerm.keyAt(i); + final String systemPermission = netdPermToSystemPerm.valueAt(i); + final int[] hasPermissionUids = + mSystemConfigManager.getSystemPermissionUids(systemPermission); + for (int j = 0; j < hasPermissionUids.length; j++) { + final int uid = hasPermissionUids[j]; + netdPermsUids.put(uid, netdPermsUids.get(uid) | netdPermission); + } + } + log("Users: " + mUsers.size() + ", Apps: " + mApps.size()); + update(mUsers, mApps, true); + sendPackagePermissionsToNetd(netdPermsUids); + } + + @VisibleForTesting + void updateAppsAllowedOnRestrictedNetworks(final Set<String> apps) { + mAppsAllowedOnRestrictedNetworks.clear(); + mAppsAllowedOnRestrictedNetworks.addAll(apps); + } + + @VisibleForTesting + static boolean isVendorApp(@NonNull ApplicationInfo appInfo) { + return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct(); + } + + @VisibleForTesting + boolean isCarryoverPackage(final ApplicationInfo appInfo) { + if (appInfo == null) return false; + return (appInfo.targetSdkVersion < VERSION_Q && isVendorApp(appInfo)) + // Backward compatibility for b/114245686, on devices that launched before Q daemons + // and apps running as the system UID are exempted from this check. + || (appInfo.uid == SYSTEM_UID && mDeps.getDeviceFirstSdkInt() < VERSION_Q); + } + + @VisibleForTesting + boolean isAppAllowedOnRestrictedNetworks(@NonNull final PackageInfo app) { + // Check whether package name is in allowed on restricted networks app list. If so, this app + // can have netd system permission. + return mAppsAllowedOnRestrictedNetworks.contains(app.packageName); + } + + @VisibleForTesting + boolean hasPermission(@NonNull final PackageInfo app, @NonNull final String permission) { + if (app.requestedPermissions == null || app.requestedPermissionsFlags == null) { + return false; + } + final int index = CollectionUtils.indexOf(app.requestedPermissions, permission); + if (index < 0 || index >= app.requestedPermissionsFlags.length) return false; + return (app.requestedPermissionsFlags[index] & REQUESTED_PERMISSION_GRANTED) != 0; + } + + @VisibleForTesting + boolean hasNetworkPermission(@NonNull final PackageInfo app) { + return hasPermission(app, CHANGE_NETWORK_STATE); + } + + @VisibleForTesting + boolean hasRestrictedNetworkPermission(@NonNull final PackageInfo app) { + // TODO : remove carryover package check in the future(b/31479477). All apps should just + // request the appropriate permission for their use case since android Q. + return isCarryoverPackage(app.applicationInfo) || isAppAllowedOnRestrictedNetworks(app) + || hasPermission(app, PERMISSION_MAINLINE_NETWORK_STACK) + || hasPermission(app, NETWORK_STACK) + || hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS); + } + + /** Returns whether the given uid has using background network permission. */ + public synchronized boolean hasUseBackgroundNetworksPermission(final int uid) { + // Apps with any of the CHANGE_NETWORK_STATE, NETWORK_STACK, CONNECTIVITY_INTERNAL or + // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission has the permission to use background + // networks. mApps contains the result of checks for both hasNetworkPermission and + // hasRestrictedNetworkPermission. If uid is in the mApps list that means uid has one of + // permissions at least. + return mApps.containsKey(uid); + } + + /** + * Returns whether the given uid has permission to use restricted networks. + */ + public synchronized boolean hasRestrictedNetworksPermission(int uid) { + return Boolean.TRUE.equals(mApps.get(uid)); + } + + private void update(Set<UserHandle> users, Map<Integer, Boolean> apps, boolean add) { + List<Integer> network = new ArrayList<>(); + List<Integer> system = new ArrayList<>(); + for (Entry<Integer, Boolean> app : apps.entrySet()) { + List<Integer> list = app.getValue() ? system : network; + for (UserHandle user : users) { + if (user == null) continue; + + list.add(user.getUid(app.getKey())); + } + } + try { + if (add) { + mNetd.networkSetPermissionForUser(INetd.PERMISSION_NETWORK, toIntArray(network)); + mNetd.networkSetPermissionForUser(INetd.PERMISSION_SYSTEM, toIntArray(system)); + } else { + mNetd.networkClearPermissionForUser(toIntArray(network)); + mNetd.networkClearPermissionForUser(toIntArray(system)); + } + } catch (RemoteException e) { + loge("Exception when updating permissions: " + e); + } + } + + /** + * Called when a user is added. See {link #ACTION_USER_ADDED}. + * + * @param user The integer userHandle of the added user. See {@link #EXTRA_USER_HANDLE}. + * + * @hide + */ + public synchronized void onUserAdded(@NonNull UserHandle user) { + mUsers.add(user); + + Set<UserHandle> users = new HashSet<>(); + users.add(user); + update(users, mApps, true); + } + + /** + * Called when an user is removed. See {link #ACTION_USER_REMOVED}. + * + * @param user The integer userHandle of the removed user. See {@link #EXTRA_USER_HANDLE}. + * + * @hide + */ + public synchronized void onUserRemoved(@NonNull UserHandle user) { + mUsers.remove(user); + + Set<UserHandle> users = new HashSet<>(); + users.add(user); + update(users, mApps, false); + } + + @VisibleForTesting + protected Boolean highestPermissionForUid(Boolean currentPermission, String name) { + if (currentPermission == SYSTEM) { + return currentPermission; + } + try { + final PackageInfo app = mPackageManager.getPackageInfo(name, + GET_PERMISSIONS | MATCH_ANY_USER); + final boolean isNetwork = hasNetworkPermission(app); + final boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app); + if (isNetwork || hasRestrictedPermission) { + currentPermission = hasRestrictedPermission; + } + } catch (NameNotFoundException e) { + // App not found. + loge("NameNotFoundException " + name); + } + return currentPermission; + } + + private int getPermissionForUid(final int uid) { + int permission = INetd.PERMISSION_NONE; + // Check all the packages for this UID. The UID has the permission if any of the + // packages in it has the permission. + final String[] packages = mPackageManager.getPackagesForUid(uid); + if (packages != null && packages.length > 0) { + for (String name : packages) { + final PackageInfo app = getPackageInfo(name); + if (app != null && app.requestedPermissions != null) { + permission |= getNetdPermissionMask(app.requestedPermissions, + app.requestedPermissionsFlags); + } + } + } else { + // The last package of this uid is removed from device. Clean the package up. + permission = INetd.PERMISSION_UNINSTALLED; + } + return permission; + } + + /** + * Called when a package is added. + * + * @param packageName The name of the new package. + * @param uid The uid of the new package. + * + * @hide + */ + public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) { + // TODO: Netd is using appId for checking traffic permission. Correct the methods that are + // using appId instead of uid actually + sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid)); + + // If multiple packages share a UID (cf: android:sharedUserId) and ask for different + // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is). + final Boolean permission = highestPermissionForUid(mApps.get(uid), packageName); + if (permission != mApps.get(uid)) { + mApps.put(uid, permission); + + Map<Integer, Boolean> apps = new HashMap<>(); + apps.put(uid, permission); + update(mUsers, apps, true); + } + + // If the newly-installed package falls within some VPN's uid range, update Netd with it. + // This needs to happen after the mApps update above, since removeBypassingUids() depends + // on mApps to check if the package can bypass VPN. + for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) { + if (UidRange.containsUid(vpn.getValue(), uid)) { + final Set<Integer> changedUids = new HashSet<>(); + changedUids.add(uid); + removeBypassingUids(changedUids, /* vpnAppUid */ -1); + updateVpnUids(vpn.getKey(), changedUids, true); + } + } + mAllApps.add(UserHandle.getAppId(uid)); + } + + private Boolean highestUidNetworkPermission(int uid) { + Boolean permission = null; + final String[] packages = mPackageManager.getPackagesForUid(uid); + if (!CollectionUtils.isEmpty(packages)) { + for (String name : packages) { + permission = highestPermissionForUid(permission, name); + if (permission == SYSTEM) { + break; + } + } + } + return permission; + } + + /** + * Called when a package is removed. + * + * @param packageName The name of the removed package or null. + * @param uid containing the integer uid previously assigned to the package. + * + * @hide + */ + public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) { + // TODO: Netd is using appId for checking traffic permission. Correct the methods that are + // using appId instead of uid actually + sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid)); + + // If the newly-removed package falls within some VPN's uid range, update Netd with it. + // This needs to happen before the mApps update below, since removeBypassingUids() depends + // on mApps to check if the package can bypass VPN. + for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) { + if (UidRange.containsUid(vpn.getValue(), uid)) { + final Set<Integer> changedUids = new HashSet<>(); + changedUids.add(uid); + removeBypassingUids(changedUids, /* vpnAppUid */ -1); + updateVpnUids(vpn.getKey(), changedUids, false); + } + } + // If the package has been removed from all users on the device, clear it form mAllApps. + if (mPackageManager.getNameForUid(uid) == null) { + mAllApps.remove(UserHandle.getAppId(uid)); + } + + Map<Integer, Boolean> apps = new HashMap<>(); + final Boolean permission = highestUidNetworkPermission(uid); + if (permission == SYSTEM) { + // An app with this UID still has the SYSTEM permission. + // Therefore, this UID must already have the SYSTEM permission. + // Nothing to do. + return; + } + + if (permission == mApps.get(uid)) { + // The permissions of this UID have not changed. Nothing to do. + return; + } else if (permission != null) { + mApps.put(uid, permission); + apps.put(uid, permission); + update(mUsers, apps, true); + } else { + mApps.remove(uid); + apps.put(uid, NETWORK); // doesn't matter which permission we pick here + update(mUsers, apps, false); + } + } + + private static int getNetdPermissionMask(String[] requestedPermissions, + int[] requestedPermissionsFlags) { + int permissions = 0; + if (requestedPermissions == null || requestedPermissionsFlags == null) return permissions; + for (int i = 0; i < requestedPermissions.length; i++) { + if (requestedPermissions[i].equals(INTERNET) + && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) { + permissions |= INetd.PERMISSION_INTERNET; + } + if (requestedPermissions[i].equals(UPDATE_DEVICE_STATS) + && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) { + permissions |= INetd.PERMISSION_UPDATE_DEVICE_STATS; + } + } + return permissions; + } + + private PackageInfo getPackageInfo(String packageName) { + try { + PackageInfo app = mPackageManager.getPackageInfo(packageName, GET_PERMISSIONS + | MATCH_ANY_USER); + return app; + } catch (NameNotFoundException e) { + return null; + } + } + + /** + * Called when a new set of UID ranges are added to an active VPN network + * + * @param iface The active VPN network's interface name + * @param rangesToAdd The new UID ranges to be added to the network + * @param vpnAppUid The uid of the VPN app + */ + public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd, + int vpnAppUid) { + // Calculate the list of new app uids under the VPN due to the new UID ranges and update + // Netd about them. Because mAllApps only contains appIds instead of uids, the result might + // be an overestimation if an app is not installed on the user on which the VPN is running, + // but that's safe. + final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps); + removeBypassingUids(changedUids, vpnAppUid); + updateVpnUids(iface, changedUids, true); + if (mVpnUidRanges.containsKey(iface)) { + mVpnUidRanges.get(iface).addAll(rangesToAdd); + } else { + mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd)); + } + } + + /** + * Called when a set of UID ranges are removed from an active VPN network + * + * @param iface The VPN network's interface name + * @param rangesToRemove Existing UID ranges to be removed from the VPN network + * @param vpnAppUid The uid of the VPN app + */ + public synchronized void onVpnUidRangesRemoved(@NonNull String iface, + Set<UidRange> rangesToRemove, int vpnAppUid) { + // Calculate the list of app uids that are no longer under the VPN due to the removed UID + // ranges and update Netd about them. + final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps); + removeBypassingUids(changedUids, vpnAppUid); + updateVpnUids(iface, changedUids, false); + Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null); + if (existingRanges == null) { + loge("Attempt to remove unknown vpn uid Range iface = " + iface); + return; + } + existingRanges.removeAll(rangesToRemove); + if (existingRanges.size() == 0) { + mVpnUidRanges.remove(iface); + } + } + + /** + * Compute the intersection of a set of UidRanges and appIds. Returns a set of uids + * that satisfies: + * 1. falls into one of the UidRange + * 2. matches one of the appIds + */ + private Set<Integer> intersectUids(Set<UidRange> ranges, Set<Integer> appIds) { + Set<Integer> result = new HashSet<>(); + for (UidRange range : ranges) { + for (int userId = range.getStartUser(); userId <= range.getEndUser(); userId++) { + for (int appId : appIds) { + final UserHandle handle = UserHandle.of(userId); + if (handle == null) continue; + + final int uid = handle.getUid(appId); + if (range.contains(uid)) { + result.add(uid); + } + } + } + } + return result; + } + + /** + * Remove all apps which can elect to bypass the VPN from the list of uids + * + * An app can elect to bypass the VPN if it hold SYSTEM permission, or if its the active VPN + * app itself. + * + * @param uids The list of uids to operate on + * @param vpnAppUid The uid of the VPN app + */ + private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) { + uids.remove(vpnAppUid); + uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM); + } + + /** + * Update netd about the list of uids that are under an active VPN connection which they cannot + * bypass. + * + * This is to instruct netd to set up appropriate filtering rules for these uids, such that they + * can only receive ingress packets from the VPN's tunnel interface (and loopback). + * + * @param iface the interface name of the active VPN connection + * @param add {@code true} if the uids are to be added to the interface, {@code false} if they + * are to be removed from the interface. + */ + private void updateVpnUids(String iface, Set<Integer> uids, boolean add) { + if (uids.size() == 0) { + return; + } + try { + if (add) { + mNetd.firewallAddUidInterfaceRules(iface, toIntArray(uids)); + } else { + mNetd.firewallRemoveUidInterfaceRules(toIntArray(uids)); + } + } catch (ServiceSpecificException e) { + // Silently ignore exception when device does not support eBPF, otherwise just log + // the exception and do not crash + if (e.errorCode != OsConstants.EOPNOTSUPP) { + loge("Exception when updating permissions: ", e); + } + } catch (RemoteException e) { + loge("Exception when updating permissions: ", e); + } + } + + /** + * Called by PackageListObserver when a package is installed/uninstalled. Send the updated + * permission information to netd. + * + * @param uid the app uid of the package installed + * @param permissions the permissions the app requested and netd cares about. + * + * @hide + */ + @VisibleForTesting + void sendPackagePermissionsForUid(int uid, int permissions) { + SparseIntArray netdPermissionsAppIds = new SparseIntArray(); + netdPermissionsAppIds.put(uid, permissions); + sendPackagePermissionsToNetd(netdPermissionsAppIds); + } + + /** + * Called by packageManagerService to send IPC to netd. Grant or revoke the INTERNET + * and/or UPDATE_DEVICE_STATS permission of the uids in array. + * + * @param netdPermissionsAppIds integer pairs of uids and the permission granted to it. If the + * permission is 0, revoke all permissions of that uid. + * + * @hide + */ + @VisibleForTesting + void sendPackagePermissionsToNetd(SparseIntArray netdPermissionsAppIds) { + if (mNetd == null) { + Log.e(TAG, "Failed to get the netd service"); + return; + } + ArrayList<Integer> allPermissionAppIds = new ArrayList<>(); + ArrayList<Integer> internetPermissionAppIds = new ArrayList<>(); + ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>(); + ArrayList<Integer> noPermissionAppIds = new ArrayList<>(); + ArrayList<Integer> uninstalledAppIds = new ArrayList<>(); + for (int i = 0; i < netdPermissionsAppIds.size(); i++) { + int permissions = netdPermissionsAppIds.valueAt(i); + switch(permissions) { + case (INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS): + allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i)); + break; + case INetd.PERMISSION_INTERNET: + internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i)); + break; + case INetd.PERMISSION_UPDATE_DEVICE_STATS: + updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i)); + break; + case INetd.PERMISSION_NONE: + noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i)); + break; + case INetd.PERMISSION_UNINSTALLED: + uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i)); + break; + default: + Log.e(TAG, "unknown permission type: " + permissions + "for uid: " + + netdPermissionsAppIds.keyAt(i)); + } + } + try { + // TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids() + if (allPermissionAppIds.size() != 0) { + mNetd.trafficSetNetPermForUids( + INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS, + toIntArray(allPermissionAppIds)); + } + if (internetPermissionAppIds.size() != 0) { + mNetd.trafficSetNetPermForUids(INetd.PERMISSION_INTERNET, + toIntArray(internetPermissionAppIds)); + } + if (updateStatsPermissionAppIds.size() != 0) { + mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UPDATE_DEVICE_STATS, + toIntArray(updateStatsPermissionAppIds)); + } + if (noPermissionAppIds.size() != 0) { + mNetd.trafficSetNetPermForUids(INetd.PERMISSION_NONE, + toIntArray(noPermissionAppIds)); + } + if (uninstalledAppIds.size() != 0) { + mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UNINSTALLED, + toIntArray(uninstalledAppIds)); + } + } catch (RemoteException e) { + Log.e(TAG, "Pass appId list of special permission failed." + e); + } + } + + /** Should only be used by unit tests */ + @VisibleForTesting + public Set<UidRange> getVpnUidRanges(String iface) { + return mVpnUidRanges.get(iface); + } + + private synchronized void onSettingChanged() { + // Step1. Update apps allowed to use restricted networks and compute the set of packages to + // update. + final Set<String> packagesToUpdate = new ArraySet<>(mAppsAllowedOnRestrictedNetworks); + updateAppsAllowedOnRestrictedNetworks(mDeps.getAppsAllowedOnRestrictedNetworks(mContext)); + packagesToUpdate.addAll(mAppsAllowedOnRestrictedNetworks); + + final Map<Integer, Boolean> updatedApps = new HashMap<>(); + final Map<Integer, Boolean> removedApps = new HashMap<>(); + + // Step2. For each package to update, find out its new permission. + for (String app : packagesToUpdate) { + final PackageInfo info = getPackageInfo(app); + if (info == null || info.applicationInfo == null) continue; + + final int uid = info.applicationInfo.uid; + final Boolean permission = highestUidNetworkPermission(uid); + + if (null == permission) { + removedApps.put(uid, NETWORK); // Doesn't matter which permission is set here. + mApps.remove(uid); + } else { + updatedApps.put(uid, permission); + mApps.put(uid, permission); + } + } + + // Step3. Update or revoke permission for uids with netd. + update(mUsers, updatedApps, true /* add */); + update(mUsers, removedApps, false /* add */); + } + + /** Dump info to dumpsys */ + public void dump(IndentingPrintWriter pw) { + pw.println("Interface filtering rules:"); + pw.increaseIndent(); + for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) { + pw.println("Interface: " + vpn.getKey()); + pw.println("UIDs: " + vpn.getValue().toString()); + pw.println(); + } + pw.decreaseIndent(); + } + + private static void log(String s) { + if (DBG) { + Log.d(TAG, s); + } + } + + private static void loge(String s) { + Log.e(TAG, s); + } + + private static void loge(String s, Throwable e) { + Log.e(TAG, s, e); + } +} diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java new file mode 100644 index 0000000000000000000000000000000000000000..dd2815d9e2e3c938a884979e1619c583bbaf0c71 --- /dev/null +++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.NetworkCapabilities; +import android.os.UserHandle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A data class containing all the per-profile network preferences. + * + * A given profile can only have one preference. + */ +public class ProfileNetworkPreferences { + /** + * A single preference, as it applies to a given user profile. + */ + public static class Preference { + @NonNull public final UserHandle user; + // Capabilities are only null when sending an object to remove the setting for a user + @Nullable public final NetworkCapabilities capabilities; + + public Preference(@NonNull final UserHandle user, + @Nullable final NetworkCapabilities capabilities) { + this.user = user; + this.capabilities = null == capabilities ? null : new NetworkCapabilities(capabilities); + } + + /** toString */ + public String toString() { + return "[ProfileNetworkPreference user=" + user + " caps=" + capabilities + "]"; + } + } + + @NonNull public final List<Preference> preferences; + + public ProfileNetworkPreferences() { + preferences = Collections.EMPTY_LIST; + } + + private ProfileNetworkPreferences(@NonNull final List<Preference> list) { + preferences = Collections.unmodifiableList(list); + } + + /** + * Returns a new object consisting of this object plus the passed preference. + * + * If a preference already exists for the same user, it will be replaced by the passed + * preference. Passing a Preference object containing a null capabilities object is equivalent + * to (and indeed, implemented as) removing the preference for this user. + */ + public ProfileNetworkPreferences plus(@NonNull final Preference pref) { + final ArrayList<Preference> newPrefs = new ArrayList<>(); + for (final Preference existingPref : preferences) { + if (!existingPref.user.equals(pref.user)) { + newPrefs.add(existingPref); + } + } + if (null != pref.capabilities) { + newPrefs.add(pref); + } + return new ProfileNetworkPreferences(newPrefs); + } + + public boolean isEmpty() { + return preferences.isEmpty(); + } +} diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..bc0929c206598b7ca6ff95c80242cf55a01d57d4 --- /dev/null +++ b/service/src/com/android/server/connectivity/ProxyTracker.java @@ -0,0 +1,358 @@ +/** + * 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.server.connectivity; + +import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_EXCLUSION_LIST; +import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_HOST; +import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PAC; +import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PORT; +import static android.provider.Settings.Global.HTTP_PROXY; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Network; +import android.net.PacProxyManager; +import android.net.Proxy; +import android.net.ProxyInfo; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import com.android.internal.annotations.GuardedBy; +import com.android.net.module.util.ProxyUtils; + +import java.util.Collections; +import java.util.Objects; + +/** + * A class to handle proxy for ConnectivityService. + * + * @hide + */ +public class ProxyTracker { + private static final String TAG = ProxyTracker.class.getSimpleName(); + private static final boolean DBG = true; + + // EXTRA_PROXY_INFO is now @removed. In order to continue sending it, hardcode its value here. + // The Proxy.EXTRA_PROXY_INFO constant is not visible to this code because android.net.Proxy + // a hidden platform constant not visible to mainline modules. + private static final String EXTRA_PROXY_INFO = "android.intent.extra.PROXY_INFO"; + + @NonNull + private final Context mContext; + + @NonNull + private final Object mProxyLock = new Object(); + // The global proxy is the proxy that is set device-wide, overriding any network-specific + // proxy. Note however that proxies are hints ; the system does not enforce their use. Hence + // this value is only for querying. + @Nullable + @GuardedBy("mProxyLock") + private ProxyInfo mGlobalProxy = null; + // The default proxy is the proxy that applies to no particular network if the global proxy + // is not set. Individual networks have their own settings that override this. This member + // is set through setDefaultProxy, which is called when the default network changes proxies + // in its LinkProperties, or when ConnectivityService switches to a new default network, or + // when PacProxyService resolves the proxy. + @Nullable + @GuardedBy("mProxyLock") + private volatile ProxyInfo mDefaultProxy = null; + // Whether the default proxy is enabled. + @GuardedBy("mProxyLock") + private boolean mDefaultProxyEnabled = true; + + private final Handler mConnectivityServiceHandler; + + private final PacProxyManager mPacProxyManager; + + private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener { + private final int mEvent; + + PacProxyInstalledListener(int event) { + mEvent = event; + } + + public void onPacProxyInstalled(@Nullable Network network, @NonNull ProxyInfo proxy) { + mConnectivityServiceHandler + .sendMessage(mConnectivityServiceHandler + .obtainMessage(mEvent, new Pair<>(network, proxy))); + } + } + + public ProxyTracker(@NonNull final Context context, + @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) { + mContext = context; + mConnectivityServiceHandler = connectivityServiceInternalHandler; + mPacProxyManager = context.getSystemService(PacProxyManager.class); + + PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent); + mPacProxyManager.addPacProxyInstalledListener( + mConnectivityServiceHandler::post, listener); + } + + // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present + // (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific + // proxy is null then there is no proxy in place). + @Nullable + private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) { + if (proxy != null && TextUtils.isEmpty(proxy.getHost()) + && Uri.EMPTY.equals(proxy.getPacFileUrl())) { + return null; + } + return proxy; + } + + // ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it + // better for determining if a new proxy broadcast is necessary: + // 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to + // avoid unnecessary broadcasts. + // 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL + // is in place. This is important so legacy PAC resolver (see com.android.proxyhandler) + // changes aren't missed. The legacy PAC resolver pretends to be a simple HTTP proxy but + // actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port + // all set. + public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) { + final ProxyInfo pa = canonicalizeProxyInfo(a); + final ProxyInfo pb = canonicalizeProxyInfo(b); + // ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check + // hosts even when PAC URLs are present to account for the legacy PAC resolver. + return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost())); + } + + /** + * Gets the default system-wide proxy. + * + * This will return the global proxy if set, otherwise the default proxy if in use. Note + * that this is not necessarily the proxy that any given process should use, as the right + * proxy for a process is the proxy for the network this process will use, which may be + * different from this value. This value is simply the default in case there is no proxy set + * in the network that will be used by a specific process. + * @return The default system-wide proxy or null if none. + */ + @Nullable + public ProxyInfo getDefaultProxy() { + // This information is already available as a world read/writable jvm property. + synchronized (mProxyLock) { + if (mGlobalProxy != null) return mGlobalProxy; + if (mDefaultProxyEnabled) return mDefaultProxy; + return null; + } + } + + /** + * Gets the global proxy. + * + * @return The global proxy or null if none. + */ + @Nullable + public ProxyInfo getGlobalProxy() { + // This information is already available as a world read/writable jvm property. + synchronized (mProxyLock) { + return mGlobalProxy; + } + } + + /** + * Read the global proxy settings and cache them in memory. + */ + public void loadGlobalProxy() { + if (loadDeprecatedGlobalHttpProxy()) { + return; + } + ContentResolver res = mContext.getContentResolver(); + String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST); + int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0); + String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST); + String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC); + if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) { + ProxyInfo proxyProperties; + if (!TextUtils.isEmpty(pacFileUrl)) { + proxyProperties = ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl)); + } else { + proxyProperties = ProxyInfo.buildDirectProxy(host, port, + ProxyUtils.exclusionStringAsList(exclList)); + } + if (!proxyProperties.isValid()) { + if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties); + return; + } + + synchronized (mProxyLock) { + mGlobalProxy = proxyProperties; + } + + if (!TextUtils.isEmpty(pacFileUrl)) { + mConnectivityServiceHandler.post( + () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties)); + } + } + } + + /** + * Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it. + * Returns {@code true} when global proxy was set successfully from deprecated setting. + */ + public boolean loadDeprecatedGlobalHttpProxy() { + final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY); + if (!TextUtils.isEmpty(proxy)) { + String data[] = proxy.split(":"); + if (data.length == 0) { + return false; + } + + final String proxyHost = data[0]; + int proxyPort = 8080; + if (data.length > 1) { + try { + proxyPort = Integer.parseInt(data[1]); + } catch (NumberFormatException e) { + return false; + } + } + final ProxyInfo p = ProxyInfo.buildDirectProxy(proxyHost, proxyPort, + Collections.emptyList()); + setGlobalProxy(p); + return true; + } + return false; + } + + /** + * Sends the system broadcast informing apps about a new proxy configuration. + * + * Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing + * to do in a "sendProxyBroadcast" method. + */ + public void sendProxyBroadcast() { + final ProxyInfo defaultProxy = getDefaultProxy(); + final ProxyInfo proxyInfo = null != defaultProxy ? + defaultProxy : ProxyInfo.buildDirectProxy("", 0, Collections.emptyList()); + mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo); + + if (!shouldSendBroadcast(proxyInfo)) { + return; + } + if (DBG) Log.d(TAG, "sending Proxy Broadcast for " + proxyInfo); + Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING | + Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + intent.putExtra(EXTRA_PROXY_INFO, proxyInfo); + final long ident = Binder.clearCallingIdentity(); + try { + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private boolean shouldSendBroadcast(ProxyInfo proxy) { + return Uri.EMPTY.equals(proxy.getPacFileUrl()) || proxy.getPort() > 0; + } + + /** + * Sets the global proxy in memory. Also writes the values to the global settings of the device. + * + * @param proxyInfo the proxy spec, or null for no proxy. + */ + public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) { + synchronized (mProxyLock) { + // ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed. + if (proxyInfo == mGlobalProxy) return; + if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return; + if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return; + + final String host; + final int port; + final String exclList; + final String pacFileUrl; + if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) || + !Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) { + if (!proxyInfo.isValid()) { + if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo); + return; + } + mGlobalProxy = new ProxyInfo(proxyInfo); + host = mGlobalProxy.getHost(); + port = mGlobalProxy.getPort(); + exclList = ProxyUtils.exclusionListAsString(mGlobalProxy.getExclusionList()); + pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl()) + ? "" : proxyInfo.getPacFileUrl().toString(); + } else { + host = ""; + port = 0; + exclList = ""; + pacFileUrl = ""; + mGlobalProxy = null; + } + final ContentResolver res = mContext.getContentResolver(); + final long token = Binder.clearCallingIdentity(); + try { + Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host); + Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port); + Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList); + Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl); + } finally { + Binder.restoreCallingIdentity(token); + } + + sendProxyBroadcast(); + } + } + + /** + * Sets the default proxy for the device. + * + * The default proxy is the proxy used for networks that do not have a specific proxy. + * @param proxyInfo the proxy spec, or null for no proxy. + */ + public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) { + synchronized (mProxyLock) { + if (Objects.equals(mDefaultProxy, proxyInfo)) return; + if (proxyInfo != null && !proxyInfo.isValid()) { + if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo); + return; + } + + // This call could be coming from the PacProxyService, containing the port of the + // local proxy. If this new proxy matches the global proxy then copy this proxy to the + // global (to get the correct local port), and send a broadcast. + // TODO: Switch PacProxyService to have its own message to send back rather than + // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy. + if ((mGlobalProxy != null) && (proxyInfo != null) + && (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) + && proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) { + mGlobalProxy = proxyInfo; + sendProxyBroadcast(); + return; + } + mDefaultProxy = proxyInfo; + + if (mGlobalProxy != null) return; + if (mDefaultProxyEnabled) { + sendProxyBroadcast(); + } + } + } +} diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..534dbe7699a75b08927b8bce5b9146f57e9206c0 --- /dev/null +++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE; + +import android.annotation.NonNull; +import android.net.IQosCallback; +import android.net.Network; +import android.net.QosCallbackException; +import android.net.QosFilter; +import android.net.QosSession; +import android.os.IBinder; +import android.os.RemoteException; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; +import android.util.Log; + +import java.util.Objects; + +/** + * Wraps callback related information and sends messages between network agent and the application. + * <p/> + * This is a satellite class of {@link com.android.server.ConnectivityService} and not meant + * to be used in other contexts. + * + * @hide + */ +class QosCallbackAgentConnection implements IBinder.DeathRecipient { + private static final String TAG = QosCallbackAgentConnection.class.getSimpleName(); + private static final boolean DBG = false; + + private final int mAgentCallbackId; + @NonNull private final QosCallbackTracker mQosCallbackTracker; + @NonNull private final IQosCallback mCallback; + @NonNull private final IBinder mBinder; + @NonNull private final QosFilter mFilter; + @NonNull private final NetworkAgentInfo mNetworkAgentInfo; + + private final int mUid; + + /** + * Gets the uid + * @return uid + */ + int getUid() { + return mUid; + } + + /** + * Gets the binder + * @return binder + */ + @NonNull + IBinder getBinder() { + return mBinder; + } + + /** + * Gets the callback id + * + * @return callback id + */ + int getAgentCallbackId() { + return mAgentCallbackId; + } + + /** + * Gets the network tied to the callback of this connection + * + * @return network + */ + @NonNull + Network getNetwork() { + return mFilter.getNetwork(); + } + + QosCallbackAgentConnection(@NonNull final QosCallbackTracker qosCallbackTracker, + final int agentCallbackId, + @NonNull final IQosCallback callback, + @NonNull final QosFilter filter, + final int uid, + @NonNull final NetworkAgentInfo networkAgentInfo) { + Objects.requireNonNull(qosCallbackTracker, "qosCallbackTracker must be non-null"); + Objects.requireNonNull(callback, "callback must be non-null"); + Objects.requireNonNull(filter, "filter must be non-null"); + Objects.requireNonNull(networkAgentInfo, "networkAgentInfo must be non-null"); + + mQosCallbackTracker = qosCallbackTracker; + mAgentCallbackId = agentCallbackId; + mCallback = callback; + mFilter = filter; + mUid = uid; + mBinder = mCallback.asBinder(); + mNetworkAgentInfo = networkAgentInfo; + } + + @Override + public void binderDied() { + logw("binderDied: binder died with callback id: " + mAgentCallbackId); + mQosCallbackTracker.unregisterCallback(mCallback); + } + + void unlinkToDeathRecipient() { + mBinder.unlinkToDeath(this, 0); + } + + // Returns false if the NetworkAgent was never notified. + boolean sendCmdRegisterCallback() { + final int exceptionType = mFilter.validate(); + if (exceptionType != EX_TYPE_FILTER_NONE) { + try { + if (DBG) log("sendCmdRegisterCallback: filter validation failed"); + mCallback.onError(exceptionType); + } catch (final RemoteException e) { + loge("sendCmdRegisterCallback:", e); + } + return false; + } + + try { + mBinder.linkToDeath(this, 0); + } catch (final RemoteException e) { + loge("failed linking to death recipient", e); + return false; + } + mNetworkAgentInfo.onQosFilterCallbackRegistered(mAgentCallbackId, mFilter); + return true; + } + + void sendCmdUnregisterCallback() { + if (DBG) log("sendCmdUnregisterCallback: unregistering"); + mNetworkAgentInfo.onQosCallbackUnregistered(mAgentCallbackId); + } + + void sendEventEpsQosSessionAvailable(final QosSession session, + final EpsBearerQosSessionAttributes attributes) { + try { + if (DBG) log("sendEventEpsQosSessionAvailable: sending..."); + mCallback.onQosEpsBearerSessionAvailable(session, attributes); + } catch (final RemoteException e) { + loge("sendEventEpsQosSessionAvailable: remote exception", e); + } + } + + void sendEventNrQosSessionAvailable(final QosSession session, + final NrQosSessionAttributes attributes) { + try { + if (DBG) log("sendEventNrQosSessionAvailable: sending..."); + mCallback.onNrQosSessionAvailable(session, attributes); + } catch (final RemoteException e) { + loge("sendEventNrQosSessionAvailable: remote exception", e); + } + } + + void sendEventQosSessionLost(@NonNull final QosSession session) { + try { + if (DBG) log("sendEventQosSessionLost: sending..."); + mCallback.onQosSessionLost(session); + } catch (final RemoteException e) { + loge("sendEventQosSessionLost: remote exception", e); + } + } + + void sendEventQosCallbackError(@QosCallbackException.ExceptionType final int exceptionType) { + try { + if (DBG) log("sendEventQosCallbackError: sending..."); + mCallback.onError(exceptionType); + } catch (final RemoteException e) { + loge("sendEventQosCallbackError: remote exception", e); + } + } + + private static void log(@NonNull final String msg) { + Log.d(TAG, msg); + } + + private static void logw(@NonNull final String msg) { + Log.w(TAG, msg); + } + + private static void loge(@NonNull final String msg, final Throwable t) { + Log.e(TAG, msg, t); + } +} diff --git a/service/src/com/android/server/connectivity/QosCallbackTracker.java b/service/src/com/android/server/connectivity/QosCallbackTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..b6ab47b276e330382aba8210f74ef10d0562582c --- /dev/null +++ b/service/src/com/android/server/connectivity/QosCallbackTracker.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IQosCallback; +import android.net.Network; +import android.net.QosCallbackException; +import android.net.QosFilter; +import android.net.QosSession; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; +import android.util.Log; + +import com.android.net.module.util.CollectionUtils; +import com.android.server.ConnectivityService; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tracks qos callbacks and handles the communication between the network agent and application. + * <p/> + * Any method prefixed by handle must be called from the + * {@link com.android.server.ConnectivityService} handler thread. + * + * @hide + */ +public class QosCallbackTracker { + private static final String TAG = QosCallbackTracker.class.getSimpleName(); + private static final boolean DBG = true; + + @NonNull + private final Handler mConnectivityServiceHandler; + + @NonNull + private final ConnectivityService.PerUidCounter mNetworkRequestCounter; + + /** + * Each agent gets a unique callback id that is used to proxy messages back to the original + * callback. + * <p/> + * Note: The fact that this is initialized to 0 is to ensure that the thread running + * {@link #handleRegisterCallback(IQosCallback, QosFilter, int, NetworkAgentInfo)} sees the + * initialized value. This would not necessarily be the case if the value was initialized to + * the non-default value. + * <p/> + * Note: The term previous does not apply to the first callback id that is assigned. + */ + private int mPreviousAgentCallbackId = 0; + + @NonNull + private final List<QosCallbackAgentConnection> mConnections = new ArrayList<>(); + + /** + * + * @param connectivityServiceHandler must be the same handler used with + * {@link com.android.server.ConnectivityService} + * @param networkRequestCounter keeps track of the number of open requests under a given + * uid + */ + public QosCallbackTracker(@NonNull final Handler connectivityServiceHandler, + final ConnectivityService.PerUidCounter networkRequestCounter) { + mConnectivityServiceHandler = connectivityServiceHandler; + mNetworkRequestCounter = networkRequestCounter; + } + + /** + * Registers the callback with the tracker + * + * @param callback the callback to register + * @param filter the filter being registered alongside the callback + */ + public void registerCallback(@NonNull final IQosCallback callback, + @NonNull final QosFilter filter, @NonNull final NetworkAgentInfo networkAgentInfo) { + final int uid = Binder.getCallingUid(); + + // Enforce that the number of requests under this uid has exceeded the allowed number + mNetworkRequestCounter.incrementCountOrThrow(uid); + + mConnectivityServiceHandler.post( + () -> handleRegisterCallback(callback, filter, uid, networkAgentInfo)); + } + + private void handleRegisterCallback(@NonNull final IQosCallback callback, + @NonNull final QosFilter filter, final int uid, + @NonNull final NetworkAgentInfo networkAgentInfo) { + final QosCallbackAgentConnection ac = + handleRegisterCallbackInternal(callback, filter, uid, networkAgentInfo); + if (ac != null) { + if (DBG) log("handleRegisterCallback: added callback " + ac.getAgentCallbackId()); + mConnections.add(ac); + } else { + mNetworkRequestCounter.decrementCount(uid); + } + } + + private QosCallbackAgentConnection handleRegisterCallbackInternal( + @NonNull final IQosCallback callback, + @NonNull final QosFilter filter, final int uid, + @NonNull final NetworkAgentInfo networkAgentInfo) { + final IBinder binder = callback.asBinder(); + if (CollectionUtils.any(mConnections, c -> c.getBinder().equals(binder))) { + // A duplicate registration would have only made this far due to a programming error. + logwtf("handleRegisterCallback: Callbacks can only be register once."); + return null; + } + + mPreviousAgentCallbackId = mPreviousAgentCallbackId + 1; + final int newCallbackId = mPreviousAgentCallbackId; + + final QosCallbackAgentConnection ac = + new QosCallbackAgentConnection(this, newCallbackId, callback, + filter, uid, networkAgentInfo); + + final int exceptionType = filter.validate(); + if (exceptionType != QosCallbackException.EX_TYPE_FILTER_NONE) { + ac.sendEventQosCallbackError(exceptionType); + return null; + } + + // Only add to the callback maps if the NetworkAgent successfully registered it + if (!ac.sendCmdRegisterCallback()) { + // There was an issue when registering the agent + if (DBG) log("handleRegisterCallback: error sending register callback"); + mNetworkRequestCounter.decrementCount(uid); + return null; + } + return ac; + } + + /** + * Unregisters callback + * @param callback callback to unregister + */ + public void unregisterCallback(@NonNull final IQosCallback callback) { + mConnectivityServiceHandler.post(() -> handleUnregisterCallback(callback.asBinder(), true)); + } + + private void handleUnregisterCallback(@NonNull final IBinder binder, + final boolean sendToNetworkAgent) { + final int connIndex = + CollectionUtils.indexOf(mConnections, c -> c.getBinder().equals(binder)); + if (connIndex < 0) { + logw("handleUnregisterCallback: no matching agentConnection"); + return; + } + final QosCallbackAgentConnection agentConnection = mConnections.get(connIndex); + + if (DBG) { + log("handleUnregisterCallback: unregister " + + agentConnection.getAgentCallbackId()); + } + + mNetworkRequestCounter.decrementCount(agentConnection.getUid()); + mConnections.remove(agentConnection); + + if (sendToNetworkAgent) { + agentConnection.sendCmdUnregisterCallback(); + } + agentConnection.unlinkToDeathRecipient(); + } + + /** + * Called when the NetworkAgent sends the qos session available event for EPS + * + * @param qosCallbackId the callback id that the qos session is now available to + * @param session the qos session that is now available + * @param attributes the qos attributes that are now available on the qos session + */ + public void sendEventEpsQosSessionAvailable(final int qosCallbackId, + final QosSession session, + final EpsBearerQosSessionAttributes attributes) { + runOnAgentConnection(qosCallbackId, "sendEventEpsQosSessionAvailable: ", + ac -> ac.sendEventEpsQosSessionAvailable(session, attributes)); + } + + /** + * Called when the NetworkAgent sends the qos session available event for NR + * + * @param qosCallbackId the callback id that the qos session is now available to + * @param session the qos session that is now available + * @param attributes the qos attributes that are now available on the qos session + */ + public void sendEventNrQosSessionAvailable(final int qosCallbackId, + final QosSession session, + final NrQosSessionAttributes attributes) { + runOnAgentConnection(qosCallbackId, "sendEventNrQosSessionAvailable: ", + ac -> ac.sendEventNrQosSessionAvailable(session, attributes)); + } + + /** + * Called when the NetworkAgent sends the qos session lost event + * + * @param qosCallbackId the callback id that lost the qos session + * @param session the corresponding qos session + */ + public void sendEventQosSessionLost(final int qosCallbackId, + final QosSession session) { + runOnAgentConnection(qosCallbackId, "sendEventQosSessionLost: ", + ac -> ac.sendEventQosSessionLost(session)); + } + + /** + * Called when the NetworkAgent sends the qos session on error event + * + * @param qosCallbackId the callback id that should receive the exception + * @param exceptionType the type of exception that caused the callback to error + */ + public void sendEventQosCallbackError(final int qosCallbackId, + @QosCallbackException.ExceptionType final int exceptionType) { + runOnAgentConnection(qosCallbackId, "sendEventQosCallbackError: ", + ac -> { + ac.sendEventQosCallbackError(exceptionType); + handleUnregisterCallback(ac.getBinder(), false); + }); + } + + /** + * Unregisters all callbacks associated to this network agent + * + * Note: Must be called on the connectivity service handler thread + * + * @param network the network that was released + */ + public void handleNetworkReleased(@Nullable final Network network) { + // Iterate in reverse order as agent connections will be removed when unregistering + for (int i = mConnections.size() - 1; i >= 0; i--) { + final QosCallbackAgentConnection agentConnection = mConnections.get(i); + if (!agentConnection.getNetwork().equals(network)) continue; + agentConnection.sendEventQosCallbackError( + QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED); + + // Call unregister workflow w\o sending anything to agent since it is disconnected. + handleUnregisterCallback(agentConnection.getBinder(), false); + } + } + + private interface AgentConnectionAction { + void execute(@NonNull QosCallbackAgentConnection agentConnection); + } + + @Nullable + private void runOnAgentConnection(final int qosCallbackId, + @NonNull final String logPrefix, + @NonNull final AgentConnectionAction action) { + mConnectivityServiceHandler.post(() -> { + final int acIndex = CollectionUtils.indexOf(mConnections, + c -> c.getAgentCallbackId() == qosCallbackId); + if (acIndex == -1) { + loge(logPrefix + ": " + qosCallbackId + " missing callback id"); + return; + } + + action.execute(mConnections.get(acIndex)); + }); + } + + private static void log(final String msg) { + Log.d(TAG, msg); + } + + private static void logw(final String msg) { + Log.w(TAG, msg); + } + + private static void loge(final String msg) { + Log.e(TAG, msg); + } + + private static void logwtf(final String msg) { + Log.wtf(TAG, msg); + } +} diff --git a/service/src/com/android/server/connectivity/TcpKeepaliveController.java b/service/src/com/android/server/connectivity/TcpKeepaliveController.java new file mode 100644 index 0000000000000000000000000000000000000000..73f34751731ef7dfde32b3ca7491476a48758c1b --- /dev/null +++ b/service/src/com/android/server/connectivity/TcpKeepaliveController.java @@ -0,0 +1,340 @@ +/* + * 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.server.connectivity; + +import static android.net.SocketKeepalive.DATA_RECEIVED; +import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET; +import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE; +import static android.net.SocketKeepalive.ERROR_UNSUPPORTED; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.system.OsConstants.ENOPROTOOPT; +import static android.system.OsConstants.FIONREAD; +import static android.system.OsConstants.IPPROTO_IP; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.IP_TOS; +import static android.system.OsConstants.IP_TTL; + +import static com.android.server.connectivity.OsCompat.TIOCOUTQ; + +import android.annotation.NonNull; +import android.net.InvalidPacketException; +import android.net.NetworkUtils; +import android.net.SocketKeepalive.InvalidSocketException; +import android.net.TcpKeepalivePacketData; +import android.net.TcpKeepalivePacketDataParcelable; +import android.net.TcpRepairWindow; +import android.net.util.KeepalivePacketDataUtil; +import android.os.Handler; +import android.os.MessageQueue; +import android.os.Messenger; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo; + +import java.io.FileDescriptor; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; + +/** + * Manage tcp socket which offloads tcp keepalive. + * + * The input socket will be changed to repair mode and the application + * will not have permission to read/write data. If the application wants + * to write data, it must stop tcp keepalive offload to leave repair mode + * first. If a remote packet arrives, repair mode will be turned off and + * offload will be stopped. The application will receive a callback to know + * it can start reading data. + * + * {start,stop}SocketMonitor are thread-safe, but care must be taken in the + * order in which they are called. Please note that while calling + * {@link #startSocketMonitor(FileDescriptor, Messenger, int)} multiple times + * with either the same slot or the same FileDescriptor without stopping it in + * between will result in an exception, calling {@link #stopSocketMonitor(int)} + * multiple times with the same int is explicitly a no-op. + * Please also note that switching the socket to repair mode is not synchronized + * with either of these operations and has to be done in an orderly fashion + * with stopSocketMonitor. Take care in calling these in the right order. + * @hide + */ +public class TcpKeepaliveController { + private static final String TAG = "TcpKeepaliveController"; + private static final boolean DBG = false; + + private final MessageQueue mFdHandlerQueue; + + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + + // Reference include/uapi/linux/tcp.h + private static final int TCP_REPAIR = 19; + private static final int TCP_REPAIR_QUEUE = 20; + private static final int TCP_QUEUE_SEQ = 21; + private static final int TCP_NO_QUEUE = 0; + private static final int TCP_RECV_QUEUE = 1; + private static final int TCP_SEND_QUEUE = 2; + private static final int TCP_REPAIR_OFF = 0; + private static final int TCP_REPAIR_ON = 1; + // Reference include/uapi/linux/sockios.h + private static final int SIOCINQ = FIONREAD; + private static final int SIOCOUTQ = TIOCOUTQ; + + /** + * Keeps track of packet listeners. + * Key: slot number of keepalive offload. + * Value: {@link FileDescriptor} being listened to. + */ + @GuardedBy("mListeners") + private final SparseArray<FileDescriptor> mListeners = new SparseArray<>(); + + public TcpKeepaliveController(final Handler connectivityServiceHandler) { + mFdHandlerQueue = connectivityServiceHandler.getLooper().getQueue(); + } + + /** Build tcp keepalive packet. */ + public static TcpKeepalivePacketData getTcpKeepalivePacket(@NonNull FileDescriptor fd) + throws InvalidPacketException, InvalidSocketException { + try { + final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd); + return KeepalivePacketDataUtil.fromStableParcelable(tcpDetails); + } catch (InvalidPacketException | InvalidSocketException e) { + switchOutOfRepairMode(fd); + throw e; + } + } + /** + * Switch the tcp socket to repair mode and query detail tcp information. + * + * @param fd the fd of socket on which to use keepalive offload. + * @return a {@link TcpKeepalivePacketDataParcelable} object for current + * tcp/ip information. + */ + private static TcpKeepalivePacketDataParcelable switchToRepairMode(FileDescriptor fd) + throws InvalidSocketException { + if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd); + final TcpKeepalivePacketDataParcelable tcpDetails = new TcpKeepalivePacketDataParcelable(); + final SocketAddress srcSockAddr; + final SocketAddress dstSockAddr; + final TcpRepairWindow trw; + + // Query source address and port. + try { + srcSockAddr = Os.getsockname(fd); + } catch (ErrnoException e) { + Log.e(TAG, "Get sockname fail: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + if (srcSockAddr instanceof InetSocketAddress) { + tcpDetails.srcAddress = getAddress((InetSocketAddress) srcSockAddr); + tcpDetails.srcPort = getPort((InetSocketAddress) srcSockAddr); + } else { + Log.e(TAG, "Invalid or mismatched SocketAddress"); + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + // Query destination address and port. + try { + dstSockAddr = Os.getpeername(fd); + } catch (ErrnoException e) { + Log.e(TAG, "Get peername fail: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + if (dstSockAddr instanceof InetSocketAddress) { + tcpDetails.dstAddress = getAddress((InetSocketAddress) dstSockAddr); + tcpDetails.dstPort = getPort((InetSocketAddress) dstSockAddr); + } else { + Log.e(TAG, "Invalid or mismatched peer SocketAddress"); + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + + // Query sequence and ack number + dropAllIncomingPackets(fd, true); + try { + // Switch to tcp repair mode. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON); + + // Check if socket is idle. + if (!isSocketIdle(fd)) { + Log.e(TAG, "Socket is not idle"); + throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE); + } + // Query write sequence number from SEND_QUEUE. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_SEND_QUEUE); + tcpDetails.seq = OsCompat.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ); + // Query read sequence number from RECV_QUEUE. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_RECV_QUEUE); + tcpDetails.ack = OsCompat.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ); + // Switch to NO_QUEUE to prevent illegal socket read/write in repair mode. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_NO_QUEUE); + // Finally, check if socket is still idle. TODO : this check needs to move to + // after starting polling to prevent a race. + if (!isReceiveQueueEmpty(fd)) { + Log.e(TAG, "Fatal: receive queue of this socket is not empty"); + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + if (!isSendQueueEmpty(fd)) { + Log.e(TAG, "Socket is not idle"); + throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE); + } + + // Query tcp window size. + trw = NetworkUtils.getTcpRepairWindow(fd); + tcpDetails.rcvWnd = trw.rcvWnd; + tcpDetails.rcvWndScale = trw.rcvWndScale; + if (tcpDetails.srcAddress.length == 4 /* V4 address length */) { + // Query TOS. + tcpDetails.tos = OsCompat.getsockoptInt(fd, IPPROTO_IP, IP_TOS); + // Query TTL. + tcpDetails.ttl = OsCompat.getsockoptInt(fd, IPPROTO_IP, IP_TTL); + } + } catch (ErrnoException e) { + Log.e(TAG, "Exception reading TCP state from socket", e); + if (e.errno == ENOPROTOOPT) { + // ENOPROTOOPT may happen in kernel version lower than 4.8. + // Treat it as ERROR_UNSUPPORTED. + throw new InvalidSocketException(ERROR_UNSUPPORTED, e); + } else { + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + } finally { + dropAllIncomingPackets(fd, false); + } + + // Keepalive sequence number is last sequence number - 1. If it couldn't be retrieved, + // then it must be set to -1, so decrement in all cases. + tcpDetails.seq = tcpDetails.seq - 1; + + return tcpDetails; + } + + /** + * Switch the tcp socket out of repair mode. + * + * @param fd the fd of socket to switch back to normal. + */ + private static void switchOutOfRepairMode(@NonNull final FileDescriptor fd) { + try { + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot switch socket out of repair mode", e); + // Well, there is not much to do here to recover + } + } + + /** + * Start monitoring incoming packets. + * + * @param fd socket fd to monitor. + * @param ki a {@link KeepaliveInfo} that tracks information about a socket keepalive. + * @param slot keepalive slot. + */ + public void startSocketMonitor(@NonNull final FileDescriptor fd, + @NonNull final KeepaliveInfo ki, final int slot) + throws IllegalArgumentException, InvalidSocketException { + synchronized (mListeners) { + if (null != mListeners.get(slot)) { + throw new IllegalArgumentException("This slot is already taken"); + } + for (int i = 0; i < mListeners.size(); ++i) { + if (fd.equals(mListeners.valueAt(i))) { + Log.e(TAG, "This fd is already registered."); + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + } + mFdHandlerQueue.addOnFileDescriptorEventListener(fd, FD_EVENTS, (readyFd, events) -> { + // This can't be called twice because the queue guarantees that once the listener + // is unregistered it can't be called again, even for a message that arrived + // before it was unregistered. + final int reason; + if (0 != (events & EVENT_ERROR)) { + reason = ERROR_INVALID_SOCKET; + } else { + reason = DATA_RECEIVED; + } + ki.onFileDescriptorInitiatedStop(reason); + // The listener returns the new set of events to listen to. Because 0 means no + // event, the listener gets unregistered. + return 0; + }); + mListeners.put(slot, fd); + } + } + + /** Stop socket monitor */ + // This slot may have been stopped automatically already because the socket received data, + // was closed on the other end or otherwise suffered some error. In this case, this function + // is a no-op. + public void stopSocketMonitor(final int slot) { + final FileDescriptor fd; + synchronized (mListeners) { + fd = mListeners.get(slot); + if (null == fd) return; + mListeners.remove(slot); + } + mFdHandlerQueue.removeOnFileDescriptorEventListener(fd); + if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd); + switchOutOfRepairMode(fd); + } + + private static byte [] getAddress(InetSocketAddress inetAddr) { + return inetAddr.getAddress().getAddress(); + } + + private static int getPort(InetSocketAddress inetAddr) { + return inetAddr.getPort(); + } + + private static boolean isSocketIdle(FileDescriptor fd) throws ErrnoException { + return isReceiveQueueEmpty(fd) && isSendQueueEmpty(fd); + } + + private static boolean isReceiveQueueEmpty(FileDescriptor fd) + throws ErrnoException { + final int result = OsCompat.ioctlInt(fd, SIOCINQ); + if (result != 0) { + Log.e(TAG, "Read queue has data"); + return false; + } + return true; + } + + private static boolean isSendQueueEmpty(FileDescriptor fd) + throws ErrnoException { + final int result = OsCompat.ioctlInt(fd, SIOCOUTQ); + if (result != 0) { + Log.e(TAG, "Write queue has data"); + return false; + } + return true; + } + + private static void dropAllIncomingPackets(FileDescriptor fd, boolean enable) + throws InvalidSocketException { + try { + if (enable) { + NetworkUtils.attachDropAllBPFFilter(fd); + } else { + NetworkUtils.detachBPFFilter(fd); + } + } catch (SocketException e) { + Log.e(TAG, "Socket Exception: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + } +} diff --git a/tests/OWNERS b/tests/OWNERS new file mode 100644 index 0000000000000000000000000000000000000000..d3836d4c6c5749cd46140b6cfd548ffe2feac5e9 --- /dev/null +++ b/tests/OWNERS @@ -0,0 +1,8 @@ +set noparent + +codewiz@google.com +jchalard@google.com +junyulai@google.com +lorenzo@google.com +reminv@google.com +satk@google.com diff --git a/tests/TEST_MAPPING b/tests/TEST_MAPPING new file mode 100644 index 0000000000000000000000000000000000000000..502f885ceb785efa2e1a30075cca46cdbafc2e79 --- /dev/null +++ b/tests/TEST_MAPPING @@ -0,0 +1,34 @@ +{ + "presubmit": [ + { + "name": "FrameworksNetIntegrationTests" + } + ], + "postsubmit": [ + { + "name": "FrameworksNetDeflakeTest" + } + ], + "auto-postsubmit": [ + // Test tag for automotive targets. These are only running in postsubmit so as to harden the + // automotive targets to avoid introducing additional test flake and build time. The plan for + // presubmit testing for auto is to augment the existing tests to cover auto use cases as well. + // Additionally, this tag is used in targeted test suites to limit resource usage on the test + // infra during the hardening phase. + // TODO: this tag to be removed once the above is no longer an issue. + { + "name": "FrameworksNetTests" + }, + { + "name": "FrameworksNetIntegrationTests" + }, + { + "name": "FrameworksNetDeflakeTest" + } + ], + "imports": [ + { + "path": "packages/modules/Connectivity" + } + ] +} \ No newline at end of file diff --git a/tests/common/Android.bp b/tests/common/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..e8963b9011ff3e7c2c12e202a8567e23ba2dc3a4 --- /dev/null +++ b/tests/common/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. +// + +// Tests in this folder are included both in unit tests and CTS. +// They must be fast and stable, and exercise public or test APIs. +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "FrameworksNetCommonTests", + defaults: ["framework-connectivity-test-defaults"], + srcs: [ + "java/**/*.java", + "java/**/*.kt", + ], + static_libs: [ + "androidx.core_core", + "androidx.test.rules", + "junit", + "mockito-target-minus-junit4", + "modules-utils-build", + "net-tests-utils", + "net-utils-framework-common", + "platform-test-annotations", + ], + libs: [ + "android.test.base.stubs", + ], +} + +// defaults for tests that need to build against framework-connectivity's @hide APIs +// Only usable from targets that have visibility on framework-connectivity.impl. +// Instead of using this, consider avoiding to depend on hidden connectivity APIs in +// tests. +java_defaults { + name: "framework-connectivity-test-defaults", + sdk_version: "core_platform", // tests can use @CorePlatformApi's + libs: [ + // order matters: classes in framework-connectivity are resolved before framework, + // meaning @hide APIs in framework-connectivity are resolved before @SystemApi + // stubs in framework + "framework-connectivity.impl", + "framework", + + // if sdk_version="" this gets automatically included, but here we need to add manually. + "framework-res", + ], +} diff --git a/tests/common/java/ParseExceptionTest.kt b/tests/common/java/ParseExceptionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b702d61a9fe163344a194ee150e933abc2481c2f --- /dev/null +++ b/tests/common/java/ParseExceptionTest.kt @@ -0,0 +1,52 @@ +/* + * 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. + */ + +import android.net.ParseException +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ParseExceptionTest { + @get:Rule + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.R) + + @Test + fun testConstructor_WithCause() { + val testMessage = "Test message" + val base = Exception("Test") + val exception = ParseException(testMessage, base) + + assertEquals(testMessage, exception.response) + assertEquals(base, exception.cause) + } + + @Test + fun testConstructor_NoCause() { + val testMessage = "Test message" + val exception = ParseException(testMessage) + + assertEquals(testMessage, exception.response) + assertNull(exception.cause) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..18a93319b271c7ac5fb64b752f2b837b70376fa2 --- /dev/null +++ b/tests/common/java/android/net/CaptivePortalDataTest.kt @@ -0,0 +1,190 @@ +/* + * 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.os.Build +import androidx.test.filters.SmallTest +import com.android.modules.utils.build.SdkLevel +import com.android.testutils.assertParcelSane +import com.android.testutils.assertParcelingIsLossless +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +class CaptivePortalDataTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + private val data = CaptivePortalData.Builder() + .setRefreshTime(123L) + .setUserPortalUrl(Uri.parse("https://portal.example.com/test")) + .setVenueInfoUrl(Uri.parse("https://venue.example.com/test")) + .setSessionExtendable(true) + .setBytesRemaining(456L) + .setExpiryTime(789L) + .setCaptive(true) + .apply { + if (SdkLevel.isAtLeastS()) { + setVenueFriendlyName("venue friendly name") + } + } + .build() + + private val dataFromPasspoint = CaptivePortalData.Builder() + .setCaptive(true) + .apply { + if (SdkLevel.isAtLeastS()) { + setVenueFriendlyName("venue friendly name") + setUserPortalUrl(Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + setVenueInfoUrl(Uri.parse("https://venue.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + } + } + .build() + + private fun makeBuilder() = CaptivePortalData.Builder(data) + + @Test + fun testParcelUnparcel() { + val fieldCount = if (SdkLevel.isAtLeastS()) 10 else 7 + assertParcelSane(data, fieldCount) + assertParcelSane(dataFromPasspoint, fieldCount) + + assertParcelingIsLossless(makeBuilder().setUserPortalUrl(null).build()) + assertParcelingIsLossless(makeBuilder().setVenueInfoUrl(null).build()) + } + + @Test + fun testEquals() { + assertEquals(data, makeBuilder().build()) + + assertNotEqualsAfterChange { it.setRefreshTime(456L) } + assertNotEqualsAfterChange { it.setUserPortalUrl(Uri.parse("https://example.com/")) } + assertNotEqualsAfterChange { it.setUserPortalUrl(null) } + assertNotEqualsAfterChange { it.setVenueInfoUrl(Uri.parse("https://example.com/")) } + assertNotEqualsAfterChange { it.setVenueInfoUrl(null) } + assertNotEqualsAfterChange { it.setSessionExtendable(false) } + assertNotEqualsAfterChange { it.setBytesRemaining(789L) } + assertNotEqualsAfterChange { it.setExpiryTime(12L) } + assertNotEqualsAfterChange { it.setCaptive(false) } + + if (SdkLevel.isAtLeastS()) { + assertNotEqualsAfterChange { it.setVenueFriendlyName("another friendly name") } + assertNotEqualsAfterChange { it.setVenueFriendlyName(null) } + + assertEquals(dataFromPasspoint, CaptivePortalData.Builder(dataFromPasspoint).build()) + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint")) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/other"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) } + assertNotEqualsAfterChange { it.setUserPortalUrl( + Uri.parse("https://tc.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + assertNotEqualsAfterChange { it.setVenueInfoUrl( + Uri.parse("https://venue.example.com/passpoint")) } + assertNotEqualsAfterChange { it.setVenueInfoUrl( + Uri.parse("https://venue.example.com/other"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) } + assertNotEqualsAfterChange { it.setVenueInfoUrl( + Uri.parse("https://venue.example.com/passpoint"), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) } + } + } + + @Test + fun testUserPortalUrl() { + assertEquals(Uri.parse("https://portal.example.com/test"), data.userPortalUrl) + } + + @Test + fun testVenueInfoUrl() { + assertEquals(Uri.parse("https://venue.example.com/test"), data.venueInfoUrl) + } + + @Test + fun testIsSessionExtendable() { + assertTrue(data.isSessionExtendable) + } + + @Test + fun testByteLimit() { + assertEquals(456L, data.byteLimit) + // Test byteLimit unset. + assertEquals(-1L, CaptivePortalData.Builder(null).build().byteLimit) + } + + @Test + fun testRefreshTimeMillis() { + assertEquals(123L, data.refreshTimeMillis) + } + + @Test + fun testExpiryTimeMillis() { + assertEquals(789L, data.expiryTimeMillis) + // Test expiryTimeMillis unset. + assertEquals(-1L, CaptivePortalData.Builder(null).build().expiryTimeMillis) + } + + @Test + fun testIsCaptive() { + assertTrue(data.isCaptive) + assertFalse(makeBuilder().setCaptive(false).build().isCaptive) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + fun testVenueFriendlyName() { + assertEquals("venue friendly name", data.venueFriendlyName) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + fun testGetVenueInfoUrlSource() { + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + data.venueInfoUrlSource) + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + dataFromPasspoint.venueInfoUrlSource) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + fun testGetUserPortalUrlSource() { + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER, + data.userPortalUrlSource) + assertEquals(CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT, + dataFromPasspoint.userPortalUrlSource) + } + + private fun CaptivePortalData.mutate(mutator: (CaptivePortalData.Builder) -> Unit) = + CaptivePortalData.Builder(this).apply { mutator(this) }.build() + + private fun assertNotEqualsAfterChange(mutator: (CaptivePortalData.Builder) -> Unit) { + assertNotEquals(data, data.mutate(mutator)) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/CaptivePortalTest.java b/tests/common/java/android/net/CaptivePortalTest.java new file mode 100644 index 0000000000000000000000000000000000000000..15d3398d43c0fd45883cf179dd526af6d3f99733 --- /dev/null +++ b/tests/common/java/android/net/CaptivePortalTest.java @@ -0,0 +1,121 @@ +/* + * 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 org.junit.Assert.assertEquals; + +import android.os.Build; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CaptivePortalTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final int DEFAULT_TIMEOUT_MS = 5000; + private static final String TEST_PACKAGE_NAME = "com.google.android.test"; + + private final class MyCaptivePortalImpl extends ICaptivePortal.Stub { + int mCode = -1; + String mPackageName = null; + + @Override + public void appResponse(final int response) throws RemoteException { + mCode = response; + } + + @Override + public void appRequest(final int request) throws RemoteException { + mCode = request; + } + + // This is only @Override on R- + public void logEvent(int eventId, String packageName) throws RemoteException { + mCode = eventId; + mPackageName = packageName; + } + } + + private interface TestFunctor { + void useCaptivePortal(CaptivePortal o); + } + + private MyCaptivePortalImpl runCaptivePortalTest(TestFunctor f) { + final MyCaptivePortalImpl cp = new MyCaptivePortalImpl(); + f.useCaptivePortal(new CaptivePortal(cp.asBinder())); + return cp; + } + + @Test + public void testReportCaptivePortalDismissed() { + final MyCaptivePortalImpl result = + runCaptivePortalTest(c -> c.reportCaptivePortalDismissed()); + assertEquals(result.mCode, CaptivePortal.APP_RETURN_DISMISSED); + } + + @Test + public void testIgnoreNetwork() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.ignoreNetwork()); + assertEquals(result.mCode, CaptivePortal.APP_RETURN_UNWANTED); + } + + @Test + public void testUseNetwork() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.useNetwork()); + assertEquals(result.mCode, CaptivePortal.APP_RETURN_WANTED_AS_IS); + } + + @IgnoreUpTo(Build.VERSION_CODES.Q) + @Test + public void testReevaluateNetwork() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.reevaluateNetwork()); + assertEquals(result.mCode, CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED); + } + + @IgnoreUpTo(Build.VERSION_CODES.R) + @Test + public void testLogEvent() { + /** + * From S testLogEvent is expected to do nothing but shouldn't crash (the API + * logEvent has been deprecated). + */ + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.logEvent( + 0, + TEST_PACKAGE_NAME)); + } + + @IgnoreAfter(Build.VERSION_CODES.R) + @Test + public void testLogEvent_UntilR() { + final MyCaptivePortalImpl result = runCaptivePortalTest(c -> c.logEvent( + 42, TEST_PACKAGE_NAME)); + assertEquals(result.mCode, 42); + assertEquals(result.mPackageName, TEST_PACKAGE_NAME); + } +} diff --git a/tests/common/java/android/net/DependenciesTest.java b/tests/common/java/android/net/DependenciesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ac1c28a45462b76c7e9d063e513f657a9923779c --- /dev/null +++ b/tests/common/java/android/net/DependenciesTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +/** + * A simple class that tests dependencies to java standard tools from the + * Network stack. These tests are not meant to be comprehensive tests of + * the relevant APIs : such tests belong in the relevant test suite for + * these dependencies. Instead, this just makes sure coverage is present + * by calling the methods in the exact way (or a representative way of how) + * they are called in the network stack. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DependenciesTest { + // Used to in ipmemorystore's RegularMaintenanceJobService to convert + // 24 hours into seconds + @Test + public void testTimeUnit() { + final int hours = 24; + final long inSeconds = TimeUnit.HOURS.toMillis(hours); + assertEquals(inSeconds, hours * 60 * 60 * 1000); + } + + private byte[] makeTrivialArray(final int size) { + final byte[] src = new byte[size]; + for (int i = 0; i < size; ++i) { + src[i] = (byte) i; + } + return src; + } + + // Used in ApfFilter to find an IP address from a byte array + @Test + public void testArrays() { + final int size = 128; + final byte[] src = makeTrivialArray(size); + + // Test copy + final int copySize = 16; + final int offset = 24; + final byte[] expected = new byte[copySize]; + for (int i = 0; i < copySize; ++i) { + expected[i] = (byte) (offset + i); + } + + final byte[] copy = Arrays.copyOfRange(src, offset, offset + copySize); + assertArrayEquals(expected, copy); + assertArrayEquals(new byte[0], Arrays.copyOfRange(src, size, size)); + } + + // Used mainly in the Dhcp code + @Test + public void testCopyOf() { + final byte[] src = makeTrivialArray(128); + final byte[] copy = Arrays.copyOf(src, src.length); + assertArrayEquals(src, copy); + assertFalse(src == copy); + + assertArrayEquals(new byte[0], Arrays.copyOf(src, 0)); + + final int excess = 16; + final byte[] biggerCopy = Arrays.copyOf(src, src.length + excess); + for (int i = src.length; i < src.length + excess; ++i) { + assertEquals(0, biggerCopy[i]); + } + for (int i = src.length - 1; i >= 0; --i) { + assertEquals(src[i], biggerCopy[i]); + } + } + + // Used mainly in DnsUtils but also various other places + @Test + public void testAsList() { + final int size = 24; + final Object[] src = new Object[size]; + final ArrayList<Object> expected = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + final Object o = new Object(); + src[i] = o; + expected.add(o); + } + assertEquals(expected, Arrays.asList(src)); + } +} diff --git a/tests/common/java/android/net/DhcpInfoTest.java b/tests/common/java/android/net/DhcpInfoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ab4726bab573bcd540b099cdd0ba022f9ab33288 --- /dev/null +++ b/tests/common/java/android/net/DhcpInfoTest.java @@ -0,0 +1,112 @@ +/* + * 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; + +import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTL; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.ParcelUtils.parcelingRoundTrip; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.annotation.Nullable; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.InetAddress; + +@RunWith(AndroidJUnit4.class) +public class DhcpInfoTest { + private static final String STR_ADDR1 = "255.255.255.255"; + private static final String STR_ADDR2 = "127.0.0.1"; + private static final String STR_ADDR3 = "192.168.1.1"; + private static final String STR_ADDR4 = "192.168.1.0"; + private static final int LEASE_TIME = 9999; + + private int ipToInteger(String ipString) throws Exception { + return inet4AddressToIntHTL((Inet4Address) InetAddress.getByName(ipString)); + } + + private DhcpInfo createDhcpInfoObject() throws Exception { + final DhcpInfo dhcpInfo = new DhcpInfo(); + dhcpInfo.ipAddress = ipToInteger(STR_ADDR1); + dhcpInfo.gateway = ipToInteger(STR_ADDR2); + dhcpInfo.netmask = ipToInteger(STR_ADDR3); + dhcpInfo.dns1 = ipToInteger(STR_ADDR4); + dhcpInfo.dns2 = ipToInteger(STR_ADDR4); + dhcpInfo.serverAddress = ipToInteger(STR_ADDR2); + dhcpInfo.leaseDuration = LEASE_TIME; + return dhcpInfo; + } + + @Test + public void testConstructor() { + new DhcpInfo(); + } + + @Test + public void testToString() throws Exception { + final String expectedDefault = "ipaddr 0.0.0.0 gateway 0.0.0.0 netmask 0.0.0.0 " + + "dns1 0.0.0.0 dns2 0.0.0.0 DHCP server 0.0.0.0 lease 0 seconds"; + + DhcpInfo dhcpInfo = new DhcpInfo(); + + // Test default string. + assertEquals(expectedDefault, dhcpInfo.toString()); + + dhcpInfo = createDhcpInfoObject(); + + final String expected = "ipaddr " + STR_ADDR1 + " gateway " + STR_ADDR2 + " netmask " + + STR_ADDR3 + " dns1 " + STR_ADDR4 + " dns2 " + STR_ADDR4 + " DHCP server " + + STR_ADDR2 + " lease " + LEASE_TIME + " seconds"; + // Test with new values + assertEquals(expected, dhcpInfo.toString()); + } + + private boolean dhcpInfoEquals(@Nullable DhcpInfo left, @Nullable DhcpInfo right) { + if (left == null && right == null) return true; + + if (left == null || right == null) return false; + + return left.ipAddress == right.ipAddress + && left.gateway == right.gateway + && left.netmask == right.netmask + && left.dns1 == right.dns1 + && left.dns2 == right.dns2 + && left.serverAddress == right.serverAddress + && left.leaseDuration == right.leaseDuration; + } + + @Test + public void testParcelDhcpInfo() throws Exception { + // Cannot use assertParcelSane() here because this requires .equals() to work as + // defined, but DhcpInfo has a different legacy behavior that we cannot change. + final DhcpInfo dhcpInfo = createDhcpInfoObject(); + assertFieldCountEquals(7, DhcpInfo.class); + + final DhcpInfo dhcpInfoRoundTrip = parcelingRoundTrip(dhcpInfo); + assertTrue(dhcpInfoEquals(null, null)); + assertFalse(dhcpInfoEquals(null, dhcpInfoRoundTrip)); + assertFalse(dhcpInfoEquals(dhcpInfo, null)); + assertTrue(dhcpInfoEquals(dhcpInfo, dhcpInfoRoundTrip)); + } +} diff --git a/tests/common/java/android/net/IpPrefixTest.java b/tests/common/java/android/net/IpPrefixTest.java new file mode 100644 index 0000000000000000000000000000000000000000..50ecb428359e2130a341b0d7a708889c5e086195 --- /dev/null +++ b/tests/common/java/android/net/IpPrefixTest.java @@ -0,0 +1,374 @@ +/* + * 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 android.net; + +import static com.android.testutils.MiscAsserts.assertEqualBothWays; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.Random; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpPrefixTest { + + private static InetAddress address(String addr) { + return InetAddress.parseNumericAddress(addr); + } + + // Explicitly cast everything to byte because "error: possible loss of precision". + private static final byte[] IPV4_BYTES = { (byte) 192, (byte) 0, (byte) 2, (byte) 4}; + private static final byte[] IPV6_BYTES = { + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, + (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef, + (byte) 0x0f, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xa0 + }; + + @Test + public void testConstructor() { + IpPrefix p; + try { + p = new IpPrefix((byte[]) null, 9); + fail("Expected NullPointerException: null byte array"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix((InetAddress) null, 10); + fail("Expected NullPointerException: null InetAddress"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix((String) null); + fail("Expected NullPointerException: null String"); + } catch (RuntimeException expected) { } + + + try { + byte[] b2 = {1, 2, 3, 4, 5}; + p = new IpPrefix(b2, 29); + fail("Expected IllegalArgumentException: invalid array length"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1.2.3.4"); + fail("Expected IllegalArgumentException: no prefix length"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1.2.3.4/"); + fail("Expected IllegalArgumentException: empty prefix length"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("foo/32"); + fail("Expected IllegalArgumentException: invalid address"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1/32"); + fail("Expected IllegalArgumentException: deprecated IPv4 format"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("1.2.3.256/32"); + fail("Expected IllegalArgumentException: invalid IPv4 address"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("foo/32"); + fail("Expected IllegalArgumentException: non-address"); + } catch (IllegalArgumentException expected) { } + + try { + p = new IpPrefix("f00:::/32"); + fail("Expected IllegalArgumentException: invalid IPv6 address"); + } catch (IllegalArgumentException expected) { } + + p = new IpPrefix("/64"); + assertEquals("::/64", p.toString()); + + p = new IpPrefix("/128"); + assertEquals("::1/128", p.toString()); + + p = new IpPrefix("[2001:db8::123]/64"); + assertEquals("2001:db8::/64", p.toString()); + } + + @Test + public void testTruncation() { + IpPrefix p; + + p = new IpPrefix(IPV4_BYTES, 32); + assertEquals("192.0.2.4/32", p.toString()); + + p = new IpPrefix(IPV4_BYTES, 29); + assertEquals("192.0.2.0/29", p.toString()); + + p = new IpPrefix(IPV4_BYTES, 8); + assertEquals("192.0.0.0/8", p.toString()); + + p = new IpPrefix(IPV4_BYTES, 0); + assertEquals("0.0.0.0/0", p.toString()); + + try { + p = new IpPrefix(IPV4_BYTES, 33); + fail("Expected IllegalArgumentException: invalid prefix length"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix(IPV4_BYTES, 128); + fail("Expected IllegalArgumentException: invalid prefix length"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix(IPV4_BYTES, -1); + fail("Expected IllegalArgumentException: negative prefix length"); + } catch (RuntimeException expected) { } + + p = new IpPrefix(IPV6_BYTES, 128); + assertEquals("2001:db8:dead:beef:f00::a0/128", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 122); + assertEquals("2001:db8:dead:beef:f00::80/122", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 64); + assertEquals("2001:db8:dead:beef::/64", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 3); + assertEquals("2000::/3", p.toString()); + + p = new IpPrefix(IPV6_BYTES, 0); + assertEquals("::/0", p.toString()); + + try { + p = new IpPrefix(IPV6_BYTES, -1); + fail("Expected IllegalArgumentException: negative prefix length"); + } catch (RuntimeException expected) { } + + try { + p = new IpPrefix(IPV6_BYTES, 129); + fail("Expected IllegalArgumentException: negative prefix length"); + } catch (RuntimeException expected) { } + + } + + @Test + public void testEquals() { + IpPrefix p1, p2; + + p1 = new IpPrefix("192.0.2.251/23"); + p2 = new IpPrefix(new byte[]{(byte) 192, (byte) 0, (byte) 2, (byte) 251}, 23); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("192.0.2.5/23"); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("192.0.2.5/24"); + assertNotEqualEitherWay(p1, p2); + + p1 = new IpPrefix("192.0.4.5/23"); + assertNotEqualEitherWay(p1, p2); + + + p1 = new IpPrefix("2001:db8:dead:beef:f00::80/122"); + p2 = new IpPrefix(IPV6_BYTES, 122); + assertEquals("2001:db8:dead:beef:f00::80/122", p2.toString()); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("2001:db8:dead:beef:f00::bf/122"); + assertEqualBothWays(p1, p2); + + p1 = new IpPrefix("2001:db8:dead:beef:f00::8:0/123"); + assertNotEqualEitherWay(p1, p2); + + p1 = new IpPrefix("2001:db8:dead:beef::/122"); + assertNotEqualEitherWay(p1, p2); + + // 192.0.2.4/32 != c000:0204::/32. + byte[] ipv6bytes = new byte[16]; + System.arraycopy(IPV4_BYTES, 0, ipv6bytes, 0, IPV4_BYTES.length); + p1 = new IpPrefix(ipv6bytes, 32); + assertEqualBothWays(p1, new IpPrefix("c000:0204::/32")); + + p2 = new IpPrefix(IPV4_BYTES, 32); + assertNotEqualEitherWay(p1, p2); + } + + @Test + public void testContainsInetAddress() { + IpPrefix p = new IpPrefix("2001:db8:f00::ace:d00d/127"); + assertTrue(p.contains(address("2001:db8:f00::ace:d00c"))); + assertTrue(p.contains(address("2001:db8:f00::ace:d00d"))); + assertFalse(p.contains(address("2001:db8:f00::ace:d00e"))); + assertFalse(p.contains(address("2001:db8:f00::bad:d00d"))); + assertFalse(p.contains(address("2001:4868:4860::8888"))); + assertFalse(p.contains(address("8.8.8.8"))); + + p = new IpPrefix("192.0.2.0/23"); + assertTrue(p.contains(address("192.0.2.43"))); + assertTrue(p.contains(address("192.0.3.21"))); + assertFalse(p.contains(address("192.0.0.21"))); + assertFalse(p.contains(address("8.8.8.8"))); + assertFalse(p.contains(address("2001:4868:4860::8888"))); + + IpPrefix ipv6Default = new IpPrefix("::/0"); + assertTrue(ipv6Default.contains(address("2001:db8::f00"))); + assertFalse(ipv6Default.contains(address("192.0.2.1"))); + + IpPrefix ipv4Default = new IpPrefix("0.0.0.0/0"); + assertTrue(ipv4Default.contains(address("255.255.255.255"))); + assertTrue(ipv4Default.contains(address("192.0.2.1"))); + assertFalse(ipv4Default.contains(address("2001:db8::f00"))); + } + + @Test + public void testContainsIpPrefix() { + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("0.0.0.0/0"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/0"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/8"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/24"))); + assertTrue(new IpPrefix("0.0.0.0/0").containsPrefix(new IpPrefix("1.2.3.4/23"))); + + assertTrue(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("1.2.3.4/8"))); + assertTrue(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("1.254.12.9/8"))); + assertTrue(new IpPrefix("1.2.3.4/21").containsPrefix(new IpPrefix("1.2.3.4/21"))); + assertTrue(new IpPrefix("1.2.3.4/32").containsPrefix(new IpPrefix("1.2.3.4/32"))); + + assertTrue(new IpPrefix("1.2.3.4/20").containsPrefix(new IpPrefix("1.2.3.0/24"))); + + assertFalse(new IpPrefix("1.2.3.4/32").containsPrefix(new IpPrefix("1.2.3.5/32"))); + assertFalse(new IpPrefix("1.2.3.4/8").containsPrefix(new IpPrefix("2.2.3.4/8"))); + assertFalse(new IpPrefix("0.0.0.0/16").containsPrefix(new IpPrefix("0.0.0.0/15"))); + assertFalse(new IpPrefix("100.0.0.0/8").containsPrefix(new IpPrefix("99.0.0.0/8"))); + + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("::/0"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/1"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("3d8a:661:a0::770/8"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/8"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/64"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/113"))); + assertTrue(new IpPrefix("::/0").containsPrefix(new IpPrefix("2001:db8::f00/128"))); + + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/64"))); + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/120"))); + assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/32"))); + assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/64").containsPrefix( + new IpPrefix("2006:db8:f00::ace:d00d/96"))); + + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/128").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00d/128"))); + assertTrue(new IpPrefix("2001:db8:f00::ace:d00d/100").containsPrefix( + new IpPrefix("2001:db8:f00::ace:ccaf/110"))); + + assertFalse(new IpPrefix("2001:db8:f00::ace:d00d/128").containsPrefix( + new IpPrefix("2001:db8:f00::ace:d00e/128"))); + assertFalse(new IpPrefix("::/30").containsPrefix(new IpPrefix("::/29"))); + } + + @Test + public void testHashCode() { + IpPrefix p = new IpPrefix(new byte[4], 0); + Random random = new Random(); + for (int i = 0; i < 100; i++) { + final IpPrefix oldP = p; + if (random.nextBoolean()) { + // IPv4. + byte[] b = new byte[4]; + random.nextBytes(b); + p = new IpPrefix(b, random.nextInt(33)); + } else { + // IPv6. + byte[] b = new byte[16]; + random.nextBytes(b); + p = new IpPrefix(b, random.nextInt(129)); + } + if (p.equals(oldP)) { + assertEquals(p.hashCode(), oldP.hashCode()); + } + if (p.hashCode() != oldP.hashCode()) { + assertNotEquals(p, oldP); + } + } + } + + @Test + public void testHashCodeIsNotConstant() { + IpPrefix[] prefixes = { + new IpPrefix("2001:db8:f00::ace:d00d/127"), + new IpPrefix("192.0.2.0/23"), + new IpPrefix("::/0"), + new IpPrefix("0.0.0.0/0"), + }; + for (int i = 0; i < prefixes.length; i++) { + for (int j = i + 1; j < prefixes.length; j++) { + assertNotEquals(prefixes[i].hashCode(), prefixes[j].hashCode()); + } + } + } + + @Test + public void testMappedAddressesAreBroken() { + // 192.0.2.0/24 != ::ffff:c000:0204/120, but because we use InetAddress, + // we are unable to comprehend that. + byte[] ipv6bytes = { + (byte) 0, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff, + (byte) 192, (byte) 0, (byte) 2, (byte) 0}; + IpPrefix p = new IpPrefix(ipv6bytes, 120); + assertEquals(16, p.getRawAddress().length); // Fine. + assertArrayEquals(ipv6bytes, p.getRawAddress()); // Fine. + + // Broken. + assertEquals("192.0.2.0/120", p.toString()); + assertEquals(InetAddress.parseNumericAddress("192.0.2.0"), p.getAddress()); + } + + @Test + public void testParceling() { + IpPrefix p; + + p = new IpPrefix("2001:4860:db8::/64"); + assertParcelingIsLossless(p); + assertTrue(p.isIPv6()); + + p = new IpPrefix("192.0.2.0/25"); + assertParcelingIsLossless(p); + assertTrue(p.isIPv4()); + + assertFieldCountEquals(2, IpPrefix.class); + } +} diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f464ec6cf0e549ffe0c980c8b396f86d696a7df1 --- /dev/null +++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.InvalidPacketException.ERROR_INVALID_IP_ADDRESS +import android.net.InvalidPacketException.ERROR_INVALID_PORT +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import java.net.InetAddress +import java.util.Arrays +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class KeepalivePacketDataTest { + @Rule @JvmField + val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() + + private val INVALID_PORT = 65537 + private val TEST_DST_PORT = 4244 + private val TEST_SRC_PORT = 4243 + + private val TESTBYTES = byteArrayOf(12, 31, 22, 44) + private val TEST_SRC_ADDRV4 = "198.168.0.2".address() + private val TEST_DST_ADDRV4 = "198.168.0.1".address() + private val TEST_ADDRV6 = "2001:db8::1".address() + + private fun String.address() = InetAddresses.parseNumericAddress(this) + + // Add for test because constructor of KeepalivePacketData is protected. + private inner class TestKeepalivePacketData( + srcAddress: InetAddress? = TEST_SRC_ADDRV4, + srcPort: Int = TEST_SRC_PORT, + dstAddress: InetAddress? = TEST_DST_ADDRV4, + dstPort: Int = TEST_DST_PORT, + data: ByteArray = TESTBYTES + ) : KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testConstructor() { + var data: TestKeepalivePacketData + + try { + data = TestKeepalivePacketData(srcAddress = null) + fail("Null src address should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + data = TestKeepalivePacketData(dstAddress = null) + fail("Null dst address should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + data = TestKeepalivePacketData(dstAddress = TEST_ADDRV6) + fail("Ip family mismatched should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + data = TestKeepalivePacketData(srcPort = INVALID_PORT) + fail("Invalid srcPort should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_PORT) + } + + try { + data = TestKeepalivePacketData(dstPort = INVALID_PORT) + fail("Invalid dstPort should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_PORT) + } + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testSrcAddress() = assertEquals(TEST_SRC_ADDRV4, TestKeepalivePacketData().srcAddress) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testDstAddress() = assertEquals(TEST_DST_ADDRV4, TestKeepalivePacketData().dstAddress) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testSrcPort() = assertEquals(TEST_SRC_PORT, TestKeepalivePacketData().srcPort) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testDstPort() = assertEquals(TEST_DST_PORT, TestKeepalivePacketData().dstPort) + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testPacket() = assertTrue(Arrays.equals(TESTBYTES, TestKeepalivePacketData().packet)) +} \ No newline at end of file diff --git a/tests/common/java/android/net/LinkAddressTest.java b/tests/common/java/android/net/LinkAddressTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2cf3cf9c11da43072e5aaa48f8a5426f85cc9619 --- /dev/null +++ b/tests/common/java/android/net/LinkAddressTest.java @@ -0,0 +1,518 @@ +/* + * 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; + +import static android.system.OsConstants.IFA_F_DADFAILED; +import static android.system.OsConstants.IFA_F_DEPRECATED; +import static android.system.OsConstants.IFA_F_OPTIMISTIC; +import static android.system.OsConstants.IFA_F_PERMANENT; +import static android.system.OsConstants.IFA_F_TEMPORARY; +import static android.system.OsConstants.IFA_F_TENTATIVE; +import static android.system.OsConstants.RT_SCOPE_HOST; +import static android.system.OsConstants.RT_SCOPE_LINK; +import static android.system.OsConstants.RT_SCOPE_SITE; +import static android.system.OsConstants.RT_SCOPE_UNIVERSE; + +import static com.android.testutils.MiscAsserts.assertEqualBothWays; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Build; +import android.os.SystemClock; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LinkAddressTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final String V4 = "192.0.2.1"; + private static final String V6 = "2001:db8::1"; + private static final InetAddress V4_ADDRESS = InetAddresses.parseNumericAddress(V4); + private static final InetAddress V6_ADDRESS = InetAddresses.parseNumericAddress(V6); + + @Test + public void testConstants() { + // RT_SCOPE_UNIVERSE = 0, but all the other constants should be nonzero. + assertNotEquals(0, RT_SCOPE_HOST); + assertNotEquals(0, RT_SCOPE_LINK); + assertNotEquals(0, RT_SCOPE_SITE); + + assertNotEquals(0, IFA_F_DEPRECATED); + assertNotEquals(0, IFA_F_PERMANENT); + assertNotEquals(0, IFA_F_TENTATIVE); + } + + @Test + public void testConstructors() throws SocketException { + LinkAddress address; + + // Valid addresses work as expected. + address = new LinkAddress(V4_ADDRESS, 25); + assertEquals(V4_ADDRESS, address.getAddress()); + assertEquals(25, address.getPrefixLength()); + assertEquals(0, address.getFlags()); + assertEquals(RT_SCOPE_UNIVERSE, address.getScope()); + assertTrue(address.isIpv4()); + + address = new LinkAddress(V6_ADDRESS, 127); + assertEquals(V6_ADDRESS, address.getAddress()); + assertEquals(127, address.getPrefixLength()); + assertEquals(0, address.getFlags()); + assertEquals(RT_SCOPE_UNIVERSE, address.getScope()); + assertTrue(address.isIpv6()); + + // Nonsensical flags/scopes or combinations thereof are acceptable. + address = new LinkAddress(V6 + "/64", IFA_F_DEPRECATED | IFA_F_PERMANENT, RT_SCOPE_LINK); + assertEquals(V6_ADDRESS, address.getAddress()); + assertEquals(64, address.getPrefixLength()); + assertEquals(IFA_F_DEPRECATED | IFA_F_PERMANENT, address.getFlags()); + assertEquals(RT_SCOPE_LINK, address.getScope()); + assertTrue(address.isIpv6()); + + address = new LinkAddress(V4 + "/23", 123, 456); + assertEquals(V4_ADDRESS, address.getAddress()); + assertEquals(23, address.getPrefixLength()); + assertEquals(123, address.getFlags()); + assertEquals(456, address.getScope()); + assertTrue(address.isIpv4()); + + address = new LinkAddress("/64", 1 /* flags */, 2 /* scope */); + assertEquals(Inet6Address.LOOPBACK, address.getAddress()); + assertEquals(64, address.getPrefixLength()); + assertEquals(1, address.getFlags()); + assertEquals(2, address.getScope()); + assertTrue(address.isIpv6()); + + address = new LinkAddress("[2001:db8::123]/64", 3 /* flags */, 4 /* scope */); + assertEquals(InetAddresses.parseNumericAddress("2001:db8::123"), address.getAddress()); + assertEquals(64, address.getPrefixLength()); + assertEquals(3, address.getFlags()); + assertEquals(4, address.getScope()); + assertTrue(address.isIpv6()); + + // InterfaceAddress doesn't have a constructor. Fetch some from an interface. + List<InterfaceAddress> addrs = NetworkInterface.getByName("lo").getInterfaceAddresses(); + + // We expect to find 127.0.0.1/8 and ::1/128, in any order. + LinkAddress ipv4Loopback, ipv6Loopback; + assertEquals(2, addrs.size()); + if (addrs.get(0).getAddress() instanceof Inet4Address) { + ipv4Loopback = new LinkAddress(addrs.get(0)); + ipv6Loopback = new LinkAddress(addrs.get(1)); + } else { + ipv4Loopback = new LinkAddress(addrs.get(1)); + ipv6Loopback = new LinkAddress(addrs.get(0)); + } + + assertEquals(InetAddresses.parseNumericAddress("127.0.0.1"), ipv4Loopback.getAddress()); + assertEquals(8, ipv4Loopback.getPrefixLength()); + + assertEquals(InetAddresses.parseNumericAddress("::1"), ipv6Loopback.getAddress()); + assertEquals(128, ipv6Loopback.getPrefixLength()); + + // Null addresses are rejected. + try { + address = new LinkAddress(null, 24); + fail("Null InetAddress should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress((String) null, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("Null string should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress((InterfaceAddress) null); + fail("Null string should cause NullPointerException"); + } catch(NullPointerException expected) {} + + // Invalid prefix lengths are rejected. + try { + address = new LinkAddress(V4_ADDRESS, -1); + fail("Negative IPv4 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V6_ADDRESS, -1); + fail("Negative IPv6 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V4_ADDRESS, 33); + fail("/33 IPv4 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V4 + "/33", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("/33 IPv4 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + + try { + address = new LinkAddress(V6_ADDRESS, 129, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("/129 IPv6 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress(V6 + "/129", IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + fail("/129 IPv6 prefix length should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + // Multicast addresses are rejected. + try { + address = new LinkAddress("224.0.0.2/32"); + fail("IPv4 multicast address should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + + try { + address = new LinkAddress("ff02::1/128"); + fail("IPv6 multicast address should cause IllegalArgumentException"); + } catch(IllegalArgumentException expected) {} + } + + @Test + public void testAddressScopes() { + assertEquals(RT_SCOPE_HOST, new LinkAddress("::/128").getScope()); + assertEquals(RT_SCOPE_HOST, new LinkAddress("0.0.0.0/32").getScope()); + + assertEquals(RT_SCOPE_LINK, new LinkAddress("::1/128").getScope()); + assertEquals(RT_SCOPE_LINK, new LinkAddress("127.0.0.5/8").getScope()); + assertEquals(RT_SCOPE_LINK, new LinkAddress("fe80::ace:d00d/64").getScope()); + assertEquals(RT_SCOPE_LINK, new LinkAddress("169.254.5.12/16").getScope()); + + assertEquals(RT_SCOPE_SITE, new LinkAddress("fec0::dead/64").getScope()); + + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("10.1.2.3/21").getScope()); + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("192.0.2.1/25").getScope()); + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("2001:db8::/64").getScope()); + assertEquals(RT_SCOPE_UNIVERSE, new LinkAddress("5000::/127").getScope()); + } + + private void assertIsSameAddressAs(LinkAddress l1, LinkAddress l2) { + assertTrue(l1 + " unexpectedly does not have same address as " + l2, + l1.isSameAddressAs(l2)); + assertTrue(l2 + " unexpectedly does not have same address as " + l1, + l2.isSameAddressAs(l1)); + } + + private void assertIsNotSameAddressAs(LinkAddress l1, LinkAddress l2) { + assertFalse(l1 + " unexpectedly has same address as " + l2, + l1.isSameAddressAs(l2)); + assertFalse(l2 + " unexpectedly has same address as " + l1, + l1.isSameAddressAs(l2)); + } + + @Test + public void testEqualsAndSameAddressAs() { + LinkAddress l1, l2, l3; + + l1 = new LinkAddress("2001:db8::1/64"); + l2 = new LinkAddress("2001:db8::1/64"); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress("2001:db8::1/65"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + l2 = new LinkAddress("2001:db8::2/64"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + + l1 = new LinkAddress("192.0.2.1/24"); + l2 = new LinkAddress("192.0.2.1/24"); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress("192.0.2.1/23"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + l2 = new LinkAddress("192.0.2.2/24"); + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + + // Check equals() and isSameAddressAs() on identical addresses with different flags. + l1 = new LinkAddress(V6_ADDRESS, 64); + l2 = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_UNIVERSE); + assertNotEqualEitherWay(l1, l2); + assertIsSameAddressAs(l1, l2); + + // Check equals() and isSameAddressAs() on identical addresses with different scope. + l1 = new LinkAddress(V4_ADDRESS, 24); + l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_UNIVERSE); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + + l2 = new LinkAddress(V4_ADDRESS, 24, 0, RT_SCOPE_HOST); + assertNotEqualEitherWay(l1, l2); + assertIsSameAddressAs(l1, l2); + + // Addresses with the same start or end bytes aren't equal between families. + l1 = new LinkAddress("32.1.13.184/24"); + l2 = new LinkAddress("2001:db8::1/24"); + l3 = new LinkAddress("::2001:db8/24"); + + byte[] ipv4Bytes = l1.getAddress().getAddress(); + byte[] l2FirstIPv6Bytes = Arrays.copyOf(l2.getAddress().getAddress(), 4); + byte[] l3LastIPv6Bytes = Arrays.copyOfRange(l3.getAddress().getAddress(), 12, 16); + assertTrue(Arrays.equals(ipv4Bytes, l2FirstIPv6Bytes)); + assertTrue(Arrays.equals(ipv4Bytes, l3LastIPv6Bytes)); + + assertNotEqualEitherWay(l1, l2); + assertIsNotSameAddressAs(l1, l2); + + assertNotEqualEitherWay(l1, l3); + assertIsNotSameAddressAs(l1, l3); + + // Because we use InetAddress, an IPv4 address is equal to its IPv4-mapped address. + // TODO: Investigate fixing this. + String addressString = V4 + "/24"; + l1 = new LinkAddress(addressString); + l2 = new LinkAddress("::ffff:" + addressString); + assertEqualBothWays(l1, l2); + assertIsSameAddressAs(l1, l2); + } + + @Test + public void testHashCode() { + LinkAddress l1, l2; + + l1 = new LinkAddress(V4_ADDRESS, 23); + l2 = new LinkAddress(V4_ADDRESS, 23, 0, RT_SCOPE_HOST); + assertNotEquals(l1.hashCode(), l2.hashCode()); + + l1 = new LinkAddress(V6_ADDRESS, 128); + l2 = new LinkAddress(V6_ADDRESS, 128, IFA_F_TENTATIVE, RT_SCOPE_UNIVERSE); + assertNotEquals(l1.hashCode(), l2.hashCode()); + } + + @Test + public void testParceling() { + LinkAddress l; + + l = new LinkAddress(V6_ADDRESS, 64, 123, 456); + assertParcelingIsLossless(l); + + l = new LinkAddress(V4 + "/28", IFA_F_PERMANENT, RT_SCOPE_LINK); + assertParcelingIsLossless(l); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testLifetimeParceling() { + final LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 123, 456, 1L, 3600000L); + assertParcelingIsLossless(l); + } + + @Test @IgnoreAfter(Build.VERSION_CODES.Q) + public void testFieldCount_Q() { + assertFieldCountEquals(4, LinkAddress.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testFieldCount() { + // Make sure any new field is covered by the above parceling tests when changing this number + assertFieldCountEquals(6, LinkAddress.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testDeprecationTime() { + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + LinkAddress.LIFETIME_UNKNOWN, 100000L); + fail("Only one time provided should cause exception"); + } catch (IllegalArgumentException expected) { } + + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + 200000L, 100000L); + fail("deprecation time later than expiration time should cause exception"); + } catch (IllegalArgumentException expected) { } + + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + -2, 100000L); + fail("negative deprecation time should cause exception"); + } catch (IllegalArgumentException expected) { } + + LinkAddress addr = new LinkAddress(V6_ADDRESS, 64, 0, 456, 100000L, 200000L); + assertEquals(100000L, addr.getDeprecationTime()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testExpirationTime() { + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + 200000L, LinkAddress.LIFETIME_UNKNOWN); + fail("Only one time provided should cause exception"); + } catch (IllegalArgumentException expected) { } + + try { + new LinkAddress(V6_ADDRESS, 64, 0, 456, + 100000L, -2); + fail("negative expiration time should cause exception"); + } catch (IllegalArgumentException expected) { } + + LinkAddress addr = new LinkAddress(V6_ADDRESS, 64, 0, 456, 100000L, 200000L); + assertEquals(200000L, addr.getExpirationTime()); + } + + @Test + public void testGetFlags() { + LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 123, RT_SCOPE_HOST); + assertEquals(123, l.getFlags()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testGetFlags_Deprecation() { + // Test if deprecated bit was added/remove automatically based on the provided deprecation + // time + LinkAddress l = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_HOST, + 1L, LinkAddress.LIFETIME_PERMANENT); + // Check if the flag is added automatically. + assertTrue((l.getFlags() & IFA_F_DEPRECATED) != 0); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_HOST, + SystemClock.elapsedRealtime() + 100000L, LinkAddress.LIFETIME_PERMANENT); + // Check if the flag is removed automatically. + assertTrue((l.getFlags() & IFA_F_DEPRECATED) == 0); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, RT_SCOPE_HOST, + LinkAddress.LIFETIME_PERMANENT, LinkAddress.LIFETIME_PERMANENT); + // Check if the permanent flag is added. + assertTrue((l.getFlags() & IFA_F_PERMANENT) != 0); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_PERMANENT, RT_SCOPE_HOST, + 1000L, SystemClock.elapsedRealtime() + 100000L); + // Check if the permanent flag is removed + assertTrue((l.getFlags() & IFA_F_PERMANENT) == 0); + } + + private void assertGlobalPreferred(LinkAddress l, String msg) { + assertTrue(msg, l.isGlobalPreferred()); + } + + private void assertNotGlobalPreferred(LinkAddress l, String msg) { + assertFalse(msg, l.isGlobalPreferred()); + } + + @Test + public void testIsGlobalPreferred() { + LinkAddress l; + + l = new LinkAddress(V4_ADDRESS, 32, 0, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v4,global,noflags"); + + l = new LinkAddress("10.10.1.7/23", 0, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v4-rfc1918,global,noflags"); + + l = new LinkAddress("10.10.1.7/23", 0, RT_SCOPE_SITE); + assertNotGlobalPreferred(l, "v4-rfc1918,site-local,noflags"); + + l = new LinkAddress("127.0.0.7/8", 0, RT_SCOPE_HOST); + assertNotGlobalPreferred(l, "v4-localhost,node-local,noflags"); + + l = new LinkAddress(V6_ADDRESS, 64, 0, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,noflags"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,permanent"); + + // IPv6 ULAs are not acceptable "global preferred" addresses. + l = new LinkAddress("fc12::1/64", 0, RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,ula1,noflags"); + + l = new LinkAddress("fd34::1/64", 0, RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,ula2,noflags"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,tempaddr"); + + l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_DADFAILED), + RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,global,tempaddr+dadfailed"); + + l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_DEPRECATED), + RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,global,tempaddr+deprecated"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_SITE); + assertNotGlobalPreferred(l, "v6,site-local,tempaddr"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_LINK); + assertNotGlobalPreferred(l, "v6,link-local,tempaddr"); + + l = new LinkAddress(V6_ADDRESS, 64, IFA_F_TEMPORARY, RT_SCOPE_HOST); + assertNotGlobalPreferred(l, "v6,node-local,tempaddr"); + + l = new LinkAddress("::1/128", IFA_F_PERMANENT, RT_SCOPE_HOST); + assertNotGlobalPreferred(l, "v6-localhost,node-local,permanent"); + + l = new LinkAddress(V6_ADDRESS, 64, (IFA_F_TEMPORARY|IFA_F_TENTATIVE), + RT_SCOPE_UNIVERSE); + assertNotGlobalPreferred(l, "v6,global,tempaddr+tentative"); + + l = new LinkAddress(V6_ADDRESS, 64, + (IFA_F_TEMPORARY|IFA_F_TENTATIVE|IFA_F_OPTIMISTIC), + RT_SCOPE_UNIVERSE); + assertGlobalPreferred(l, "v6,global,tempaddr+optimistic"); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testIsGlobalPreferred_DeprecatedInFuture() { + final LinkAddress l = new LinkAddress(V6_ADDRESS, 64, IFA_F_DEPRECATED, + RT_SCOPE_UNIVERSE, SystemClock.elapsedRealtime() + 100000, + SystemClock.elapsedRealtime() + 200000); + // Although the deprecated bit is set, but the deprecation time is in the future, test + // if the flag is removed automatically. + assertGlobalPreferred(l, "v6,global,tempaddr+deprecated in the future"); + } +} diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..550953d0612df9b435c79f144528e1afdafac308 --- /dev/null +++ b/tests/common/java/android/net/LinkPropertiesTest.java @@ -0,0 +1,1271 @@ +/* + * 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; + +import static android.net.RouteInfo.RTN_THROW; +import static android.net.RouteInfo.RTN_UNICAST; +import static android.net.RouteInfo.RTN_UNREACHABLE; + +import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; +import static com.android.testutils.ParcelUtils.parcelingRoundTrip; + +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.Assert.fail; + +import android.net.LinkProperties.ProvisioningChange; +import android.os.Build; +import android.system.OsConstants; +import android.util.ArraySet; + +import androidx.core.os.BuildCompat; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.LinkPropertiesUtils.CompareResult; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LinkPropertiesTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final InetAddress ADDRV4 = address("75.208.6.1"); + private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + private static final InetAddress DNS1 = address("75.208.7.1"); + private static final InetAddress DNS2 = address("69.78.7.1"); + private static final InetAddress DNS6 = address("2001:4860:4860::8888"); + private static final InetAddress PRIVDNS1 = address("1.1.1.1"); + private static final InetAddress PRIVDNS2 = address("1.0.0.1"); + private static final InetAddress PRIVDNS6 = address("2606:4700:4700::1111"); + private static final InetAddress PCSCFV4 = address("10.77.25.37"); + private static final InetAddress PCSCFV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:1"); + private static final InetAddress GATEWAY1 = address("75.208.8.1"); + private static final InetAddress GATEWAY2 = address("69.78.8.1"); + private static final InetAddress GATEWAY61 = address("fe80::6:0000:613"); + private static final InetAddress GATEWAY62 = address("fe80::6:22%lo"); + private static final InetAddress TESTIPV4ADDR = address("192.168.47.42"); + private static final InetAddress TESTIPV6ADDR = address("fe80::7:33%43"); + private static final Inet4Address DHCPSERVER = (Inet4Address) address("192.0.2.1"); + private static final String NAME = "qmi0"; + private static final String DOMAINS = "google.com"; + private static final String PRIV_DNS_SERVER_NAME = "private.dns.com"; + private static final String TCP_BUFFER_SIZES = "524288,1048576,2097152,262144,524288,1048576"; + private static final int MTU = 1500; + private static final LinkAddress LINKADDRV4 = new LinkAddress(ADDRV4, 32); + private static final LinkAddress LINKADDRV6 = new LinkAddress(ADDRV6, 128); + private static final LinkAddress LINKADDRV6LINKLOCAL = new LinkAddress("fe80::1/64"); + private static final Uri CAPPORT_API_URL = Uri.parse("https://test.example.com/capportapi"); + + // CaptivePortalData cannot be in a constant as it does not exist on Q. + // The test runner also crashes when scanning for tests if it is a return type. + private static Object getCaptivePortalData() { + return new CaptivePortalData.Builder() + .setVenueInfoUrl(Uri.parse("https://test.example.com/venue")).build(); + } + + private static InetAddress address(String addrString) { + return InetAddresses.parseNumericAddress(addrString); + } + + private static boolean isAtLeastR() { + // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R) + return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR(); + } + + private void checkEmpty(final LinkProperties lp) { + assertEquals(0, lp.getAllInterfaceNames().size()); + assertEquals(0, lp.getAllAddresses().size()); + assertEquals(0, lp.getDnsServers().size()); + assertEquals(0, lp.getValidatedPrivateDnsServers().size()); + assertEquals(0, lp.getPcscfServers().size()); + assertEquals(0, lp.getAllRoutes().size()); + assertEquals(0, lp.getAllLinkAddresses().size()); + assertEquals(0, lp.getStackedLinks().size()); + assertEquals(0, lp.getMtu()); + assertNull(lp.getPrivateDnsServerName()); + assertNull(lp.getDomains()); + assertNull(lp.getHttpProxy()); + assertNull(lp.getTcpBufferSizes()); + assertNull(lp.getNat64Prefix()); + assertFalse(lp.isProvisioned()); + assertFalse(lp.isIpv4Provisioned()); + assertFalse(lp.isIpv6Provisioned()); + assertFalse(lp.isPrivateDnsActive()); + + if (isAtLeastR()) { + assertNull(lp.getDhcpServerAddress()); + assertFalse(lp.isWakeOnLanSupported()); + assertNull(lp.getCaptivePortalApiUrl()); + assertNull(lp.getCaptivePortalData()); + } + } + + private LinkProperties makeTestObject() { + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(NAME); + lp.addLinkAddress(LINKADDRV4); + lp.addLinkAddress(LINKADDRV6); + lp.addDnsServer(DNS1); + lp.addDnsServer(DNS2); + lp.addValidatedPrivateDnsServer(PRIVDNS1); + lp.addValidatedPrivateDnsServer(PRIVDNS2); + lp.setUsePrivateDns(true); + lp.setPrivateDnsServerName(PRIV_DNS_SERVER_NAME); + lp.addPcscfServer(PCSCFV6); + lp.setDomains(DOMAINS); + lp.addRoute(new RouteInfo(GATEWAY1)); + lp.addRoute(new RouteInfo(GATEWAY2)); + lp.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888)); + lp.setMtu(MTU); + lp.setTcpBufferSizes(TCP_BUFFER_SIZES); + lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96")); + if (isAtLeastR()) { + lp.setDhcpServerAddress(DHCPSERVER); + lp.setWakeOnLanSupported(true); + lp.setCaptivePortalApiUrl(CAPPORT_API_URL); + lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData()); + } + return lp; + } + + public void assertLinkPropertiesEqual(LinkProperties source, LinkProperties target) { + // Check implementation of equals(), element by element. + assertTrue(source.isIdenticalInterfaceName(target)); + assertTrue(target.isIdenticalInterfaceName(source)); + + assertTrue(source.isIdenticalAddresses(target)); + assertTrue(target.isIdenticalAddresses(source)); + + assertTrue(source.isIdenticalDnses(target)); + assertTrue(target.isIdenticalDnses(source)); + + assertTrue(source.isIdenticalPrivateDns(target)); + assertTrue(target.isIdenticalPrivateDns(source)); + + assertTrue(source.isIdenticalValidatedPrivateDnses(target)); + assertTrue(target.isIdenticalValidatedPrivateDnses(source)); + + assertTrue(source.isIdenticalPcscfs(target)); + assertTrue(target.isIdenticalPcscfs(source)); + + assertTrue(source.isIdenticalRoutes(target)); + assertTrue(target.isIdenticalRoutes(source)); + + assertTrue(source.isIdenticalHttpProxy(target)); + assertTrue(target.isIdenticalHttpProxy(source)); + + assertTrue(source.isIdenticalStackedLinks(target)); + assertTrue(target.isIdenticalStackedLinks(source)); + + assertTrue(source.isIdenticalMtu(target)); + assertTrue(target.isIdenticalMtu(source)); + + assertTrue(source.isIdenticalTcpBufferSizes(target)); + assertTrue(target.isIdenticalTcpBufferSizes(source)); + + if (isAtLeastR()) { + assertTrue(source.isIdenticalDhcpServerAddress(target)); + assertTrue(source.isIdenticalDhcpServerAddress(source)); + + assertTrue(source.isIdenticalWakeOnLan(target)); + assertTrue(target.isIdenticalWakeOnLan(source)); + + assertTrue(source.isIdenticalCaptivePortalApiUrl(target)); + assertTrue(target.isIdenticalCaptivePortalApiUrl(source)); + + assertTrue(source.isIdenticalCaptivePortalData(target)); + assertTrue(target.isIdenticalCaptivePortalData(source)); + } + + // Check result of equals(). + assertTrue(source.equals(target)); + assertTrue(target.equals(source)); + + // Check hashCode. + assertEquals(source.hashCode(), target.hashCode()); + } + + @Test + public void testEqualsNull() { + LinkProperties source = new LinkProperties(); + LinkProperties target = new LinkProperties(); + + assertFalse(source == target); + assertLinkPropertiesEqual(source, target); + } + + @Test + public void testEqualsSameOrder() throws Exception { + LinkProperties source = new LinkProperties(); + source.setInterfaceName(NAME); + // set 2 link addresses + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + // set 2 dnses + source.addDnsServer(DNS1); + source.addDnsServer(DNS2); + // set 1 pcscf + source.addPcscfServer(PCSCFV6); + // set 2 gateways + source.addRoute(new RouteInfo(GATEWAY1)); + source.addRoute(new RouteInfo(GATEWAY2)); + source.setMtu(MTU); + + LinkProperties target = new LinkProperties(); + + // All fields are same + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + + assertLinkPropertiesEqual(source, target); + + target.clear(); + // change Interface Name + target.setInterfaceName("qmi1"); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + // change link addresses + target.addLinkAddress(new LinkAddress(address("75.208.6.2"), 32)); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + // change dnses + target.addDnsServer(address("75.208.7.2")); + target.addDnsServer(DNS2); + target.addPcscfServer(PCSCFV6); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(address("75.208.7.2")); + target.addDnsServer(DNS2); + // change pcscf + target.addPcscfServer(address("2001::1")); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + target.setMtu(MTU); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + // change gateway + target.addRoute(new RouteInfo(address("75.208.8.2"))); + target.setMtu(MTU); + target.addRoute(new RouteInfo(GATEWAY2)); + assertFalse(source.equals(target)); + + target.clear(); + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addDnsServer(DNS1); + target.addDnsServer(DNS2); + target.addRoute(new RouteInfo(GATEWAY1)); + target.addRoute(new RouteInfo(GATEWAY2)); + // change mtu + target.setMtu(1440); + assertFalse(source.equals(target)); + } + + @Test + public void testEqualsDifferentOrder() throws Exception { + LinkProperties source = new LinkProperties(); + source.setInterfaceName(NAME); + // set 2 link addresses + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + // set 2 dnses + source.addDnsServer(DNS1); + source.addDnsServer(DNS2); + // set 2 gateways + source.addRoute(new RouteInfo(LINKADDRV4, GATEWAY1)); + source.addRoute(new RouteInfo(GATEWAY2)); + source.setMtu(MTU); + + LinkProperties target = new LinkProperties(); + // Exchange order + target.setInterfaceName(NAME); + target.addLinkAddress(LINKADDRV6); + target.addLinkAddress(LINKADDRV4); + target.addDnsServer(DNS2); + target.addDnsServer(DNS1); + target.addRoute(new RouteInfo(GATEWAY2)); + target.addRoute(new RouteInfo(LINKADDRV4, GATEWAY1)); + target.setMtu(MTU); + + assertLinkPropertiesEqual(source, target); + } + + @Test + public void testEqualsDuplicated() throws Exception { + LinkProperties source = new LinkProperties(); + // set 3 link addresses, eg, [A, A, B] + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + + LinkProperties target = new LinkProperties(); + // set 3 link addresses, eg, [A, B, B] + target.addLinkAddress(LINKADDRV4); + target.addLinkAddress(LINKADDRV6); + target.addLinkAddress(LINKADDRV6); + + assertLinkPropertiesEqual(source, target); + } + + private void assertAllRoutesHaveInterface(String iface, LinkProperties lp) { + for (RouteInfo r : lp.getRoutes()) { + assertEquals(iface, r.getInterface()); + } + } + + private void assertAllRoutesNotHaveInterface(String iface, LinkProperties lp) { + for (RouteInfo r : lp.getRoutes()) { + assertNotEquals(iface, r.getInterface()); + } + } + + @Test + public void testRouteInterfaces() { + LinkAddress prefix1 = new LinkAddress(address("2001:db8:1::"), 48); + LinkAddress prefix2 = new LinkAddress(address("2001:db8:2::"), 48); + InetAddress address = ADDRV6; + + // Add a route with no interface to a LinkProperties with no interface. No errors. + LinkProperties lp = new LinkProperties(); + RouteInfo r = new RouteInfo(prefix1, address, null); + assertTrue(lp.addRoute(r)); + assertEquals(1, lp.getRoutes().size()); + assertAllRoutesHaveInterface(null, lp); + + // Adding the same route twice has no effect. + assertFalse(lp.addRoute(r)); + assertEquals(1, lp.getRoutes().size()); + + // Add a route with an interface. Expect an exception. + r = new RouteInfo(prefix2, address, "wlan0"); + try { + lp.addRoute(r); + fail("Adding wlan0 route to LP with no interface, expect exception"); + } catch (IllegalArgumentException expected) {} + + // Change the interface name. All the routes should change their interface name too. + lp.setInterfaceName("rmnet0"); + assertAllRoutesHaveInterface("rmnet0", lp); + assertAllRoutesNotHaveInterface(null, lp); + assertAllRoutesNotHaveInterface("wlan0", lp); + + // Now add a route with the wrong interface. This causes an exception too. + try { + lp.addRoute(r); + fail("Adding wlan0 route to rmnet0 LP, expect exception"); + } catch (IllegalArgumentException expected) {} + + // If the interface name matches, the route is added. + r = new RouteInfo(prefix2, null, "wlan0"); + lp.setInterfaceName("wlan0"); + lp.addRoute(r); + assertEquals(2, lp.getRoutes().size()); + assertAllRoutesHaveInterface("wlan0", lp); + assertAllRoutesNotHaveInterface("rmnet0", lp); + + // Routes with null interfaces are converted to wlan0. + r = RouteInfo.makeHostRoute(ADDRV6, null); + lp.addRoute(r); + assertEquals(3, lp.getRoutes().size()); + assertAllRoutesHaveInterface("wlan0", lp); + + // Check routes are updated correctly when calling setInterfaceName. + LinkProperties lp2 = new LinkProperties(lp); + assertAllRoutesHaveInterface("wlan0", lp2); + final CompareResult<RouteInfo> cr1 = + new CompareResult<>(lp.getAllRoutes(), lp2.getAllRoutes()); + assertEquals(0, cr1.added.size()); + assertEquals(0, cr1.removed.size()); + + lp2.setInterfaceName("p2p0"); + assertAllRoutesHaveInterface("p2p0", lp2); + assertAllRoutesNotHaveInterface("wlan0", lp2); + final CompareResult<RouteInfo> cr2 = + new CompareResult<>(lp.getAllRoutes(), lp2.getAllRoutes()); + assertEquals(3, cr2.added.size()); + assertEquals(3, cr2.removed.size()); + + // Remove route with incorrect interface, no route removed. + lp.removeRoute(new RouteInfo(prefix2, null, null)); + assertEquals(3, lp.getRoutes().size()); + + // Check remove works when interface is correct. + lp.removeRoute(new RouteInfo(prefix2, null, "wlan0")); + assertEquals(2, lp.getRoutes().size()); + assertAllRoutesHaveInterface("wlan0", lp); + assertAllRoutesNotHaveInterface("p2p0", lp); + } + + @Test + public void testStackedInterfaces() { + LinkProperties rmnet0 = new LinkProperties(); + rmnet0.setInterfaceName("rmnet0"); + rmnet0.addLinkAddress(LINKADDRV6); + + LinkProperties clat4 = new LinkProperties(); + clat4.setInterfaceName("clat4"); + clat4.addLinkAddress(LINKADDRV4); + + assertEquals(0, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(1, rmnet0.getAllAddresses().size()); + assertEquals(1, rmnet0.getAllLinkAddresses().size()); + assertEquals(1, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + + rmnet0.addStackedLink(clat4); + assertEquals(1, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(2, rmnet0.getAllAddresses().size()); + assertEquals(2, rmnet0.getAllLinkAddresses().size()); + assertEquals(2, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + assertEquals("clat4", rmnet0.getAllInterfaceNames().get(1)); + + rmnet0.addStackedLink(clat4); + assertEquals(1, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(2, rmnet0.getAllAddresses().size()); + assertEquals(2, rmnet0.getAllLinkAddresses().size()); + assertEquals(2, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + assertEquals("clat4", rmnet0.getAllInterfaceNames().get(1)); + + assertEquals(0, clat4.getStackedLinks().size()); + + // Modify an item in the returned collection to see what happens. + for (LinkProperties link : rmnet0.getStackedLinks()) { + if (link.getInterfaceName().equals("clat4")) { + link.setInterfaceName("newname"); + } + } + for (LinkProperties link : rmnet0.getStackedLinks()) { + assertFalse("newname".equals(link.getInterfaceName())); + } + + assertTrue(rmnet0.removeStackedLink("clat4")); + assertEquals(0, rmnet0.getStackedLinks().size()); + assertEquals(1, rmnet0.getAddresses().size()); + assertEquals(1, rmnet0.getLinkAddresses().size()); + assertEquals(1, rmnet0.getAllAddresses().size()); + assertEquals(1, rmnet0.getAllLinkAddresses().size()); + assertEquals(1, rmnet0.getAllInterfaceNames().size()); + assertEquals("rmnet0", rmnet0.getAllInterfaceNames().get(0)); + + assertFalse(rmnet0.removeStackedLink("clat4")); + } + + private LinkAddress getFirstLinkAddress(LinkProperties lp) { + return lp.getLinkAddresses().iterator().next(); + } + + @Test + public void testAddressMethods() { + LinkProperties lp = new LinkProperties(); + + // No addresses. + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + + // Addresses on stacked links don't count. + LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("stacked"); + lp.addStackedLink(stacked); + stacked.addLinkAddress(LINKADDRV4); + stacked.addLinkAddress(LINKADDRV6); + assertTrue(stacked.hasIpv4Address()); + assertTrue(stacked.hasGlobalIpv6Address()); + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + lp.removeStackedLink("stacked"); + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + + // Addresses on the base link. + // Check the return values of hasIpvXAddress and ensure the add/remove methods return true + // iff something changes. + assertEquals(0, lp.getLinkAddresses().size()); + assertTrue(lp.addLinkAddress(LINKADDRV6)); + assertEquals(1, lp.getLinkAddresses().size()); + assertFalse(lp.hasIpv4Address()); + assertTrue(lp.hasGlobalIpv6Address()); + + assertTrue(lp.removeLinkAddress(LINKADDRV6)); + assertEquals(0, lp.getLinkAddresses().size()); + + assertTrue(lp.addLinkAddress(LINKADDRV6LINKLOCAL)); + assertEquals(1, lp.getLinkAddresses().size()); + assertFalse(lp.hasGlobalIpv6Address()); + + assertTrue(lp.addLinkAddress(LINKADDRV4)); + assertEquals(2, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertFalse(lp.hasGlobalIpv6Address()); + + assertTrue(lp.addLinkAddress(LINKADDRV6)); + assertEquals(3, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertTrue(lp.hasGlobalIpv6Address()); + + assertTrue(lp.removeLinkAddress(LINKADDRV6LINKLOCAL)); + assertEquals(2, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertTrue(lp.hasGlobalIpv6Address()); + + // Adding an address twice has no effect. + // Removing an address that's not present has no effect. + assertFalse(lp.addLinkAddress(LINKADDRV4)); + assertEquals(2, lp.getLinkAddresses().size()); + assertTrue(lp.hasIpv4Address()); + assertTrue(lp.removeLinkAddress(LINKADDRV4)); + assertEquals(1, lp.getLinkAddresses().size()); + assertFalse(lp.hasIpv4Address()); + assertFalse(lp.removeLinkAddress(LINKADDRV4)); + assertEquals(1, lp.getLinkAddresses().size()); + + // Adding an address that's already present but with different properties causes the + // existing address to be updated and returns true. + // Start with only LINKADDRV6. + assertEquals(1, lp.getLinkAddresses().size()); + assertEquals(LINKADDRV6, getFirstLinkAddress(lp)); + + // Create a LinkAddress object for the same address, but with different flags. + LinkAddress deprecated = new LinkAddress(ADDRV6, 128, + OsConstants.IFA_F_DEPRECATED, OsConstants.RT_SCOPE_UNIVERSE); + assertTrue(deprecated.isSameAddressAs(LINKADDRV6)); + assertFalse(deprecated.equals(LINKADDRV6)); + + // Check that adding it updates the existing address instead of adding a new one. + assertTrue(lp.addLinkAddress(deprecated)); + assertEquals(1, lp.getLinkAddresses().size()); + assertEquals(deprecated, getFirstLinkAddress(lp)); + assertFalse(LINKADDRV6.equals(getFirstLinkAddress(lp))); + + // Removing LINKADDRV6 removes deprecated, because removing addresses ignores properties. + assertTrue(lp.removeLinkAddress(LINKADDRV6)); + assertEquals(0, lp.getLinkAddresses().size()); + } + + @Test + public void testLinkAddresses() { + final LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(LINKADDRV4); + lp.addLinkAddress(LINKADDRV6); + + final LinkProperties lp2 = new LinkProperties(); + lp2.addLinkAddress(LINKADDRV6); + + final LinkProperties lp3 = new LinkProperties(); + final List<LinkAddress> linkAddresses = Arrays.asList(LINKADDRV4); + lp3.setLinkAddresses(linkAddresses); + + assertFalse(lp.equals(lp2)); + assertFalse(lp2.equals(lp3)); + + lp.removeLinkAddress(LINKADDRV4); + assertTrue(lp.equals(lp2)); + + lp2.setLinkAddresses(lp3.getLinkAddresses()); + assertTrue(lp2.equals(lp3)); + } + + @Test + public void testNat64Prefix() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.addLinkAddress(LINKADDRV4); + lp.addLinkAddress(LINKADDRV6); + + assertNull(lp.getNat64Prefix()); + + IpPrefix p = new IpPrefix("64:ff9b::/96"); + lp.setNat64Prefix(p); + assertEquals(p, lp.getNat64Prefix()); + + p = new IpPrefix("2001:db8:a:b:1:2:3::/96"); + lp.setNat64Prefix(p); + assertEquals(p, lp.getNat64Prefix()); + + p = new IpPrefix("2001:db8:a:b:1:2::/80"); + try { + lp.setNat64Prefix(p); + } catch (IllegalArgumentException expected) { + } + + p = new IpPrefix("64:ff9b::/64"); + try { + lp.setNat64Prefix(p); + } catch (IllegalArgumentException expected) { + } + + assertEquals(new IpPrefix("2001:db8:a:b:1:2:3::/96"), lp.getNat64Prefix()); + + lp.setNat64Prefix(null); + assertNull(lp.getNat64Prefix()); + } + + @Test + public void testIsProvisioned() { + LinkProperties lp4 = new LinkProperties(); + assertFalse("v4only:empty", lp4.isProvisioned()); + lp4.addLinkAddress(LINKADDRV4); + assertFalse("v4only:addr-only", lp4.isProvisioned()); + lp4.addDnsServer(DNS1); + assertFalse("v4only:addr+dns", lp4.isProvisioned()); + lp4.addRoute(new RouteInfo(GATEWAY1)); + assertTrue("v4only:addr+dns+route", lp4.isProvisioned()); + assertTrue("v4only:addr+dns+route", lp4.isIpv4Provisioned()); + assertFalse("v4only:addr+dns+route", lp4.isIpv6Provisioned()); + + LinkProperties lp6 = new LinkProperties(); + assertFalse("v6only:empty", lp6.isProvisioned()); + lp6.addLinkAddress(LINKADDRV6LINKLOCAL); + assertFalse("v6only:fe80-only", lp6.isProvisioned()); + lp6.addDnsServer(DNS6); + assertFalse("v6only:fe80+dns", lp6.isProvisioned()); + lp6.addRoute(new RouteInfo(GATEWAY61)); + assertFalse("v6only:fe80+dns+route", lp6.isProvisioned()); + lp6.addLinkAddress(LINKADDRV6); + assertTrue("v6only:fe80+global+dns+route", lp6.isIpv6Provisioned()); + assertTrue("v6only:fe80+global+dns+route", lp6.isProvisioned()); + lp6.removeLinkAddress(LINKADDRV6LINKLOCAL); + assertFalse("v6only:global+dns+route", lp6.isIpv4Provisioned()); + assertTrue("v6only:global+dns+route", lp6.isIpv6Provisioned()); + assertTrue("v6only:global+dns+route", lp6.isProvisioned()); + + LinkProperties lp46 = new LinkProperties(); + lp46.addLinkAddress(LINKADDRV4); + lp46.addLinkAddress(LINKADDRV6); + lp46.addDnsServer(DNS1); + lp46.addDnsServer(DNS6); + assertFalse("dualstack:missing-routes", lp46.isProvisioned()); + lp46.addRoute(new RouteInfo(GATEWAY1)); + assertTrue("dualstack:v4-provisioned", lp46.isIpv4Provisioned()); + assertFalse("dualstack:v4-provisioned", lp46.isIpv6Provisioned()); + assertTrue("dualstack:v4-provisioned", lp46.isProvisioned()); + lp46.addRoute(new RouteInfo(GATEWAY61)); + assertTrue("dualstack:both-provisioned", lp46.isIpv4Provisioned()); + assertTrue("dualstack:both-provisioned", lp46.isIpv6Provisioned()); + assertTrue("dualstack:both-provisioned", lp46.isProvisioned()); + + // A link with an IPv6 address and default route, but IPv4 DNS server. + LinkProperties mixed = new LinkProperties(); + mixed.addLinkAddress(LINKADDRV6); + mixed.addDnsServer(DNS1); + mixed.addRoute(new RouteInfo(GATEWAY61)); + assertFalse("mixed:addr6+route6+dns4", mixed.isIpv4Provisioned()); + assertFalse("mixed:addr6+route6+dns4", mixed.isIpv6Provisioned()); + assertFalse("mixed:addr6+route6+dns4", mixed.isProvisioned()); + } + + @Test + public void testCompareProvisioning() { + LinkProperties v4lp = new LinkProperties(); + v4lp.addLinkAddress(LINKADDRV4); + v4lp.addRoute(new RouteInfo(GATEWAY1)); + v4lp.addDnsServer(DNS1); + assertTrue(v4lp.isProvisioned()); + + LinkProperties v4r = new LinkProperties(v4lp); + v4r.removeDnsServer(DNS1); + assertFalse(v4r.isProvisioned()); + + assertEquals(ProvisioningChange.STILL_NOT_PROVISIONED, + LinkProperties.compareProvisioning(v4r, v4r)); + assertEquals(ProvisioningChange.LOST_PROVISIONING, + LinkProperties.compareProvisioning(v4lp, v4r)); + assertEquals(ProvisioningChange.GAINED_PROVISIONING, + LinkProperties.compareProvisioning(v4r, v4lp)); + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v4lp, v4lp)); + + // Check that losing IPv4 provisioning on a dualstack network is + // seen as a total loss of provisioning. + LinkProperties v6lp = new LinkProperties(); + v6lp.addLinkAddress(LINKADDRV6); + v6lp.addRoute(new RouteInfo(GATEWAY61)); + v6lp.addDnsServer(DNS6); + assertFalse(v6lp.isIpv4Provisioned()); + assertTrue(v6lp.isIpv6Provisioned()); + assertTrue(v6lp.isProvisioned()); + + LinkProperties v46lp = new LinkProperties(v6lp); + v46lp.addLinkAddress(LINKADDRV4); + v46lp.addRoute(new RouteInfo(GATEWAY1)); + v46lp.addDnsServer(DNS1); + assertTrue(v46lp.isIpv4Provisioned()); + assertTrue(v46lp.isIpv6Provisioned()); + assertTrue(v46lp.isProvisioned()); + + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v4lp, v46lp)); + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v6lp, v46lp)); + assertEquals(ProvisioningChange.LOST_PROVISIONING, + LinkProperties.compareProvisioning(v46lp, v6lp)); + assertEquals(ProvisioningChange.LOST_PROVISIONING, + LinkProperties.compareProvisioning(v46lp, v4lp)); + + // Check that losing and gaining a secondary router does not change + // the provisioning status. + LinkProperties v6lp2 = new LinkProperties(v6lp); + v6lp2.addRoute(new RouteInfo(GATEWAY62)); + assertTrue(v6lp2.isProvisioned()); + + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v6lp2, v6lp)); + assertEquals(ProvisioningChange.STILL_PROVISIONED, + LinkProperties.compareProvisioning(v6lp, v6lp2)); + } + + @Test + public void testIsReachable() { + final LinkProperties v4lp = new LinkProperties(); + assertFalse(v4lp.isReachable(DNS1)); + assertFalse(v4lp.isReachable(DNS2)); + + // Add an on-link route, making the on-link DNS server reachable, + // but there is still no IPv4 address. + assertTrue(v4lp.addRoute(new RouteInfo(new IpPrefix(address("75.208.0.0"), 16)))); + assertFalse(v4lp.isReachable(DNS1)); + assertFalse(v4lp.isReachable(DNS2)); + + // Adding an IPv4 address (right now, any IPv4 address) means we use + // the routes to compute likely reachability. + assertTrue(v4lp.addLinkAddress(new LinkAddress(ADDRV4, 16))); + assertTrue(v4lp.isReachable(DNS1)); + assertFalse(v4lp.isReachable(DNS2)); + + // Adding a default route makes the off-link DNS server reachable. + assertTrue(v4lp.addRoute(new RouteInfo(GATEWAY1))); + assertTrue(v4lp.isReachable(DNS1)); + assertTrue(v4lp.isReachable(DNS2)); + + final LinkProperties v6lp = new LinkProperties(); + final InetAddress kLinkLocalDns = address("fe80::6:1"); + final InetAddress kLinkLocalDnsWithScope = address("fe80::6:2%43"); + final InetAddress kOnLinkDns = address("2001:db8:85a3::53"); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertFalse(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertFalse(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a link-local route, making the link-local DNS servers reachable. Because + // we assume the presence of an IPv6 link-local address, link-local DNS servers + // are considered reachable, but only those with a non-zero scope identifier. + assertTrue(v6lp.addRoute(new RouteInfo(new IpPrefix(address("fe80::"), 64)))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertFalse(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a link-local address--nothing changes. + assertTrue(v6lp.addLinkAddress(LINKADDRV6LINKLOCAL)); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertFalse(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a global route on link, but no global address yet. DNS servers reachable + // via a route that doesn't require a gateway: give them the benefit of the + // doubt and hope the link-local source address suffices for communication. + assertTrue(v6lp.addRoute(new RouteInfo(new IpPrefix(address("2001:db8:85a3::"), 64)))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertTrue(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Add a global address; the on-link global address DNS server is (still) + // presumed reachable. + assertTrue(v6lp.addLinkAddress(new LinkAddress(ADDRV6, 64))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertTrue(v6lp.isReachable(kOnLinkDns)); + assertFalse(v6lp.isReachable(DNS6)); + + // Adding a default route makes the off-link DNS server reachable. + assertTrue(v6lp.addRoute(new RouteInfo(GATEWAY62))); + assertFalse(v6lp.isReachable(kLinkLocalDns)); + assertTrue(v6lp.isReachable(kLinkLocalDnsWithScope)); + assertTrue(v6lp.isReachable(kOnLinkDns)); + assertTrue(v6lp.isReachable(DNS6)); + + // Check isReachable on stacked links. This requires that the source IP address be assigned + // on the interface returned by the route lookup. + LinkProperties stacked = new LinkProperties(); + + // Can't add a stacked link without an interface name. + stacked.setInterfaceName("v4-test0"); + v6lp.addStackedLink(stacked); + + InetAddress stackedAddress = address("192.0.0.4"); + LinkAddress stackedLinkAddress = new LinkAddress(stackedAddress, 32); + assertFalse(v6lp.isReachable(stackedAddress)); + stacked.addLinkAddress(stackedLinkAddress); + assertFalse(v6lp.isReachable(stackedAddress)); + stacked.addRoute(new RouteInfo(stackedLinkAddress)); + assertTrue(stacked.isReachable(stackedAddress)); + assertTrue(v6lp.isReachable(stackedAddress)); + + assertFalse(v6lp.isReachable(DNS1)); + stacked.addRoute(new RouteInfo((IpPrefix) null, stackedAddress)); + assertTrue(v6lp.isReachable(DNS1)); + } + + @Test + public void testLinkPropertiesEnsureDirectlyConnectedRoutes() { + // IPv4 case: no route added initially + LinkProperties rmnet0 = new LinkProperties(); + rmnet0.setInterfaceName("rmnet0"); + rmnet0.addLinkAddress(new LinkAddress("10.0.0.2/8")); + RouteInfo directRoute0 = new RouteInfo(new IpPrefix("10.0.0.0/8"), null, + rmnet0.getInterfaceName()); + + // Since no routes is added explicitly, getAllRoutes() should return empty. + assertTrue(rmnet0.getAllRoutes().isEmpty()); + rmnet0.ensureDirectlyConnectedRoutes(); + // ensureDirectlyConnectedRoutes() should have added the missing local route. + assertEqualRoutes(Collections.singletonList(directRoute0), rmnet0.getAllRoutes()); + + // IPv4 case: both direct and default routes added initially + LinkProperties rmnet1 = new LinkProperties(); + rmnet1.setInterfaceName("rmnet1"); + rmnet1.addLinkAddress(new LinkAddress("10.0.0.3/8")); + RouteInfo defaultRoute1 = new RouteInfo((IpPrefix) null, address("10.0.0.1"), + rmnet1.getInterfaceName()); + RouteInfo directRoute1 = new RouteInfo(new IpPrefix("10.0.0.0/8"), null, + rmnet1.getInterfaceName()); + rmnet1.addRoute(defaultRoute1); + rmnet1.addRoute(directRoute1); + + // Check added routes + assertEqualRoutes(Arrays.asList(defaultRoute1, directRoute1), rmnet1.getAllRoutes()); + // ensureDirectlyConnectedRoutes() shouldn't change the routes since direct connected + // route is already part of the configuration. + rmnet1.ensureDirectlyConnectedRoutes(); + assertEqualRoutes(Arrays.asList(defaultRoute1, directRoute1), rmnet1.getAllRoutes()); + + // IPv6 case: only default routes added initially + LinkProperties rmnet2 = new LinkProperties(); + rmnet2.setInterfaceName("rmnet2"); + rmnet2.addLinkAddress(new LinkAddress("fe80::cafe/64")); + rmnet2.addLinkAddress(new LinkAddress("2001:db8::2/64")); + RouteInfo defaultRoute2 = new RouteInfo((IpPrefix) null, address("2001:db8::1"), + rmnet2.getInterfaceName()); + RouteInfo directRoute2 = new RouteInfo(new IpPrefix("2001:db8::/64"), null, + rmnet2.getInterfaceName()); + RouteInfo linkLocalRoute2 = new RouteInfo(new IpPrefix("fe80::/64"), null, + rmnet2.getInterfaceName()); + rmnet2.addRoute(defaultRoute2); + + assertEqualRoutes(Arrays.asList(defaultRoute2), rmnet2.getAllRoutes()); + rmnet2.ensureDirectlyConnectedRoutes(); + assertEqualRoutes(Arrays.asList(defaultRoute2, directRoute2, linkLocalRoute2), + rmnet2.getAllRoutes()); + + // Corner case: no interface name + LinkProperties rmnet3 = new LinkProperties(); + rmnet3.addLinkAddress(new LinkAddress("192.168.0.2/24")); + RouteInfo directRoute3 = new RouteInfo(new IpPrefix("192.168.0.0/24"), null, + rmnet3.getInterfaceName()); + + assertTrue(rmnet3.getAllRoutes().isEmpty()); + rmnet3.ensureDirectlyConnectedRoutes(); + assertEqualRoutes(Collections.singletonList(directRoute3), rmnet3.getAllRoutes()); + } + + private void assertEqualRoutes(Collection<RouteInfo> expected, Collection<RouteInfo> actual) { + Set<RouteInfo> expectedSet = new ArraySet<>(expected); + Set<RouteInfo> actualSet = new ArraySet<>(actual); + // Duplicated entries in actual routes are considered failures + assertEquals(actual.size(), actualSet.size()); + + assertEquals(expectedSet, actualSet); + } + + private static LinkProperties makeLinkPropertiesForParceling() { + LinkProperties source = new LinkProperties(); + source.setInterfaceName(NAME); + + source.addLinkAddress(LINKADDRV4); + source.addLinkAddress(LINKADDRV6); + + source.addDnsServer(DNS1); + source.addDnsServer(DNS2); + source.addDnsServer(GATEWAY62); + + source.addPcscfServer(TESTIPV4ADDR); + source.addPcscfServer(TESTIPV6ADDR); + + source.setUsePrivateDns(true); + source.setPrivateDnsServerName(PRIV_DNS_SERVER_NAME); + + source.setDomains(DOMAINS); + + source.addRoute(new RouteInfo(GATEWAY1)); + source.addRoute(new RouteInfo(GATEWAY2)); + + source.addValidatedPrivateDnsServer(DNS6); + source.addValidatedPrivateDnsServer(GATEWAY61); + source.addValidatedPrivateDnsServer(TESTIPV6ADDR); + + source.setHttpProxy(ProxyInfo.buildDirectProxy("test", 8888)); + + source.setMtu(MTU); + + source.setTcpBufferSizes(TCP_BUFFER_SIZES); + + source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96")); + + final LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("test-stacked"); + source.addStackedLink(stacked); + + return source; + } + + @Test @IgnoreAfter(Build.VERSION_CODES.Q) + public void testLinkPropertiesParcelable_Q() throws Exception { + final LinkProperties source = makeLinkPropertiesForParceling(); + assertParcelSane(source, 14 /* fieldCount */); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testLinkPropertiesParcelable() throws Exception { + final LinkProperties source = makeLinkPropertiesForParceling(); + + source.setWakeOnLanSupported(true); + source.setCaptivePortalApiUrl(CAPPORT_API_URL); + source.setCaptivePortalData((CaptivePortalData) getCaptivePortalData()); + source.setDhcpServerAddress((Inet4Address) GATEWAY1); + assertParcelSane(new LinkProperties(source, true /* parcelSensitiveFields */), + 18 /* fieldCount */); + + // Verify that without using a sensitiveFieldsParcelingCopy, sensitive fields are cleared. + final LinkProperties sanitized = new LinkProperties(source); + sanitized.setCaptivePortalApiUrl(null); + sanitized.setCaptivePortalData(null); + assertEquals(sanitized, parcelingRoundTrip(source)); + } + + // Parceling of the scope was broken until Q-QPR2 + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testLinkLocalDnsServerParceling() throws Exception { + final String strAddress = "fe80::1%lo"; + final LinkProperties lp = new LinkProperties(); + lp.addDnsServer(address(strAddress)); + final LinkProperties unparceled = parcelingRoundTrip(lp); + // Inet6Address#equals does not test for the scope id + assertEquals(strAddress, unparceled.getDnsServers().get(0).getHostAddress()); + } + + @Test + public void testParcelUninitialized() throws Exception { + LinkProperties empty = new LinkProperties(); + assertParcelingIsLossless(empty); + } + + @Test + public void testConstructor() { + LinkProperties lp = new LinkProperties(); + checkEmpty(lp); + assertLinkPropertiesEqual(lp, new LinkProperties(lp)); + assertLinkPropertiesEqual(lp, new LinkProperties()); + + lp = makeTestObject(); + assertLinkPropertiesEqual(lp, new LinkProperties(lp)); + } + + @Test + public void testDnsServers() { + final LinkProperties lp = new LinkProperties(); + final List<InetAddress> dnsServers = Arrays.asList(DNS1, DNS2); + lp.setDnsServers(dnsServers); + assertEquals(2, lp.getDnsServers().size()); + assertEquals(DNS1, lp.getDnsServers().get(0)); + assertEquals(DNS2, lp.getDnsServers().get(1)); + + lp.removeDnsServer(DNS1); + assertEquals(1, lp.getDnsServers().size()); + assertEquals(DNS2, lp.getDnsServers().get(0)); + + lp.addDnsServer(DNS6); + assertEquals(2, lp.getDnsServers().size()); + assertEquals(DNS2, lp.getDnsServers().get(0)); + assertEquals(DNS6, lp.getDnsServers().get(1)); + } + + @Test + public void testValidatedPrivateDnsServers() { + final LinkProperties lp = new LinkProperties(); + final List<InetAddress> privDnsServers = Arrays.asList(PRIVDNS1, PRIVDNS2); + lp.setValidatedPrivateDnsServers(privDnsServers); + assertEquals(2, lp.getValidatedPrivateDnsServers().size()); + assertEquals(PRIVDNS1, lp.getValidatedPrivateDnsServers().get(0)); + assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(1)); + + lp.removeValidatedPrivateDnsServer(PRIVDNS1); + assertEquals(1, lp.getValidatedPrivateDnsServers().size()); + assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(0)); + + lp.addValidatedPrivateDnsServer(PRIVDNS6); + assertEquals(2, lp.getValidatedPrivateDnsServers().size()); + assertEquals(PRIVDNS2, lp.getValidatedPrivateDnsServers().get(0)); + assertEquals(PRIVDNS6, lp.getValidatedPrivateDnsServers().get(1)); + } + + @Test + public void testPcscfServers() { + final LinkProperties lp = new LinkProperties(); + final List<InetAddress> pcscfServers = Arrays.asList(PCSCFV4); + lp.setPcscfServers(pcscfServers); + assertEquals(1, lp.getPcscfServers().size()); + assertEquals(PCSCFV4, lp.getPcscfServers().get(0)); + + lp.removePcscfServer(PCSCFV4); + assertEquals(0, lp.getPcscfServers().size()); + + lp.addPcscfServer(PCSCFV6); + assertEquals(1, lp.getPcscfServers().size()); + assertEquals(PCSCFV6, lp.getPcscfServers().get(0)); + } + + @Test + public void testTcpBufferSizes() { + final LinkProperties lp = makeTestObject(); + assertEquals(TCP_BUFFER_SIZES, lp.getTcpBufferSizes()); + + lp.setTcpBufferSizes(null); + assertNull(lp.getTcpBufferSizes()); + } + + @Test + public void testHasIpv6DefaultRoute() { + final LinkProperties lp = makeTestObject(); + assertFalse(lp.hasIPv6DefaultRoute()); + + lp.addRoute(new RouteInfo(GATEWAY61)); + assertTrue(lp.hasIPv6DefaultRoute()); + } + + @Test + public void testHttpProxy() { + final LinkProperties lp = makeTestObject(); + assertTrue(lp.getHttpProxy().equals(ProxyInfo.buildDirectProxy("test", 8888))); + } + + @Test + public void testPrivateDnsServerName() { + final LinkProperties lp = makeTestObject(); + assertEquals(PRIV_DNS_SERVER_NAME, lp.getPrivateDnsServerName()); + + lp.setPrivateDnsServerName(null); + assertNull(lp.getPrivateDnsServerName()); + } + + @Test + public void testUsePrivateDns() { + final LinkProperties lp = makeTestObject(); + assertTrue(lp.isPrivateDnsActive()); + + lp.clear(); + assertFalse(lp.isPrivateDnsActive()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testDhcpServerAddress() { + final LinkProperties lp = makeTestObject(); + assertEquals(DHCPSERVER, lp.getDhcpServerAddress()); + + lp.clear(); + assertNull(lp.getDhcpServerAddress()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testWakeOnLanSupported() { + final LinkProperties lp = makeTestObject(); + assertTrue(lp.isWakeOnLanSupported()); + + lp.clear(); + assertFalse(lp.isWakeOnLanSupported()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCaptivePortalApiUrl() { + final LinkProperties lp = makeTestObject(); + assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl()); + + lp.clear(); + assertNull(lp.getCaptivePortalApiUrl()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCaptivePortalData() { + final LinkProperties lp = makeTestObject(); + assertEquals(getCaptivePortalData(), lp.getCaptivePortalData()); + + lp.clear(); + assertNull(lp.getCaptivePortalData()); + } + + private LinkProperties makeIpv4LinkProperties() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(NAME); + linkProperties.addLinkAddress(LINKADDRV4); + linkProperties.addDnsServer(DNS1); + linkProperties.addRoute(new RouteInfo(GATEWAY1)); + linkProperties.addRoute(new RouteInfo(GATEWAY2)); + return linkProperties; + } + + private LinkProperties makeIpv6LinkProperties() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(NAME); + linkProperties.addLinkAddress(LINKADDRV6); + linkProperties.addDnsServer(DNS6); + linkProperties.addRoute(new RouteInfo(GATEWAY61)); + linkProperties.addRoute(new RouteInfo(GATEWAY62)); + return linkProperties; + } + + @Test + public void testHasIpv4DefaultRoute() { + final LinkProperties Ipv4 = makeIpv4LinkProperties(); + assertTrue(Ipv4.hasIpv4DefaultRoute()); + final LinkProperties Ipv6 = makeIpv6LinkProperties(); + assertFalse(Ipv6.hasIpv4DefaultRoute()); + } + + @Test + public void testHasIpv4DnsServer() { + final LinkProperties Ipv4 = makeIpv4LinkProperties(); + assertTrue(Ipv4.hasIpv4DnsServer()); + final LinkProperties Ipv6 = makeIpv6LinkProperties(); + assertFalse(Ipv6.hasIpv4DnsServer()); + } + + @Test + public void testHasIpv6DnsServer() { + final LinkProperties Ipv4 = makeIpv4LinkProperties(); + assertFalse(Ipv4.hasIpv6DnsServer()); + final LinkProperties Ipv6 = makeIpv6LinkProperties(); + assertTrue(Ipv6.hasIpv6DnsServer()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testHasIpv4UnreachableDefaultRoute() { + final LinkProperties lp = makeTestObject(); + assertFalse(lp.hasIpv4UnreachableDefaultRoute()); + assertFalse(lp.hasIpv6UnreachableDefaultRoute()); + + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); + assertTrue(lp.hasIpv4UnreachableDefaultRoute()); + assertFalse(lp.hasIpv6UnreachableDefaultRoute()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testHasIpv6UnreachableDefaultRoute() { + final LinkProperties lp = makeTestObject(); + assertFalse(lp.hasIpv6UnreachableDefaultRoute()); + assertFalse(lp.hasIpv4UnreachableDefaultRoute()); + + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE)); + assertTrue(lp.hasIpv6UnreachableDefaultRoute()); + assertFalse(lp.hasIpv4UnreachableDefaultRoute()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRouteAddWithSameKey() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan0"); + final IpPrefix v6 = new IpPrefix("64:ff9b::/96"); + lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280)); + assertEquals(1, lp.getRoutes().size()); + lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500)); + assertEquals(1, lp.getRoutes().size()); + final IpPrefix v4 = new IpPrefix("192.0.2.128/25"); + lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460)); + assertEquals(2, lp.getRoutes().size()); + lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460)); + assertEquals(2, lp.getRoutes().size()); + } +} diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5e44d59fcabff8425cb5f0b8167297f72a6f63a --- /dev/null +++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.wifi.aware.DiscoverySession +import android.net.wifi.aware.PeerHandle +import android.net.wifi.aware.WifiAwareNetworkSpecifier +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 + +import com.android.testutils.assertParcelSane +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo + +import java.lang.IllegalStateException + +import org.junit.Assert.assertFalse +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito + +@RunWith(AndroidJUnit4::class) +@SmallTest +class MatchAllNetworkSpecifierTest { + @Rule @JvmField + val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() + + private val specifier = MatchAllNetworkSpecifier() + private val discoverySession = Mockito.mock(DiscoverySession::class.java) + private val peerHandle = Mockito.mock(PeerHandle::class.java) + private val wifiAwareNetworkSpecifier = WifiAwareNetworkSpecifier.Builder(discoverySession, + peerHandle).build() + + @Test + fun testParcel() { + assertParcelSane(MatchAllNetworkSpecifier(), 0) + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + @IgnoreAfter(Build.VERSION_CODES.R) + // Only run this test on Android R. + // The method - satisfiedBy() has changed to canBeSatisfiedBy() starting from Android R, so the + // method - canBeSatisfiedBy() cannot be found when running this test on Android Q. + fun testCanBeSatisfiedBy_OnlyForR() { + // MatchAllNetworkSpecifier didn't follow its parent class to change the satisfiedBy() to + // canBeSatisfiedBy(), so if a caller calls MatchAllNetworkSpecifier#canBeSatisfiedBy(), the + // NetworkSpecifier#canBeSatisfiedBy() will be called actually, and false will be returned. + // Although it's not meeting the expectation, the behavior still needs to be verified. + assertFalse(specifier.canBeSatisfiedBy(wifiAwareNetworkSpecifier)) + } + + @Test(expected = IllegalStateException::class) + @IgnoreUpTo(Build.VERSION_CODES.R) + fun testCanBeSatisfiedBy() { + specifier.canBeSatisfiedBy(wifiAwareNetworkSpecifier) + } +} diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..46f39dd016fd2993bd365aa689fab0f4c5b43080 --- /dev/null +++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.InvalidPacketException.ERROR_INVALID_IP_ADDRESS +import android.net.InvalidPacketException.ERROR_INVALID_PORT +import android.net.NattSocketKeepalive.NATT_PORT +import android.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertEqualBothWays +import com.android.testutils.assertFieldCountEquals +import com.android.testutils.assertParcelSane +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.parcelingRoundTrip +import java.net.InetAddress +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NattKeepalivePacketDataTest { + @Rule @JvmField + val ignoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule() + + /* Refer to the definition in {@code NattKeepalivePacketData} */ + private val IPV4_HEADER_LENGTH = 20 + private val UDP_HEADER_LENGTH = 8 + + private val TEST_PORT = 4243 + private val TEST_PORT2 = 4244 + private val TEST_SRC_ADDRV4 = "198.168.0.2".address() + private val TEST_DST_ADDRV4 = "198.168.0.1".address() + private val TEST_ADDRV6 = "2001:db8::1".address() + + private fun String.address() = InetAddresses.parseNumericAddress(this) + private fun nattKeepalivePacket( + srcAddress: InetAddress? = TEST_SRC_ADDRV4, + srcPort: Int = TEST_PORT, + dstAddress: InetAddress? = TEST_DST_ADDRV4, + dstPort: Int = NATT_PORT + ) = NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, dstAddress, dstPort) + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testConstructor() { + try { + nattKeepalivePacket(dstPort = TEST_PORT) + fail("Dst port is not NATT port should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_PORT) + } + + try { + nattKeepalivePacket(srcAddress = TEST_ADDRV6) + fail("A v6 srcAddress should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + nattKeepalivePacket(dstAddress = TEST_ADDRV6) + fail("A v6 dstAddress should cause exception") + } catch (e: InvalidPacketException) { + assertEquals(e.error, ERROR_INVALID_IP_ADDRESS) + } + + try { + parcelingRoundTrip( + NattKeepalivePacketData(TEST_SRC_ADDRV4, TEST_PORT, TEST_DST_ADDRV4, TEST_PORT, + byteArrayOf(12, 31, 22, 44))) + fail("Invalid data should cause exception") + } catch (e: IllegalArgumentException) { } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testParcel() { + assertParcelSane(nattKeepalivePacket(), 0) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testEquals() { + assertEqualBothWays(nattKeepalivePacket(), nattKeepalivePacket()) + assertNotEquals(nattKeepalivePacket(dstAddress = TEST_SRC_ADDRV4), nattKeepalivePacket()) + assertNotEquals(nattKeepalivePacket(srcAddress = TEST_DST_ADDRV4), nattKeepalivePacket()) + // Test src port only because dst port have to be NATT_PORT + assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket()) + // Make sure the parceling test is updated if fields are added in the base class. + assertFieldCountEquals(5, KeepalivePacketData::class.java) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testHashCode() { + assertEquals(nattKeepalivePacket().hashCode(), nattKeepalivePacket().hashCode()) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b45b3d69ce9ffa31ba158100fa307f49491eb05 --- /dev/null +++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt @@ -0,0 +1,91 @@ +/* + * 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.os.Build +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.modules.utils.build.SdkLevel.isAtLeastS +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetworkAgentConfigTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testParcelNetworkAgentConfig() { + val config = NetworkAgentConfig.Builder().apply { + setExplicitlySelected(true) + setLegacyType(ConnectivityManager.TYPE_ETHERNET) + setSubscriberId("MySubId") + setPartialConnectivityAcceptable(false) + setUnvalidatedConnectivityAcceptable(true) + if (isAtLeastS()) { + setBypassableVpn(true) + } + }.build() + if (isAtLeastS()) { + // From S, the config will have 12 items + assertParcelSane(config, 12) + } else { + // For R or below, the config will have 10 items + assertParcelSane(config, 10) + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testBuilder() { + val config = NetworkAgentConfig.Builder().apply { + setExplicitlySelected(true) + setLegacyType(ConnectivityManager.TYPE_ETHERNET) + setSubscriberId("MySubId") + setPartialConnectivityAcceptable(false) + setUnvalidatedConnectivityAcceptable(true) + setLegacyTypeName("TEST_NETWORK") + if (isAtLeastS()) { + setNat64DetectionEnabled(false) + setProvisioningNotificationEnabled(false) + setBypassableVpn(true) + } + }.build() + + assertTrue(config.isExplicitlySelected()) + assertEquals(ConnectivityManager.TYPE_ETHERNET, config.getLegacyType()) + assertEquals("MySubId", config.getSubscriberId()) + assertFalse(config.isPartialConnectivityAcceptable()) + assertTrue(config.isUnvalidatedConnectivityAcceptable()) + assertEquals("TEST_NETWORK", config.getLegacyTypeName()) + if (isAtLeastS()) { + assertFalse(config.isNat64DetectionEnabled()) + assertFalse(config.isProvisioningNotificationEnabled()) + assertTrue(config.isBypassableVpn()) + } else { + assertTrue(config.isNat64DetectionEnabled()) + assertTrue(config.isProvisioningNotificationEnabled()) + } + } +} diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9537786055565874c157d9e68f2aa42c5be5d47b --- /dev/null +++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java @@ -0,0 +1,1170 @@ +/* + * 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; + +import static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; +import static android.net.NetworkCapabilities.MAX_TRANSPORT; +import static android.net.NetworkCapabilities.MIN_TRANSPORT; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; +import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; +import static android.os.Process.INVALID_UID; + +import static com.android.modules.utils.build.SdkLevel.isAtLeastR; +import static com.android.modules.utils.build.SdkLevel.isAtLeastS; +import static com.android.net.module.util.NetworkCapabilitiesUtils.TRANSPORT_USB; +import static com.android.testutils.MiscAsserts.assertEmpty; +import static com.android.testutils.MiscAsserts.assertThrows; +import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +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.net.wifi.aware.DiscoverySession; +import android.net.wifi.aware.PeerHandle; +import android.net.wifi.aware.WifiAwareNetworkSpecifier; +import android.os.Build; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.ArraySet; +import android.util.Range; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.CompatUtil; +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 org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkCapabilitiesTest { + private static final String TEST_SSID = "TEST_SSID"; + private static final String DIFFERENT_TEST_SSID = "DIFFERENT_TEST_SSID"; + private static final int TEST_SUBID1 = 1; + private static final int TEST_SUBID2 = 2; + private static final int TEST_SUBID3 = 3; + + @Rule + public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule(); + + private DiscoverySession mDiscoverySession = Mockito.mock(DiscoverySession.class); + private PeerHandle mPeerHandle = Mockito.mock(PeerHandle.class); + + @Test + public void testMaybeMarkCapabilitiesRestricted() { + // check that internet does not get restricted + NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // metered-ness shouldn't matter + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // add EIMS - bundled with unrestricted means it's unrestricted + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_INTERNET); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertTrue(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // just a restricted cap should be restricted regardless of meteredness + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // try 2 restricted caps + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_CBS); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.addCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + netCap = new NetworkCapabilities(); + netCap.addCapability(NET_CAPABILITY_CBS); + netCap.addCapability(NET_CAPABILITY_EIMS); + netCap.removeCapability(NET_CAPABILITY_NOT_METERED); + netCap.maybeMarkCapabilitiesRestricted(); + assertFalse(netCap.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + + @Test + public void testDescribeImmutableDifferences() { + NetworkCapabilities nc1; + NetworkCapabilities nc2; + + // Transports changing + nc1 = new NetworkCapabilities().addTransportType(TRANSPORT_CELLULAR); + nc2 = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI); + assertNotEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // Mutable capability changing + nc1 = new NetworkCapabilities().addCapability(NET_CAPABILITY_VALIDATED); + nc2 = new NetworkCapabilities(); + assertEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // NOT_METERED changing (http://b/63326103) + nc1 = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_NOT_METERED) + .addCapability(NET_CAPABILITY_INTERNET); + nc2 = new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET); + assertEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // Immutable capability changing + nc1 = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + nc2 = new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET); + assertNotEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + + // Specifier changing + nc1 = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI); + nc2 = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier("eth42")); + assertNotEquals("", nc1.describeImmutableDifferences(nc2)); + assertEquals("", nc1.describeImmutableDifferences(nc1)); + } + + @Test + public void testLinkBandwidthUtils() { + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, NetworkCapabilities + .minBandwidth(LINK_BANDWIDTH_UNSPECIFIED, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(10, NetworkCapabilities + .minBandwidth(LINK_BANDWIDTH_UNSPECIFIED, 10)); + assertEquals(10, NetworkCapabilities + .minBandwidth(10, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(10, NetworkCapabilities + .minBandwidth(10, 20)); + + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, NetworkCapabilities + .maxBandwidth(LINK_BANDWIDTH_UNSPECIFIED, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(10, NetworkCapabilities + .maxBandwidth(LINK_BANDWIDTH_UNSPECIFIED, 10)); + assertEquals(10, NetworkCapabilities + .maxBandwidth(10, LINK_BANDWIDTH_UNSPECIFIED)); + assertEquals(20, NetworkCapabilities + .maxBandwidth(10, 20)); + } + + @Test + public void testSetUids() { + final NetworkCapabilities netCap = new NetworkCapabilities(); + // Null uids match all UIDs + netCap.setUids(null); + assertTrue(netCap.appliesToUid(10)); + assertTrue(netCap.appliesToUid(200)); + assertTrue(netCap.appliesToUid(3000)); + assertTrue(netCap.appliesToUid(10010)); + assertTrue(netCap.appliesToUidRange(new UidRange(50, 100))); + assertTrue(netCap.appliesToUidRange(new UidRange(70, 72))); + assertTrue(netCap.appliesToUidRange(new UidRange(3500, 3912))); + assertTrue(netCap.appliesToUidRange(new UidRange(1, 100000))); + + if (isAtLeastS()) { + final Set<Range<Integer>> uids = new ArraySet<>(); + uids.add(uidRange(50, 100)); + uids.add(uidRange(3000, 4000)); + netCap.setUids(uids); + assertTrue(netCap.appliesToUid(50)); + assertTrue(netCap.appliesToUid(80)); + assertTrue(netCap.appliesToUid(100)); + assertTrue(netCap.appliesToUid(3000)); + assertTrue(netCap.appliesToUid(3001)); + assertFalse(netCap.appliesToUid(10)); + assertFalse(netCap.appliesToUid(25)); + assertFalse(netCap.appliesToUid(49)); + assertFalse(netCap.appliesToUid(101)); + assertFalse(netCap.appliesToUid(2000)); + assertFalse(netCap.appliesToUid(100000)); + + assertTrue(netCap.appliesToUidRange(new UidRange(50, 100))); + assertTrue(netCap.appliesToUidRange(new UidRange(70, 72))); + assertTrue(netCap.appliesToUidRange(new UidRange(3500, 3912))); + assertFalse(netCap.appliesToUidRange(new UidRange(1, 100))); + assertFalse(netCap.appliesToUidRange(new UidRange(49, 100))); + assertFalse(netCap.appliesToUidRange(new UidRange(1, 10))); + assertFalse(netCap.appliesToUidRange(new UidRange(60, 101))); + assertFalse(netCap.appliesToUidRange(new UidRange(60, 3400))); + + NetworkCapabilities netCap2 = new NetworkCapabilities(); + // A new netcap object has null UIDs, so anything will satisfy it. + assertTrue(netCap2.satisfiedByUids(netCap)); + // Still not equal though. + assertFalse(netCap2.equalsUids(netCap)); + netCap2.setUids(uids); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.equalsUids(netCap2)); + assertTrue(netCap2.equalsUids(netCap)); + + uids.add(uidRange(600, 700)); + netCap2.setUids(uids); + assertFalse(netCap2.satisfiedByUids(netCap)); + assertFalse(netCap.appliesToUid(650)); + assertTrue(netCap2.appliesToUid(650)); + netCap.combineCapabilities(netCap2); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.appliesToUid(650)); + assertFalse(netCap.appliesToUid(500)); + + assertTrue(new NetworkCapabilities().satisfiedByUids(netCap)); + netCap.combineCapabilities(new NetworkCapabilities()); + assertTrue(netCap.appliesToUid(500)); + assertTrue(netCap.appliesToUidRange(new UidRange(1, 100000))); + assertFalse(netCap2.appliesToUid(500)); + assertFalse(netCap2.appliesToUidRange(new UidRange(1, 100000))); + assertTrue(new NetworkCapabilities().satisfiedByUids(netCap)); + + // Null uids satisfies everything. + netCap.setUids(null); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.satisfiedByUids(netCap2)); + netCap2.setUids(null); + assertTrue(netCap2.satisfiedByUids(netCap)); + assertTrue(netCap.satisfiedByUids(netCap2)); + } + } + + @Test + public void testParcelNetworkCapabilities() { + final Set<Range<Integer>> uids = new ArraySet<>(); + uids.add(uidRange(50, 100)); + uids.add(uidRange(3000, 4000)); + final NetworkCapabilities netCap = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_NOT_METERED); + if (isAtLeastS()) { + netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)); + netCap.setUids(uids); + } + if (isAtLeastR()) { + netCap.setOwnerUid(123); + netCap.setAdministratorUids(new int[] {5, 11}); + } + assertParcelingIsLossless(netCap); + netCap.setSSID(TEST_SSID); + testParcelSane(netCap); + } + + @Test + public void testParcelNetworkCapabilitiesWithRequestorUidAndPackageName() { + final NetworkCapabilities netCap = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_NOT_METERED); + if (isAtLeastR()) { + netCap.setRequestorPackageName("com.android.test"); + netCap.setRequestorUid(9304); + } + assertParcelingIsLossless(netCap); + netCap.setSSID(TEST_SSID); + testParcelSane(netCap); + } + + private void testParcelSane(NetworkCapabilities cap) { + if (isAtLeastS()) { + assertParcelSane(cap, 16); + } else if (isAtLeastR()) { + assertParcelSane(cap, 15); + } else { + assertParcelSane(cap, 11); + } + } + + private static NetworkCapabilities createNetworkCapabilitiesWithTransportInfo() { + return new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_NOT_METERED) + .setSSID(TEST_SSID) + .setTransportInfo(new TestTransportInfo()) + .setRequestorPackageName("com.android.test") + .setRequestorUid(9304); + } + + @Test + public void testNetworkCapabilitiesCopyWithNoRedactions() { + assumeTrue(isAtLeastS()); + + final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo(); + final NetworkCapabilities netCapWithNoRedactions = + new NetworkCapabilities(netCap, NetworkCapabilities.REDACT_NONE); + TestTransportInfo testTransportInfo = + (TestTransportInfo) netCapWithNoRedactions.getTransportInfo(); + assertFalse(testTransportInfo.locationRedacted); + assertFalse(testTransportInfo.localMacAddressRedacted); + assertFalse(testTransportInfo.settingsRedacted); + } + + @Test + public void testNetworkCapabilitiesCopyWithoutLocationSensitiveFields() { + assumeTrue(isAtLeastS()); + + final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo(); + final NetworkCapabilities netCapWithNoRedactions = + new NetworkCapabilities(netCap, REDACT_FOR_ACCESS_FINE_LOCATION); + TestTransportInfo testTransportInfo = + (TestTransportInfo) netCapWithNoRedactions.getTransportInfo(); + assertTrue(testTransportInfo.locationRedacted); + assertFalse(testTransportInfo.localMacAddressRedacted); + assertFalse(testTransportInfo.settingsRedacted); + } + + @Test + public void testOemPaid() { + NetworkCapabilities nc = new NetworkCapabilities(); + // By default OEM_PAID is neither in the required or forbidden lists and the network is not + // restricted. + if (isAtLeastS()) { + assertFalse(nc.hasForbiddenCapability(NET_CAPABILITY_OEM_PAID)); + } + assertFalse(nc.hasCapability(NET_CAPABILITY_OEM_PAID)); + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Adding OEM_PAID to capability list should make network restricted. + nc.addCapability(NET_CAPABILITY_OEM_PAID); + nc.addCapability(NET_CAPABILITY_INTERNET); // Combine with unrestricted capability. + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_OEM_PAID)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Now let's make request for OEM_PAID network. + NetworkCapabilities nr = new NetworkCapabilities(); + nr.addCapability(NET_CAPABILITY_OEM_PAID); + nr.maybeMarkCapabilitiesRestricted(); + assertTrue(nr.satisfiedByNetworkCapabilities(nc)); + + // Request fails for network with the default capabilities. + assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities())); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testOemPrivate() { + NetworkCapabilities nc = new NetworkCapabilities(); + // By default OEM_PRIVATE is neither in the required or forbidden lists and the network is + // not restricted. + assertFalse(nc.hasForbiddenCapability(NET_CAPABILITY_OEM_PRIVATE)); + assertFalse(nc.hasCapability(NET_CAPABILITY_OEM_PRIVATE)); + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Adding OEM_PRIVATE to capability list should make network restricted. + nc.addCapability(NET_CAPABILITY_OEM_PRIVATE); + nc.addCapability(NET_CAPABILITY_INTERNET); // Combine with unrestricted capability. + nc.maybeMarkCapabilitiesRestricted(); + assertTrue(nc.hasCapability(NET_CAPABILITY_OEM_PRIVATE)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Now let's make request for OEM_PRIVATE network. + NetworkCapabilities nr = new NetworkCapabilities(); + nr.addCapability(NET_CAPABILITY_OEM_PRIVATE); + nr.maybeMarkCapabilitiesRestricted(); + assertTrue(nr.satisfiedByNetworkCapabilities(nc)); + + // Request fails for network with the default capabilities. + assertFalse(nr.satisfiedByNetworkCapabilities(new NetworkCapabilities())); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testForbiddenCapabilities() { + NetworkCapabilities network = new NetworkCapabilities(); + + NetworkCapabilities request = new NetworkCapabilities(); + assertTrue("Request: " + request + ", Network:" + network, + request.satisfiedByNetworkCapabilities(network)); + + // Requesting absence of capabilities that network doesn't have. Request should satisfy. + request.addForbiddenCapability(NET_CAPABILITY_WIFI_P2P); + request.addForbiddenCapability(NET_CAPABILITY_NOT_METERED); + assertTrue(request.satisfiedByNetworkCapabilities(network)); + assertArrayEquals(new int[]{NET_CAPABILITY_WIFI_P2P, + NET_CAPABILITY_NOT_METERED}, + request.getForbiddenCapabilities()); + + // This is a default capability, just want to make sure its there because we use it below. + assertTrue(network.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Verify that adding forbidden capability will effectively remove it from capability list. + request.addForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.hasForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertFalse(request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + // Now this request won't be satisfied because network contains NOT_RESTRICTED. + assertFalse(request.satisfiedByNetworkCapabilities(network)); + network.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.satisfiedByNetworkCapabilities(network)); + + // Verify that adding capability will effectively remove it from forbidden list + request.addCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertFalse(request.hasForbiddenCapability(NET_CAPABILITY_NOT_RESTRICTED)); + + assertFalse(request.satisfiedByNetworkCapabilities(network)); + network.addCapability(NET_CAPABILITY_NOT_RESTRICTED); + assertTrue(request.satisfiedByNetworkCapabilities(network)); + } + + @Test + public void testConnectivityManagedCapabilities() { + NetworkCapabilities nc = new NetworkCapabilities(); + assertFalse(nc.hasConnectivityManagedCapability()); + // Check every single system managed capability. + nc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + assertTrue(nc.hasConnectivityManagedCapability()); + nc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + nc.addCapability(NET_CAPABILITY_FOREGROUND); + assertTrue(nc.hasConnectivityManagedCapability()); + nc.removeCapability(NET_CAPABILITY_FOREGROUND); + nc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + assertTrue(nc.hasConnectivityManagedCapability()); + nc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY); + nc.addCapability(NET_CAPABILITY_VALIDATED); + assertTrue(nc.hasConnectivityManagedCapability()); + } + + @Test + public void testEqualsNetCapabilities() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + assertTrue(nc1.equalsNetCapabilities(nc2)); + assertEquals(nc1, nc2); + + nc1.addCapability(NET_CAPABILITY_MMS); + assertFalse(nc1.equalsNetCapabilities(nc2)); + assertNotEquals(nc1, nc2); + nc2.addCapability(NET_CAPABILITY_MMS); + assertTrue(nc1.equalsNetCapabilities(nc2)); + assertEquals(nc1, nc2); + + if (isAtLeastS()) { + nc1.addForbiddenCapability(NET_CAPABILITY_INTERNET); + assertFalse(nc1.equalsNetCapabilities(nc2)); + nc2.addForbiddenCapability(NET_CAPABILITY_INTERNET); + assertTrue(nc1.equalsNetCapabilities(nc2)); + + // Remove a required capability doesn't affect forbidden capabilities. + // This is a behaviour change from R to S. + nc1.removeCapability(NET_CAPABILITY_INTERNET); + assertTrue(nc1.equalsNetCapabilities(nc2)); + + nc1.removeForbiddenCapability(NET_CAPABILITY_INTERNET); + assertFalse(nc1.equalsNetCapabilities(nc2)); + nc2.removeForbiddenCapability(NET_CAPABILITY_INTERNET); + assertTrue(nc1.equalsNetCapabilities(nc2)); + } + } + + @Test + public void testSSID() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + assertTrue(nc2.satisfiedBySSID(nc1)); + + nc1.setSSID(TEST_SSID); + assertTrue(nc2.satisfiedBySSID(nc1)); + nc2.setSSID("different " + TEST_SSID); + assertFalse(nc2.satisfiedBySSID(nc1)); + + assertTrue(nc1.satisfiedByImmutableNetworkCapabilities(nc2)); + assertFalse(nc1.satisfiedByNetworkCapabilities(nc2)); + } + + private ArraySet<Range<Integer>> uidRanges(int from, int to) { + final ArraySet<Range<Integer>> range = new ArraySet<>(1); + range.add(uidRange(from, to)); + return range; + } + + private Range<Integer> uidRange(int from, int to) { + return new Range<Integer>(from, to); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testSetAdministratorUids() { + NetworkCapabilities nc = + new NetworkCapabilities().setAdministratorUids(new int[] {2, 1, 3}); + + assertArrayEquals(new int[] {1, 2, 3}, nc.getAdministratorUids()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testSetAdministratorUidsWithDuplicates() { + try { + new NetworkCapabilities().setAdministratorUids(new int[] {1, 1}); + fail("Expected IllegalArgumentException for duplicate uids"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testCombineCapabilities() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + + if (isAtLeastS()) { + nc1.addForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + } + nc1.addCapability(NET_CAPABILITY_NOT_ROAMING); + assertNotEquals(nc1, nc2); + nc2.combineCapabilities(nc1); + assertEquals(nc1, nc2); + assertTrue(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + if (isAtLeastS()) { + assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL)); + } + + if (isAtLeastS()) { + // This will effectively move NOT_ROAMING capability from required to forbidden for nc1. + nc1.addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING); + // It is not allowed to have the same capability in both wanted and forbidden list. + assertThrows(IllegalArgumentException.class, () -> nc2.combineCapabilities(nc1)); + // Remove forbidden capability to continue other tests. + nc1.removeForbiddenCapability(NET_CAPABILITY_NOT_ROAMING); + } + + nc1.setSSID(TEST_SSID); + nc2.combineCapabilities(nc1); + if (isAtLeastR()) { + assertTrue(TEST_SSID.equals(nc2.getSsid())); + } + + // Because they now have the same SSID, the following call should not throw + nc2.combineCapabilities(nc1); + + nc1.setSSID(DIFFERENT_TEST_SSID); + try { + nc2.combineCapabilities(nc1); + fail("Expected IllegalStateException: can't combine different SSIDs"); + } catch (IllegalStateException expected) {} + nc1.setSSID(TEST_SSID); + + if (isAtLeastS()) { + nc1.setUids(uidRanges(10, 13)); + assertNotEquals(nc1, nc2); + nc2.combineCapabilities(nc1); // Everything + 10~13 is still everything. + assertNotEquals(nc1, nc2); + nc1.combineCapabilities(nc2); // 10~13 + everything is everything. + assertEquals(nc1, nc2); + nc1.setUids(uidRanges(10, 13)); + nc2.setUids(uidRanges(20, 23)); + assertNotEquals(nc1, nc2); + nc1.combineCapabilities(nc2); + assertTrue(nc1.appliesToUid(12)); + assertFalse(nc2.appliesToUid(12)); + assertTrue(nc1.appliesToUid(22)); + assertTrue(nc2.appliesToUid(22)); + + // Verify the subscription id list can be combined only when they are equal. + nc1.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)); + nc2.setSubscriptionIds(Set.of(TEST_SUBID2)); + assertThrows(IllegalStateException.class, () -> nc2.combineCapabilities(nc1)); + + nc2.setSubscriptionIds(Set.of()); + assertThrows(IllegalStateException.class, () -> nc2.combineCapabilities(nc1)); + + nc2.setSubscriptionIds(Set.of(TEST_SUBID2, TEST_SUBID1)); + nc2.combineCapabilities(nc1); + assertEquals(Set.of(TEST_SUBID2, TEST_SUBID1), nc2.getSubscriptionIds()); + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCombineCapabilities_AdministratorUids() { + final NetworkCapabilities nc1 = new NetworkCapabilities(); + final NetworkCapabilities nc2 = new NetworkCapabilities(); + + final int[] adminUids = {3, 6, 12}; + nc1.setAdministratorUids(adminUids); + nc2.combineCapabilities(nc1); + assertTrue(nc2.equalsAdministratorUids(nc1)); + assertArrayEquals(nc2.getAdministratorUids(), adminUids); + + final int[] adminUidsOtherOrder = {3, 12, 6}; + nc1.setAdministratorUids(adminUidsOtherOrder); + assertTrue(nc2.equalsAdministratorUids(nc1)); + + final int[] adminUids2 = {11, 1, 12, 3, 6}; + nc1.setAdministratorUids(adminUids2); + assertFalse(nc2.equalsAdministratorUids(nc1)); + assertFalse(Arrays.equals(nc2.getAdministratorUids(), adminUids2)); + try { + nc2.combineCapabilities(nc1); + fail("Shouldn't be able to combine different lists of admin UIDs"); + } catch (IllegalStateException expected) { } + } + + @Test + public void testSetCapabilities() { + final int[] REQUIRED_CAPABILITIES = new int[] { + NET_CAPABILITY_INTERNET, NET_CAPABILITY_NOT_VPN }; + + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + + nc1.setCapabilities(REQUIRED_CAPABILITIES); + assertArrayEquals(REQUIRED_CAPABILITIES, nc1.getCapabilities()); + + // Verify that setting and adding capabilities leads to the same object state. + nc2.clearAll(); + for (int cap : REQUIRED_CAPABILITIES) { + nc2.addCapability(cap); + } + assertEquals(nc1, nc2); + + if (isAtLeastS()) { + final int[] forbiddenCapabilities = new int[]{ + NET_CAPABILITY_NOT_METERED, NET_CAPABILITY_NOT_RESTRICTED }; + + nc1.setCapabilities(REQUIRED_CAPABILITIES, forbiddenCapabilities); + assertArrayEquals(REQUIRED_CAPABILITIES, nc1.getCapabilities()); + assertArrayEquals(forbiddenCapabilities, nc1.getForbiddenCapabilities()); + + nc2.clearAll(); + for (int cap : REQUIRED_CAPABILITIES) { + nc2.addCapability(cap); + } + for (int cap : forbiddenCapabilities) { + nc2.addForbiddenCapability(cap); + } + assertEquals(nc1, nc2); + } + } + + @Test + public void testSetNetworkSpecifierOnMultiTransportNc() { + // Sequence 1: Transport + Transport + NetworkSpecifier + NetworkCapabilities nc1 = new NetworkCapabilities(); + nc1.addTransportType(TRANSPORT_CELLULAR).addTransportType(TRANSPORT_WIFI); + try { + nc1.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier("eth0")); + fail("Cannot set NetworkSpecifier on a NetworkCapability with multiple transports!"); + } catch (IllegalStateException expected) { + // empty + } + + // Sequence 2: Transport + NetworkSpecifier + Transport + NetworkCapabilities nc2 = new NetworkCapabilities(); + nc2.addTransportType(TRANSPORT_CELLULAR).setNetworkSpecifier( + CompatUtil.makeEthernetNetworkSpecifier("testtap3")); + try { + nc2.addTransportType(TRANSPORT_WIFI); + fail("Cannot set a second TransportType of a network which has a NetworkSpecifier!"); + } catch (IllegalStateException expected) { + // empty + } + } + + @Test + public void testSetTransportInfoOnMultiTransportNc() { + // Sequence 1: Transport + Transport + TransportInfo + NetworkCapabilities nc1 = new NetworkCapabilities(); + nc1.addTransportType(TRANSPORT_CELLULAR).addTransportType(TRANSPORT_WIFI) + .setTransportInfo(new TestTransportInfo()); + + // Sequence 2: Transport + NetworkSpecifier + Transport + NetworkCapabilities nc2 = new NetworkCapabilities(); + nc2.addTransportType(TRANSPORT_CELLULAR).setTransportInfo(new TestTransportInfo()) + .addTransportType(TRANSPORT_WIFI); + } + + @Test + public void testCombineTransportInfo() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + nc1.setTransportInfo(new TestTransportInfo()); + + NetworkCapabilities nc2 = new NetworkCapabilities(); + // new TransportInfo so that object is not #equals to nc1's TransportInfo (that's where + // combine fails) + nc2.setTransportInfo(new TestTransportInfo()); + + try { + nc1.combineCapabilities(nc2); + fail("Should not be able to combine NetworkCabilities which contain TransportInfos"); + } catch (IllegalStateException expected) { + // empty + } + + // verify that can combine with identical TransportInfo objects + NetworkCapabilities nc3 = new NetworkCapabilities(); + nc3.setTransportInfo(nc1.getTransportInfo()); + nc1.combineCapabilities(nc3); + } + + @Test + public void testSet() { + NetworkCapabilities nc1 = new NetworkCapabilities(); + NetworkCapabilities nc2 = new NetworkCapabilities(); + + if (isAtLeastS()) { + nc1.addForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL); + } + nc1.addCapability(NET_CAPABILITY_NOT_ROAMING); + assertNotEquals(nc1, nc2); + nc2.set(nc1); + assertEquals(nc1, nc2); + assertTrue(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + if (isAtLeastS()) { + assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_CAPTIVE_PORTAL)); + } + + if (isAtLeastS()) { + // This will effectively move NOT_ROAMING capability from required to forbidden for nc1. + nc1.addForbiddenCapability(NET_CAPABILITY_NOT_ROAMING); + } + nc1.setSSID(TEST_SSID); + nc2.set(nc1); + assertEquals(nc1, nc2); + if (isAtLeastS()) { + // Contrary to combineCapabilities, set() will have removed the NOT_ROAMING capability + // from nc2. + assertFalse(nc2.hasCapability(NET_CAPABILITY_NOT_ROAMING)); + assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_NOT_ROAMING)); + } + + if (isAtLeastR()) { + assertTrue(TEST_SSID.equals(nc2.getSsid())); + } + + nc1.setSSID(DIFFERENT_TEST_SSID); + nc2.set(nc1); + assertEquals(nc1, nc2); + if (isAtLeastR()) { + assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid())); + } + if (isAtLeastS()) { + nc1.setUids(uidRanges(10, 13)); + } else { + nc1.setUids(null); + } + nc2.set(nc1); // Overwrites, as opposed to combineCapabilities + assertEquals(nc1, nc2); + + if (isAtLeastS()) { + assertThrows(NullPointerException.class, () -> nc1.setSubscriptionIds(null)); + nc1.setSubscriptionIds(Set.of()); + nc2.set(nc1); + assertEquals(nc1, nc2); + + nc1.setSubscriptionIds(Set.of(TEST_SUBID1)); + nc2.set(nc1); + assertEquals(nc1, nc2); + + nc2.setSubscriptionIds(Set.of(TEST_SUBID2, TEST_SUBID1)); + nc2.set(nc1); + assertEquals(nc1, nc2); + + nc2.setSubscriptionIds(Set.of(TEST_SUBID3, TEST_SUBID2)); + assertNotEquals(nc1, nc2); + } + } + + @Test + public void testGetTransportTypes() { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.addTransportType(TRANSPORT_CELLULAR); + nc.addTransportType(TRANSPORT_WIFI); + nc.addTransportType(TRANSPORT_VPN); + nc.addTransportType(TRANSPORT_TEST); + + final int[] transportTypes = nc.getTransportTypes(); + assertEquals(4, transportTypes.length); + assertEquals(TRANSPORT_CELLULAR, transportTypes[0]); + assertEquals(TRANSPORT_WIFI, transportTypes[1]); + assertEquals(TRANSPORT_VPN, transportTypes[2]); + assertEquals(TRANSPORT_TEST, transportTypes[3]); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testTelephonyNetworkSpecifier() { + final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1); + final NetworkCapabilities nc1 = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .build(); + assertEquals(specifier, nc1.getNetworkSpecifier()); + try { + final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() + .setNetworkSpecifier(specifier) + .build(); + fail("Must have a single transport type. Without transport type or multiple transport" + + " types is invalid."); + } catch (IllegalStateException expected) { } + } + + @Test + public void testWifiAwareNetworkSpecifier() { + final NetworkCapabilities nc = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI_AWARE); + // If NetworkSpecifier is not set, the default value is null. + assertNull(nc.getNetworkSpecifier()); + final WifiAwareNetworkSpecifier specifier = new WifiAwareNetworkSpecifier.Builder( + mDiscoverySession, mPeerHandle).build(); + nc.setNetworkSpecifier(specifier); + assertEquals(specifier, nc.getNetworkSpecifier()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testAdministratorUidsAndOwnerUid() { + // Test default owner uid. + // If the owner uid is not set, the default value should be Process.INVALID_UID. + final NetworkCapabilities nc1 = new NetworkCapabilities.Builder().build(); + assertEquals(INVALID_UID, nc1.getOwnerUid()); + // Test setAdministratorUids and getAdministratorUids. + final int[] administratorUids = {1001, 10001}; + final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() + .setAdministratorUids(administratorUids) + .build(); + assertTrue(Arrays.equals(administratorUids, nc2.getAdministratorUids())); + // Test setOwnerUid and getOwnerUid. + // The owner UID must be included in administrator UIDs, or throw IllegalStateException. + try { + final NetworkCapabilities nc3 = new NetworkCapabilities.Builder() + .setOwnerUid(1001) + .build(); + fail("The owner UID must be included in administrator UIDs."); + } catch (IllegalStateException expected) { } + final NetworkCapabilities nc4 = new NetworkCapabilities.Builder() + .setAdministratorUids(administratorUids) + .setOwnerUid(1001) + .build(); + assertEquals(1001, nc4.getOwnerUid()); + try { + final NetworkCapabilities nc5 = new NetworkCapabilities.Builder() + .setAdministratorUids(null) + .build(); + fail("Should not set null into setAdministratorUids"); + } catch (NullPointerException expected) { } + } + + private static NetworkCapabilities capsWithSubIds(Integer ... subIds) { + // Since the NetworkRequest would put NOT_VCN_MANAGED capabilities in general, for + // every NetworkCapabilities that simulates networks needs to add it too in order to + // satisfy these requests. + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .setSubscriptionIds(new ArraySet<>(subIds)).build(); + assertEquals(new ArraySet<>(subIds), nc.getSubscriptionIds()); + return nc; + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testSubIds() throws Exception { + final NetworkCapabilities ncWithoutId = capsWithSubIds(); + final NetworkCapabilities ncWithId = capsWithSubIds(TEST_SUBID1); + final NetworkCapabilities ncWithOtherIds = capsWithSubIds(TEST_SUBID1, TEST_SUBID3); + final NetworkCapabilities ncWithoutRequestedIds = capsWithSubIds(TEST_SUBID3); + + final NetworkRequest requestWithoutId = new NetworkRequest.Builder().build(); + assertEmpty(requestWithoutId.networkCapabilities.getSubscriptionIds()); + final NetworkRequest requestWithIds = new NetworkRequest.Builder() + .setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2)).build(); + assertEquals(Set.of(TEST_SUBID1, TEST_SUBID2), + requestWithIds.networkCapabilities.getSubscriptionIds()); + + assertFalse(requestWithIds.canBeSatisfiedBy(ncWithoutId)); + assertTrue(requestWithIds.canBeSatisfiedBy(ncWithOtherIds)); + assertFalse(requestWithIds.canBeSatisfiedBy(ncWithoutRequestedIds)); + assertTrue(requestWithIds.canBeSatisfiedBy(ncWithId)); + assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithoutId)); + assertTrue(requestWithoutId.canBeSatisfiedBy(ncWithId)); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testEqualsSubIds() throws Exception { + assertEquals(capsWithSubIds(), capsWithSubIds()); + assertNotEquals(capsWithSubIds(), capsWithSubIds(TEST_SUBID1)); + assertEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID1)); + assertNotEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID2)); + assertNotEquals(capsWithSubIds(TEST_SUBID1), capsWithSubIds(TEST_SUBID2, TEST_SUBID1)); + assertEquals(capsWithSubIds(TEST_SUBID1, TEST_SUBID2), + capsWithSubIds(TEST_SUBID2, TEST_SUBID1)); + } + + @Test + public void testLinkBandwidthKbps() { + final NetworkCapabilities nc = new NetworkCapabilities(); + // The default value of LinkDown/UpstreamBandwidthKbps should be LINK_BANDWIDTH_UNSPECIFIED. + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, nc.getLinkDownstreamBandwidthKbps()); + assertEquals(LINK_BANDWIDTH_UNSPECIFIED, nc.getLinkUpstreamBandwidthKbps()); + nc.setLinkDownstreamBandwidthKbps(512); + nc.setLinkUpstreamBandwidthKbps(128); + assertEquals(512, nc.getLinkDownstreamBandwidthKbps()); + assertNotEquals(128, nc.getLinkDownstreamBandwidthKbps()); + assertEquals(128, nc.getLinkUpstreamBandwidthKbps()); + assertNotEquals(512, nc.getLinkUpstreamBandwidthKbps()); + } + + private int getMaxTransport() { + if (!isAtLeastS() && MAX_TRANSPORT == TRANSPORT_USB) return MAX_TRANSPORT - 1; + return MAX_TRANSPORT; + } + + @Test + public void testSignalStrength() { + final NetworkCapabilities nc = new NetworkCapabilities(); + // The default value of signal strength should be SIGNAL_STRENGTH_UNSPECIFIED. + assertEquals(SIGNAL_STRENGTH_UNSPECIFIED, nc.getSignalStrength()); + nc.setSignalStrength(-80); + assertEquals(-80, nc.getSignalStrength()); + assertNotEquals(-50, nc.getSignalStrength()); + } + + private void assertNoTransport(NetworkCapabilities nc) { + for (int i = MIN_TRANSPORT; i <= getMaxTransport(); i++) { + assertFalse(nc.hasTransport(i)); + } + } + + // Checks that all transport types from MIN_TRANSPORT to maxTransportType are set and all + // transport types from maxTransportType + 1 to MAX_TRANSPORT are not set when positiveSequence + // is true. If positiveSequence is false, then the check sequence is opposite. + private void checkCurrentTransportTypes(NetworkCapabilities nc, int maxTransportType, + boolean positiveSequence) { + for (int i = MIN_TRANSPORT; i <= maxTransportType; i++) { + if (positiveSequence) { + assertTrue(nc.hasTransport(i)); + } else { + assertFalse(nc.hasTransport(i)); + } + } + for (int i = getMaxTransport(); i > maxTransportType; i--) { + if (positiveSequence) { + assertFalse(nc.hasTransport(i)); + } else { + assertTrue(nc.hasTransport(i)); + } + } + } + + @Test + public void testMultipleTransportTypes() { + final NetworkCapabilities nc = new NetworkCapabilities(); + assertNoTransport(nc); + // Test adding multiple transport types. + for (int i = MIN_TRANSPORT; i <= getMaxTransport(); i++) { + nc.addTransportType(i); + checkCurrentTransportTypes(nc, i, true /* positiveSequence */); + } + // Test removing multiple transport types. + for (int i = MIN_TRANSPORT; i <= getMaxTransport(); i++) { + nc.removeTransportType(i); + checkCurrentTransportTypes(nc, i, false /* positiveSequence */); + } + assertNoTransport(nc); + nc.addTransportType(TRANSPORT_WIFI); + assertTrue(nc.hasTransport(TRANSPORT_WIFI)); + assertFalse(nc.hasTransport(TRANSPORT_VPN)); + nc.addTransportType(TRANSPORT_VPN); + assertTrue(nc.hasTransport(TRANSPORT_WIFI)); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + nc.removeTransportType(TRANSPORT_WIFI); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + nc.removeTransportType(TRANSPORT_VPN); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + assertFalse(nc.hasTransport(TRANSPORT_VPN)); + assertNoTransport(nc); + } + + @Test + public void testAddAndRemoveTransportType() { + final NetworkCapabilities nc = new NetworkCapabilities(); + try { + nc.addTransportType(-1); + fail("Should not set invalid transport type into addTransportType"); + } catch (IllegalArgumentException expected) { } + try { + nc.removeTransportType(-1); + fail("Should not set invalid transport type into removeTransportType"); + } catch (IllegalArgumentException e) { } + } + + /** + * Test TransportInfo to verify redaction mechanism. + */ + private static class TestTransportInfo implements TransportInfo { + public final boolean locationRedacted; + public final boolean localMacAddressRedacted; + public final boolean settingsRedacted; + + TestTransportInfo() { + locationRedacted = false; + localMacAddressRedacted = false; + settingsRedacted = false; + } + + TestTransportInfo(boolean locationRedacted, + boolean localMacAddressRedacted, + boolean settingsRedacted) { + this.locationRedacted = locationRedacted; + this.localMacAddressRedacted = + localMacAddressRedacted; + this.settingsRedacted = settingsRedacted; + } + + @Override + public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) { + return new TestTransportInfo( + (redactions & NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION) != 0, + (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0, + (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0 + ); + } + + @Override + public @NetworkCapabilities.RedactionType long getApplicableRedactions() { + return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS + | REDACT_FOR_NETWORK_SETTINGS; + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testBuilder() { + final int ownerUid = 1001; + final int signalStrength = -80; + final int requestUid = 10100; + final int[] administratorUids = {ownerUid, 10001}; + final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier(1); + final TransportInfo transportInfo = new TransportInfo() {}; + final String ssid = "TEST_SSID"; + final String packageName = "com.google.test.networkcapabilities"; + final NetworkCapabilities nc = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI) + .addTransportType(TRANSPORT_CELLULAR) + .removeTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_EIMS) + .addCapability(NET_CAPABILITY_CBS) + .removeCapability(NET_CAPABILITY_CBS) + .setAdministratorUids(administratorUids) + .setOwnerUid(ownerUid) + .setLinkDownstreamBandwidthKbps(512) + .setLinkUpstreamBandwidthKbps(128) + .setNetworkSpecifier(specifier) + .setTransportInfo(transportInfo) + .setSignalStrength(signalStrength) + .setSsid(ssid) + .setRequestorUid(requestUid) + .setRequestorPackageName(packageName) + .build(); + assertEquals(1, nc.getTransportTypes().length); + assertEquals(TRANSPORT_WIFI, nc.getTransportTypes()[0]); + assertTrue(nc.hasCapability(NET_CAPABILITY_EIMS)); + assertFalse(nc.hasCapability(NET_CAPABILITY_CBS)); + assertTrue(Arrays.equals(administratorUids, nc.getAdministratorUids())); + assertEquals(ownerUid, nc.getOwnerUid()); + assertEquals(512, nc.getLinkDownstreamBandwidthKbps()); + assertNotEquals(128, nc.getLinkDownstreamBandwidthKbps()); + assertEquals(128, nc.getLinkUpstreamBandwidthKbps()); + assertNotEquals(512, nc.getLinkUpstreamBandwidthKbps()); + assertEquals(specifier, nc.getNetworkSpecifier()); + assertEquals(transportInfo, nc.getTransportInfo()); + assertEquals(signalStrength, nc.getSignalStrength()); + assertNotEquals(-50, nc.getSignalStrength()); + assertEquals(ssid, nc.getSsid()); + assertEquals(requestUid, nc.getRequestorUid()); + assertEquals(packageName, nc.getRequestorPackageName()); + // Cannot assign null into NetworkCapabilities.Builder + try { + final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(null); + fail("Should not set null into NetworkCapabilities.Builder"); + } catch (NullPointerException expected) { } + assertEquals(nc, new NetworkCapabilities.Builder(nc).build()); + + if (isAtLeastS()) { + final NetworkCapabilities nc2 = new NetworkCapabilities.Builder() + .setSubscriptionIds(Set.of(TEST_SUBID1)).build(); + assertEquals(Set.of(TEST_SUBID1), nc2.getSubscriptionIds()); + } + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBuilderWithoutDefaultCap() { + final NetworkCapabilities nc = + NetworkCapabilities.Builder.withoutDefaultCapabilities().build(); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertFalse(nc.hasCapability(NET_CAPABILITY_TRUSTED)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_VPN)); + // Ensure test case fails if new net cap is added into default cap but no update here. + assertEquals(0, nc.getCapabilities().length); + } +} diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7424157bea74ebfbd3ccefc46651e565546a45e0 --- /dev/null +++ b/tests/common/java/android/net/NetworkProviderTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.app.Instrumentation +import android.content.Context +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkProviderTest.TestNetworkCallback.CallbackEntry.OnUnavailable +import android.net.NetworkProviderTest.TestNetworkProvider.CallbackEntry.OnNetworkRequestWithdrawn +import android.net.NetworkProviderTest.TestNetworkProvider.CallbackEntry.OnNetworkRequested +import android.os.Build +import android.os.HandlerThread +import android.os.Looper +import androidx.test.InstrumentationRegistry +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.isDevSdkInRange +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.verifyNoMoreInteractions +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +private const val DEFAULT_TIMEOUT_MS = 5000L +private val instrumentation: Instrumentation + get() = InstrumentationRegistry.getInstrumentation() +private val context: Context get() = InstrumentationRegistry.getContext() +private val PROVIDER_NAME = "NetworkProviderTest" + +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +class NetworkProviderTest { + private val mCm = context.getSystemService(ConnectivityManager::class.java) + private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread") + + @Before + fun setUp() { + instrumentation.getUiAutomation().adoptShellPermissionIdentity() + mHandlerThread.start() + } + + @After + fun tearDown() { + mHandlerThread.quitSafely() + instrumentation.getUiAutomation().dropShellPermissionIdentity() + } + + private class TestNetworkProvider(context: Context, looper: Looper) : + NetworkProvider(context, looper, PROVIDER_NAME) { + private val seenEvents = ArrayTrackRecord<CallbackEntry>().newReadHead() + + sealed class CallbackEntry { + data class OnNetworkRequested( + val request: NetworkRequest, + val score: Int, + val id: Int + ) : CallbackEntry() + data class OnNetworkRequestWithdrawn(val request: NetworkRequest) : CallbackEntry() + } + + override fun onNetworkRequested(request: NetworkRequest, score: Int, id: Int) { + seenEvents.add(OnNetworkRequested(request, score, id)) + } + + override fun onNetworkRequestWithdrawn(request: NetworkRequest) { + seenEvents.add(OnNetworkRequestWithdrawn(request)) + } + + inline fun <reified T : CallbackEntry> expectCallback( + crossinline predicate: (T) -> Boolean + ) = seenEvents.poll(DEFAULT_TIMEOUT_MS) { it is T && predicate(it) } + } + + private fun createNetworkProvider(ctx: Context = context): TestNetworkProvider { + return TestNetworkProvider(ctx, mHandlerThread.looper) + } + + @Test + fun testOnNetworkRequested() { + val provider = createNetworkProvider() + assertEquals(provider.getProviderId(), NetworkProvider.ID_NONE) + mCm.registerNetworkProvider(provider) + assertNotEquals(provider.getProviderId(), NetworkProvider.ID_NONE) + + val specifier = CompatUtil.makeTestNetworkSpecifier( + UUID.randomUUID().toString()) + val nr: NetworkRequest = NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(specifier) + .build() + val cb = ConnectivityManager.NetworkCallback() + mCm.requestNetwork(nr, cb) + provider.expectCallback<OnNetworkRequested>() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.request.hasTransport(TRANSPORT_TEST) + } + + val initialScore = 40 + val updatedScore = 60 + val nc = NetworkCapabilities().apply { + addTransportType(NetworkCapabilities.TRANSPORT_TEST) + removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + setNetworkSpecifier(specifier) + } + val lp = LinkProperties() + val config = NetworkAgentConfig.Builder().build() + val agent = object : NetworkAgent(context, mHandlerThread.looper, "TestAgent", nc, lp, + initialScore, config, provider) {} + + provider.expectCallback<OnNetworkRequested>() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.score == initialScore && + callback.id == agent.providerId + } + + agent.sendNetworkScore(updatedScore) + provider.expectCallback<OnNetworkRequested>() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.score == updatedScore && + callback.id == agent.providerId + } + + mCm.unregisterNetworkCallback(cb) + provider.expectCallback<OnNetworkRequestWithdrawn>() { callback -> + callback.request.getNetworkSpecifier() == specifier && + callback.request.hasTransport(TRANSPORT_TEST) + } + mCm.unregisterNetworkProvider(provider) + // Provider id should be ID_NONE after unregister network provider + assertEquals(provider.getProviderId(), NetworkProvider.ID_NONE) + // unregisterNetworkProvider should not crash even if it's called on an + // already unregistered provider. + mCm.unregisterNetworkProvider(provider) + } + + private class TestNetworkCallback : ConnectivityManager.NetworkCallback() { + private val seenEvents = ArrayTrackRecord<CallbackEntry>().newReadHead() + sealed class CallbackEntry { + object OnUnavailable : CallbackEntry() + } + + override fun onUnavailable() { + seenEvents.add(OnUnavailable) + } + + inline fun <reified T : CallbackEntry> expectCallback( + crossinline predicate: (T) -> Boolean + ) = seenEvents.poll(DEFAULT_TIMEOUT_MS) { it is T && predicate(it) } + } + + @Test + fun testDeclareNetworkRequestUnfulfillable() { + val mockContext = mock(Context::class.java) + doReturn(mCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE) + val provider = createNetworkProvider(mockContext) + // ConnectivityManager not required at creation time after R + if (!isDevSdkInRange(0, Build.VERSION_CODES.R)) { + verifyNoMoreInteractions(mockContext) + } + + mCm.registerNetworkProvider(provider) + + val specifier = CompatUtil.makeTestNetworkSpecifier( + UUID.randomUUID().toString()) + val nr: NetworkRequest = NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(specifier) + .build() + + val cb = TestNetworkCallback() + mCm.requestNetwork(nr, cb) + provider.declareNetworkRequestUnfulfillable(nr) + cb.expectCallback<OnUnavailable>() { nr.getNetworkSpecifier() == specifier } + mCm.unregisterNetworkProvider(provider) + } +} diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f3409f53596f882e9978d3bfc4489ef1bc296e5f --- /dev/null +++ b/tests/common/java/android/net/NetworkSpecifierTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.assertTrue +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.Q) +class NetworkSpecifierTest { + private class TestNetworkSpecifier( + val intData: Int = 123, + val stringData: String = "init" + ) : NetworkSpecifier() { + override fun canBeSatisfiedBy(other: NetworkSpecifier?): Boolean = + other != null && + other is TestNetworkSpecifier && + other.intData >= intData && + stringData.equals(other.stringData) + + override fun redact(): NetworkSpecifier = TestNetworkSpecifier(intData, "redact") + } + + @Test + fun testRedact() { + val ns: TestNetworkSpecifier = TestNetworkSpecifier() + val redactNs = ns.redact() + assertTrue(redactNs is TestNetworkSpecifier) + assertEquals(ns.intData, redactNs.intData) + assertNotEquals(ns.stringData, redactNs.stringData) + assertTrue("redact".equals(redactNs.stringData)) + } + + @Test + fun testcanBeSatisfiedBy() { + val target: TestNetworkSpecifier = TestNetworkSpecifier() + assertFalse(target.canBeSatisfiedBy(null)) + assertTrue(target.canBeSatisfiedBy(TestNetworkSpecifier())) + val otherNs = TelephonyNetworkSpecifier.Builder().setSubscriptionId(123).build() + assertFalse(target.canBeSatisfiedBy(otherNs)) + assertTrue(target.canBeSatisfiedBy(TestNetworkSpecifier(intData = 999))) + assertFalse(target.canBeSatisfiedBy(TestNetworkSpecifier(intData = 1))) + assertFalse(target.canBeSatisfiedBy(TestNetworkSpecifier(stringData = "diff"))) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/NetworkStackTest.java b/tests/common/java/android/net/NetworkStackTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f8f9c72374ad3b8c15a8484003f7c23234a813be --- /dev/null +++ b/tests/common/java/android/net/NetworkStackTest.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 android.net; + +import static org.junit.Assert.assertEquals; + +import android.os.Build; +import android.os.IBinder; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +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.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class NetworkStackTest { + @Rule + public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule(); + + @Mock private IBinder mConnectorBinder; + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testGetService() { + NetworkStack.setServiceForTest(mConnectorBinder); + assertEquals(NetworkStack.getService(), mConnectorBinder); + } +} diff --git a/tests/common/java/android/net/NetworkStateSnapshotTest.kt b/tests/common/java/android/net/NetworkStateSnapshotTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ca4d9551f393c7e115716037b979560045c3829 --- /dev/null +++ b/tests/common/java/android/net/NetworkStateSnapshotTest.kt @@ -0,0 +1,73 @@ +/* + * 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.net.ConnectivityManager.TYPE_NONE +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.InetAddresses.parseNumericAddress +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import java.net.Inet4Address +import java.net.Inet6Address + +private const val TEST_IMSI = "imsi1" +private const val TEST_SSID = "SSID1" +private const val TEST_NETID = 123 + +private val TEST_IPV4_GATEWAY = parseNumericAddress("192.168.222.3") as Inet4Address +private val TEST_IPV6_GATEWAY = parseNumericAddress("2001:db8::1") as Inet6Address +private val TEST_IPV4_LINKADDR = LinkAddress("192.168.222.123/24") +private val TEST_IPV6_LINKADDR = LinkAddress("2001:db8::123/64") +private val TEST_IFACE = "fake0" +private val TEST_LINK_PROPERTIES = LinkProperties().apply { + interfaceName = TEST_IFACE + addLinkAddress(TEST_IPV4_LINKADDR) + addLinkAddress(TEST_IPV6_LINKADDR) + + // Add default routes + addRoute(RouteInfo(IpPrefix(parseNumericAddress("0.0.0.0"), 0), TEST_IPV4_GATEWAY)) + addRoute(RouteInfo(IpPrefix(parseNumericAddress("::"), 0), TEST_IPV6_GATEWAY)) +} + +private val TEST_CAPABILITIES = NetworkCapabilities().apply { + addTransportType(TRANSPORT_WIFI) + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true) + setSSID(TEST_SSID) +} + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class NetworkStateSnapshotTest { + + @Test + fun testParcelUnparcel() { + val emptySnapshot = NetworkStateSnapshot(Network(TEST_NETID), NetworkCapabilities(), + LinkProperties(), null, TYPE_NONE) + val snapshot = NetworkStateSnapshot( + Network(TEST_NETID), TEST_CAPABILITIES, TEST_LINK_PROPERTIES, TEST_IMSI, TYPE_WIFI) + assertParcelSane(emptySnapshot, 5) + assertParcelSane(snapshot, 5) + } +} diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7423c733c6ebd16c48557bc00fcbfa1ea5951db8 --- /dev/null +++ b/tests/common/java/android/net/NetworkTest.java @@ -0,0 +1,202 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Build; +import android.platform.test.annotations.AppModeFull; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.net.DatagramSocket; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.SocketException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkTest { + final Network mNetwork = new Network(99); + + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + @Test + public void testBindSocketOfInvalidFdThrows() throws Exception { + + final FileDescriptor fd = new FileDescriptor(); + assertFalse(fd.valid()); + + try { + mNetwork.bindSocket(fd); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + public void testBindSocketOfNonSocketFdThrows() throws Exception { + final File devNull = new File("/dev/null"); + assertTrue(devNull.canRead()); + + final FileInputStream fis = new FileInputStream(devNull); + assertTrue(null != fis.getFD()); + assertTrue(fis.getFD().valid()); + + try { + mNetwork.bindSocket(fis.getFD()); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + @AppModeFull(reason = "Socket cannot bind in instant app mode") + public void testBindSocketOfConnectedDatagramSocketThrows() throws Exception { + final DatagramSocket mDgramSocket = new DatagramSocket(0, (InetAddress) Inet6Address.ANY); + mDgramSocket.connect((InetAddress) Inet6Address.LOOPBACK, 53); + assertTrue(mDgramSocket.isConnected()); + + try { + mNetwork.bindSocket(mDgramSocket); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + public void testBindSocketOfLocalSocketThrows() throws Exception { + final LocalSocket mLocalClient = new LocalSocket(); + mLocalClient.bind(new LocalSocketAddress("testClient")); + assertTrue(mLocalClient.getFileDescriptor().valid()); + + try { + mNetwork.bindSocket(mLocalClient.getFileDescriptor()); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + + final LocalServerSocket mLocalServer = new LocalServerSocket("testServer"); + mLocalClient.connect(mLocalServer.getLocalSocketAddress()); + assertTrue(mLocalClient.isConnected()); + + try { + mNetwork.bindSocket(mLocalClient.getFileDescriptor()); + fail("SocketException not thrown"); + } catch (SocketException expected) {} + } + + @Test + public void testZeroIsObviousForDebugging() { + Network zero = new Network(0); + assertEquals(0, zero.hashCode()); + assertEquals(0, zero.getNetworkHandle()); + assertEquals("0", zero.toString()); + } + + @Test + public void testGetNetworkHandle() { + Network one = new Network(1); + Network two = new Network(2); + Network three = new Network(3); + + // None of the hashcodes are zero. + assertNotEquals(0, one.hashCode()); + assertNotEquals(0, two.hashCode()); + assertNotEquals(0, three.hashCode()); + + // All the hashcodes are distinct. + assertNotEquals(one.hashCode(), two.hashCode()); + assertNotEquals(one.hashCode(), three.hashCode()); + assertNotEquals(two.hashCode(), three.hashCode()); + + // None of the handles are zero. + assertNotEquals(0, one.getNetworkHandle()); + assertNotEquals(0, two.getNetworkHandle()); + assertNotEquals(0, three.getNetworkHandle()); + + // All the handles are distinct. + assertNotEquals(one.getNetworkHandle(), two.getNetworkHandle()); + assertNotEquals(one.getNetworkHandle(), three.getNetworkHandle()); + assertNotEquals(two.getNetworkHandle(), three.getNetworkHandle()); + + // The handles are not equal to the hashcodes. + assertNotEquals(one.hashCode(), one.getNetworkHandle()); + assertNotEquals(two.hashCode(), two.getNetworkHandle()); + assertNotEquals(three.hashCode(), three.getNetworkHandle()); + + // Adjust as necessary to test an implementation's specific constants. + // When running with runtest, "adb logcat -s TestRunner" can be useful. + assertEquals(7700664333L, one.getNetworkHandle()); + assertEquals(11995631629L, two.getNetworkHandle()); + assertEquals(16290598925L, three.getNetworkHandle()); + } + + // getNetId() did not exist in Q + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testGetNetId() { + assertEquals(1234, new Network(1234).getNetId()); + assertEquals(2345, new Network(2345, true).getNetId()); + } + + @Test + public void testFromNetworkHandle() { + final Network network = new Network(1234); + assertEquals(network.netId, Network.fromNetworkHandle(network.getNetworkHandle()).netId); + } + + // Parsing private DNS bypassing handle was not supported until S + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testFromNetworkHandlePrivateDnsBypass_S() { + final Network network = new Network(1234, true); + + final Network recreatedNetwork = Network.fromNetworkHandle(network.getNetworkHandle()); + assertEquals(network.netId, recreatedNetwork.netId); + assertEquals(network.getNetIdForResolv(), recreatedNetwork.getNetIdForResolv()); + } + + @Test @IgnoreAfter(Build.VERSION_CODES.R) + public void testFromNetworkHandlePrivateDnsBypass_R() { + final Network network = new Network(1234, true); + + final Network recreatedNetwork = Network.fromNetworkHandle(network.getNetworkHandle()); + assertEquals(network.netId, recreatedNetwork.netId); + // Until R included, fromNetworkHandle would not parse the private DNS bypass flag + assertEquals(network.netId, recreatedNetwork.getNetIdForResolv()); + } + + @Test + public void testGetPrivateDnsBypassingCopy() { + final Network copy = mNetwork.getPrivateDnsBypassingCopy(); + assertEquals(mNetwork.netId, copy.netId); + assertNotEquals(copy.netId, copy.getNetIdForResolv()); + assertNotEquals(mNetwork.getNetIdForResolv(), copy.getNetIdForResolv()); + } +} diff --git a/tests/common/java/android/net/OemNetworkPreferencesTest.java b/tests/common/java/android/net/OemNetworkPreferencesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fd29a9539de8e9cdc205b88633b4b313b806957d --- /dev/null +++ b/tests/common/java/android/net/OemNetworkPreferencesTest.java @@ -0,0 +1,152 @@ +/* + * 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 static com.android.testutils.MiscAsserts.assertThrows; +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.Build; + +import androidx.test.filters.SmallTest; + +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +@IgnoreUpTo(Build.VERSION_CODES.R) +@RunWith(DevSdkIgnoreRunner.class) +@SmallTest +public class OemNetworkPreferencesTest { + + private static final int TEST_PREF = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED; + private static final String TEST_PACKAGE = "com.google.apps.contacts"; + + private final OemNetworkPreferences.Builder mBuilder = new OemNetworkPreferences.Builder(); + + @Test + public void testBuilderAddNetworkPreferenceRequiresNonNullPackageName() { + assertThrows(NullPointerException.class, + () -> mBuilder.addNetworkPreference(null, TEST_PREF)); + } + + @Test + public void testBuilderRemoveNetworkPreferenceRequiresNonNullPackageName() { + assertThrows(NullPointerException.class, + () -> mBuilder.clearNetworkPreference(null)); + } + + @Test + public void testGetNetworkPreferenceReturnsCorrectValue() { + final int expectedNumberOfMappings = 1; + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final Map<String, Integer> networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertEquals(expectedNumberOfMappings, networkPreferences.size()); + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + } + + @Test + public void testGetNetworkPreferenceReturnsUnmodifiableValue() { + final String newPackage = "new.com.google.apps.contacts"; + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final Map<String, Integer> networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertThrows(UnsupportedOperationException.class, + () -> networkPreferences.put(newPackage, TEST_PREF)); + + assertThrows(UnsupportedOperationException.class, + () -> networkPreferences.remove(TEST_PACKAGE)); + + } + + @Test + public void testToStringReturnsCorrectValue() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final String networkPreferencesString = mBuilder.build().getNetworkPreferences().toString(); + + assertTrue(networkPreferencesString.contains(Integer.toString(TEST_PREF))); + assertTrue(networkPreferencesString.contains(TEST_PACKAGE)); + } + + @Test + public void testOemNetworkPreferencesParcelable() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + + final OemNetworkPreferences prefs = mBuilder.build(); + + assertParcelSane(prefs, 1 /* fieldCount */); + } + + @Test + public void testAddNetworkPreferenceOverwritesPriorPreference() { + final int newPref = OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + Map<String, Integer> networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), TEST_PREF); + + mBuilder.addNetworkPreference(TEST_PACKAGE, newPref); + networkPreferences = mBuilder.build().getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), newPref); + } + + @Test + public void testRemoveNetworkPreferenceRemovesValue() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + Map<String, Integer> networkPreferences = + mBuilder.build().getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + + mBuilder.clearNetworkPreference(TEST_PACKAGE); + networkPreferences = mBuilder.build().getNetworkPreferences(); + + assertFalse(networkPreferences.containsKey(TEST_PACKAGE)); + } + + @Test + public void testConstructorByOemNetworkPreferencesSetsValue() { + mBuilder.addNetworkPreference(TEST_PACKAGE, TEST_PREF); + OemNetworkPreferences networkPreference = mBuilder.build(); + + final Map<String, Integer> networkPreferences = + new OemNetworkPreferences + .Builder(networkPreference) + .build() + .getNetworkPreferences(); + + assertTrue(networkPreferences.containsKey(TEST_PACKAGE)); + assertEquals(networkPreferences.get(TEST_PACKAGE).intValue(), TEST_PREF); + } +} diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..71689f91972641f4c32e2d178d30113bfbb393f8 --- /dev/null +++ b/tests/common/java/android/net/RouteInfoTest.java @@ -0,0 +1,434 @@ +/* + * 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; + +import static android.net.RouteInfo.RTN_UNREACHABLE; + +import static com.android.testutils.MiscAsserts.assertEqualBothWays; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; +import static com.android.testutils.MiscAsserts.assertNotEqualEitherWay; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +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.Assert.fail; + +import android.os.Build; + +import androidx.core.os.BuildCompat; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class RouteInfoTest { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + private static final int INVALID_ROUTE_TYPE = -1; + + private InetAddress Address(String addr) { + return InetAddresses.parseNumericAddress(addr); + } + + private IpPrefix Prefix(String prefix) { + return new IpPrefix(prefix); + } + + private static boolean isAtLeastR() { + // BuildCompat.isAtLeastR is documented to return false on release SDKs (including R) + return Build.VERSION.SDK_INT > Build.VERSION_CODES.Q || BuildCompat.isAtLeastR(); + } + + @Test + public void testConstructor() { + RouteInfo r; + // Invalid input. + try { + r = new RouteInfo((IpPrefix) null, null, "rmnet0"); + fail("Expected RuntimeException: destination and gateway null"); + } catch (RuntimeException e) { } + + try { + r = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "rmnet0", + INVALID_ROUTE_TYPE); + fail("Invalid route type should cause exception"); + } catch (IllegalArgumentException e) { } + + try { + r = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("192.0.2.1"), "rmnet0", + RTN_UNREACHABLE); + fail("Address family mismatch should cause exception"); + } catch (IllegalArgumentException e) { } + + try { + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("2001:db8::1"), "rmnet0", + RTN_UNREACHABLE); + fail("Address family mismatch should cause exception"); + } catch (IllegalArgumentException e) { } + + // Null destination is default route. + r = new RouteInfo((IpPrefix) null, Address("2001:db8::1"), null); + assertEquals(Prefix("::/0"), r.getDestination()); + assertEquals(Address("2001:db8::1"), r.getGateway()); + assertNull(r.getInterface()); + + r = new RouteInfo((IpPrefix) null, Address("192.0.2.1"), "wlan0"); + assertEquals(Prefix("0.0.0.0/0"), r.getDestination()); + assertEquals(Address("192.0.2.1"), r.getGateway()); + assertEquals("wlan0", r.getInterface()); + + // Null gateway sets gateway to unspecified address (why?). + r = new RouteInfo(Prefix("2001:db8:beef:cafe::/48"), null, "lo"); + assertEquals(Prefix("2001:db8:beef::/48"), r.getDestination()); + assertEquals(Address("::"), r.getGateway()); + assertEquals("lo", r.getInterface()); + + r = new RouteInfo(Prefix("192.0.2.5/24"), null); + assertEquals(Prefix("192.0.2.0/24"), r.getDestination()); + assertEquals(Address("0.0.0.0"), r.getGateway()); + assertNull(r.getInterface()); + } + + @Test + public void testMatches() { + class PatchedRouteInfo { + private final RouteInfo mRouteInfo; + + public PatchedRouteInfo(IpPrefix destination, InetAddress gateway, String iface) { + mRouteInfo = new RouteInfo(destination, gateway, iface); + } + + public boolean matches(InetAddress destination) { + return mRouteInfo.matches(destination); + } + } + + PatchedRouteInfo r; + + r = new PatchedRouteInfo(Prefix("2001:db8:f00::ace:d00d/127"), null, "rmnet0"); + assertTrue(r.matches(Address("2001:db8:f00::ace:d00c"))); + assertTrue(r.matches(Address("2001:db8:f00::ace:d00d"))); + assertFalse(r.matches(Address("2001:db8:f00::ace:d00e"))); + assertFalse(r.matches(Address("2001:db8:f00::bad:d00d"))); + assertFalse(r.matches(Address("2001:4868:4860::8888"))); + assertFalse(r.matches(Address("8.8.8.8"))); + + r = new PatchedRouteInfo(Prefix("192.0.2.0/23"), null, "wlan0"); + assertTrue(r.matches(Address("192.0.2.43"))); + assertTrue(r.matches(Address("192.0.3.21"))); + assertFalse(r.matches(Address("192.0.0.21"))); + assertFalse(r.matches(Address("8.8.8.8"))); + + PatchedRouteInfo ipv6Default = new PatchedRouteInfo(Prefix("::/0"), null, "rmnet0"); + assertTrue(ipv6Default.matches(Address("2001:db8::f00"))); + assertFalse(ipv6Default.matches(Address("192.0.2.1"))); + + PatchedRouteInfo ipv4Default = new PatchedRouteInfo(Prefix("0.0.0.0/0"), null, "rmnet0"); + assertTrue(ipv4Default.matches(Address("255.255.255.255"))); + assertTrue(ipv4Default.matches(Address("192.0.2.1"))); + assertFalse(ipv4Default.matches(Address("2001:db8::f00"))); + } + + @Test + public void testEquals() { + // IPv4 + RouteInfo r1 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0"); + RouteInfo r2 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "wlan0"); + assertEqualBothWays(r1, r2); + + RouteInfo r3 = new RouteInfo(Prefix("2001:db8:ace::/49"), Address("2001:db8::1"), "wlan0"); + RouteInfo r4 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::2"), "wlan0"); + RouteInfo r5 = new RouteInfo(Prefix("2001:db8:ace::/48"), Address("2001:db8::1"), "rmnet0"); + assertNotEqualEitherWay(r1, r3); + assertNotEqualEitherWay(r1, r4); + assertNotEqualEitherWay(r1, r5); + + // IPv6 + r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0"); + r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "wlan0"); + assertEqualBothWays(r1, r2); + + r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0"); + r4 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.2"), "wlan0"); + r5 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), "rmnet0"); + assertNotEqualEitherWay(r1, r3); + assertNotEqualEitherWay(r1, r4); + assertNotEqualEitherWay(r1, r5); + + // Interfaces (but not destinations or gateways) can be null. + r1 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null); + r2 = new RouteInfo(Prefix("192.0.2.0/25"), Address("192.0.2.1"), null); + r3 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0"); + assertEqualBothWays(r1, r2); + assertNotEqualEitherWay(r1, r3); + } + + @Test + public void testHostAndDefaultRoutes() { + RouteInfo r; + + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0"); + assertFalse(r.isHostRoute()); + assertTrue(r.isDefaultRoute()); + assertTrue(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("::/0"), Address("::"), "wlan0"); + assertFalse(r.isHostRoute()); + assertTrue(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertTrue(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0"); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("2001:db8::/48"), null, "wlan0"); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("192.0.2.0/32"), Address("0.0.0.0"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("2001:db8::/128"), Address("::"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("192.0.2.0/32"), null, "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("2001:db8::/128"), null, "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("::/128"), Address("fe80::"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(Prefix("0.0.0.0/32"), Address("192.0.2.1"), "wlan0"); + assertTrue(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertTrue(r.isIPv4UnreachableDefault()); + assertFalse(r.isIPv6UnreachableDefault()); + } + + r = new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE); + assertFalse(r.isHostRoute()); + assertFalse(r.isDefaultRoute()); + assertFalse(r.isIPv4Default()); + assertFalse(r.isIPv6Default()); + if (isAtLeastR()) { + assertFalse(r.isIPv4UnreachableDefault()); + assertTrue(r.isIPv6UnreachableDefault()); + } + } + + @Test + public void testTruncation() { + LinkAddress l; + RouteInfo r; + + l = new LinkAddress("192.0.2.5/30"); + r = new RouteInfo(l, Address("192.0.2.1"), "wlan0"); + assertEquals("192.0.2.4", r.getDestination().getAddress().getHostAddress()); + + l = new LinkAddress("2001:db8:1:f::5/63"); + r = new RouteInfo(l, Address("2001:db8:5::1"), "wlan0"); + assertEquals("2001:db8:1:e::", r.getDestination().getAddress().getHostAddress()); + } + + // Make sure that creating routes to multicast addresses doesn't throw an exception. Even though + // there's nothing we can do with them, we don't want to crash if, e.g., someone calls + // requestRouteToHostAddress("230.0.0.0", MOBILE_HIPRI); + @Test + public void testMulticastRoute() { + RouteInfo r; + r = new RouteInfo(Prefix("230.0.0.0/32"), Address("192.0.2.1"), "wlan0"); + r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), "wlan0"); + // No exceptions? Good. + } + + @Test + public void testParceling() { + RouteInfo r; + r = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), null); + assertParcelingIsLossless(r); + r = new RouteInfo(Prefix("192.0.2.0/24"), null, "wlan0"); + assertParcelingIsLossless(r); + r = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", RTN_UNREACHABLE); + assertParcelingIsLossless(r); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testMtuParceling() { + final RouteInfo r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::"), "testiface", + RTN_UNREACHABLE, 1450 /* mtu */); + assertParcelingIsLossless(r); + } + + @Test @IgnoreAfter(Build.VERSION_CODES.Q) + public void testFieldCount_Q() { + assertFieldCountEquals(6, RouteInfo.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testFieldCount() { + // Make sure any new field is covered by the above parceling tests when changing this number + assertFieldCountEquals(7, RouteInfo.class); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testMtu() { + RouteInfo r; + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0", + RouteInfo.RTN_UNICAST, 1500); + assertEquals(1500, r.getMtu()); + + r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0"); + assertEquals(0, r.getMtu()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testRouteKey() { + RouteInfo.RouteKey k1, k2; + // Only prefix, null gateway and null interface + k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality + k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", + RTN_UNREACHABLE, 1450).getRouteKey(); + k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0", + RouteInfo.RTN_UNICAST, 1400).getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // Different scope IDs are ignored by the kernel, so we consider them equal here too. + k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey(); + k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey(); + assertEquals(k1, k2); + assertEquals(k1.hashCode(), k2.hashCode()); + + // Different prefix + k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey(); + assertNotEquals(k1, k2); + + // Different gateway + k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey(); + k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey(); + assertNotEquals(k1, k2); + + // Different interface + k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey(); + k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey(); + assertNotEquals(k1, k2); + } +} diff --git a/tests/common/java/android/net/StaticIpConfigurationTest.java b/tests/common/java/android/net/StaticIpConfigurationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b5f23bf19a3c0fafbb7e848dc3df9a9994b86f71 --- /dev/null +++ b/tests/common/java/android/net/StaticIpConfigurationTest.java @@ -0,0 +1,269 @@ +/* + * 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 android.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.os.Parcel; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class StaticIpConfigurationTest { + + private static final String ADDRSTR = "192.0.2.2/25"; + private static final LinkAddress ADDR = new LinkAddress(ADDRSTR); + private static final InetAddress GATEWAY = IpAddress("192.0.2.1"); + private static final InetAddress OFFLINKGATEWAY = IpAddress("192.0.2.129"); + private static final InetAddress DNS1 = IpAddress("8.8.8.8"); + private static final InetAddress DNS2 = IpAddress("8.8.4.4"); + private static final InetAddress DNS3 = IpAddress("4.2.2.2"); + private static final String IFACE = "eth0"; + private static final String FAKE_DOMAINS = "google.com"; + + private static InetAddress IpAddress(String addr) { + return InetAddress.parseNumericAddress(addr); + } + + private void checkEmpty(StaticIpConfiguration s) { + assertNull(s.ipAddress); + assertNull(s.gateway); + assertNull(s.domains); + assertEquals(0, s.dnsServers.size()); + } + + private StaticIpConfiguration makeTestObject() { + StaticIpConfiguration s = new StaticIpConfiguration(); + s.ipAddress = ADDR; + s.gateway = GATEWAY; + s.dnsServers.add(DNS1); + s.dnsServers.add(DNS2); + s.dnsServers.add(DNS3); + s.domains = FAKE_DOMAINS; + return s; + } + + @Test + public void testConstructor() { + StaticIpConfiguration s = new StaticIpConfiguration(); + checkEmpty(s); + } + + @Test + public void testCopyAndClear() { + StaticIpConfiguration empty = new StaticIpConfiguration((StaticIpConfiguration) null); + checkEmpty(empty); + + StaticIpConfiguration s1 = makeTestObject(); + StaticIpConfiguration s2 = new StaticIpConfiguration(s1); + assertEquals(s1, s2); + s2.clear(); + assertEquals(empty, s2); + } + + @Test + public void testHashCodeAndEquals() { + HashSet<Integer> hashCodes = new HashSet(); + hashCodes.add(0); + + StaticIpConfiguration s = new StaticIpConfiguration(); + // Check that this hash code is nonzero and different from all the ones seen so far. + assertTrue(hashCodes.add(s.hashCode())); + + s.ipAddress = ADDR; + assertTrue(hashCodes.add(s.hashCode())); + + s.gateway = GATEWAY; + assertTrue(hashCodes.add(s.hashCode())); + + s.dnsServers.add(DNS1); + assertTrue(hashCodes.add(s.hashCode())); + + s.dnsServers.add(DNS2); + assertTrue(hashCodes.add(s.hashCode())); + + s.dnsServers.add(DNS3); + assertTrue(hashCodes.add(s.hashCode())); + + s.domains = "example.com"; + assertTrue(hashCodes.add(s.hashCode())); + + assertFalse(s.equals(null)); + assertEquals(s, s); + + StaticIpConfiguration s2 = new StaticIpConfiguration(s); + assertEquals(s, s2); + + s.ipAddress = new LinkAddress(DNS1, 32); + assertNotEquals(s, s2); + + s2 = new StaticIpConfiguration(s); + s.domains = "foo"; + assertNotEquals(s, s2); + + s2 = new StaticIpConfiguration(s); + s.gateway = DNS2; + assertNotEquals(s, s2); + + s2 = new StaticIpConfiguration(s); + s.dnsServers.add(DNS3); + assertNotEquals(s, s2); + } + + @Test + public void testToLinkProperties() { + LinkProperties expected = new LinkProperties(); + expected.setInterfaceName(IFACE); + + StaticIpConfiguration s = new StaticIpConfiguration(); + assertEquals(expected, s.toLinkProperties(IFACE)); + + final RouteInfo connectedRoute = new RouteInfo(new IpPrefix(ADDRSTR), null, IFACE); + s.ipAddress = ADDR; + expected.addLinkAddress(ADDR); + expected.addRoute(connectedRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.gateway = GATEWAY; + RouteInfo defaultRoute = new RouteInfo(new IpPrefix("0.0.0.0/0"), GATEWAY, IFACE); + expected.addRoute(defaultRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.gateway = OFFLINKGATEWAY; + expected.removeRoute(defaultRoute); + defaultRoute = new RouteInfo(new IpPrefix("0.0.0.0/0"), OFFLINKGATEWAY, IFACE); + expected.addRoute(defaultRoute); + + RouteInfo gatewayRoute = new RouteInfo(new IpPrefix("192.0.2.129/32"), null, IFACE); + expected.addRoute(gatewayRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.dnsServers.add(DNS1); + expected.addDnsServer(DNS1); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.dnsServers.add(DNS2); + s.dnsServers.add(DNS3); + expected.addDnsServer(DNS2); + expected.addDnsServer(DNS3); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.domains = FAKE_DOMAINS; + expected.setDomains(FAKE_DOMAINS); + assertEquals(expected, s.toLinkProperties(IFACE)); + + s.gateway = null; + expected.removeRoute(defaultRoute); + expected.removeRoute(gatewayRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + + // Without knowing the IP address, we don't have a directly-connected route, so we can't + // tell if the gateway is off-link or not and we don't add a host route. This isn't a real + // configuration, but we should at least not crash. + s.gateway = OFFLINKGATEWAY; + s.ipAddress = null; + expected.removeLinkAddress(ADDR); + expected.removeRoute(connectedRoute); + expected.addRoute(defaultRoute); + assertEquals(expected, s.toLinkProperties(IFACE)); + } + + private StaticIpConfiguration passThroughParcel(StaticIpConfiguration s) { + Parcel p = Parcel.obtain(); + StaticIpConfiguration s2 = null; + try { + s.writeToParcel(p, 0); + p.setDataPosition(0); + s2 = StaticIpConfiguration.readFromParcel(p); + } finally { + p.recycle(); + } + assertNotNull(s2); + return s2; + } + + @Test + public void testParceling() { + StaticIpConfiguration s = makeTestObject(); + StaticIpConfiguration s2 = passThroughParcel(s); + assertEquals(s, s2); + } + + @Test + public void testBuilder() { + final ArrayList<InetAddress> dnsServers = new ArrayList<>(); + dnsServers.add(DNS1); + + final StaticIpConfiguration s = new StaticIpConfiguration.Builder() + .setIpAddress(ADDR) + .setGateway(GATEWAY) + .setDomains(FAKE_DOMAINS) + .setDnsServers(dnsServers) + .build(); + + assertEquals(s.ipAddress, s.getIpAddress()); + assertEquals(ADDR, s.getIpAddress()); + assertEquals(s.gateway, s.getGateway()); + assertEquals(GATEWAY, s.getGateway()); + assertEquals(s.domains, s.getDomains()); + assertEquals(FAKE_DOMAINS, s.getDomains()); + assertTrue(s.dnsServers.equals(s.getDnsServers())); + assertEquals(1, s.getDnsServers().size()); + assertEquals(DNS1, s.getDnsServers().get(0)); + } + + @Test + public void testAddDnsServers() { + final StaticIpConfiguration s = new StaticIpConfiguration((StaticIpConfiguration) null); + checkEmpty(s); + + s.addDnsServer(DNS1); + assertEquals(1, s.getDnsServers().size()); + assertEquals(DNS1, s.getDnsServers().get(0)); + + s.addDnsServer(DNS2); + s.addDnsServer(DNS3); + assertEquals(3, s.getDnsServers().size()); + assertEquals(DNS2, s.getDnsServers().get(1)); + assertEquals(DNS3, s.getDnsServers().get(2)); + } + + @Test + public void testGetRoutes() { + final StaticIpConfiguration s = makeTestObject(); + final List<RouteInfo> routeInfoList = s.getRoutes(IFACE); + + assertEquals(2, routeInfoList.size()); + assertEquals(new RouteInfo(ADDR, (InetAddress) null, IFACE), routeInfoList.get(0)); + assertEquals(new RouteInfo((IpPrefix) null, GATEWAY, IFACE), routeInfoList.get(1)); + } +} diff --git a/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7a18bb08faa8eda347f471e79617305cd9a3a88c --- /dev/null +++ b/tests/common/java/android/net/TcpKeepalivePacketDataTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.os.Build +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.assertFieldCountEquals +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import java.net.InetAddress +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) // TcpKeepalivePacketData added to SDK in S +class TcpKeepalivePacketDataTest { + private fun makeData( + srcAddress: InetAddress = parseNumericAddress("192.0.2.123"), + srcPort: Int = 1234, + dstAddress: InetAddress = parseNumericAddress("192.0.2.231"), + dstPort: Int = 4321, + data: ByteArray = byteArrayOf(1, 2, 3), + tcpSeq: Int = 135, + tcpAck: Int = 246, + tcpWnd: Int = 1234, + tcpWndScale: Int = 2, + ipTos: Int = 0x12, + ipTtl: Int = 10 + ) = TcpKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, data, tcpSeq, tcpAck, + tcpWnd, tcpWndScale, ipTos, ipTtl) + + @Test + fun testEquals() { + val data1 = makeData() + val data2 = makeData() + assertEquals(data1, data2) + assertEquals(data1.hashCode(), data2.hashCode()) + } + + @Test + fun testNotEquals() { + assertNotEquals(makeData(srcAddress = parseNumericAddress("192.0.2.124")), makeData()) + assertNotEquals(makeData(srcPort = 1235), makeData()) + assertNotEquals(makeData(dstAddress = parseNumericAddress("192.0.2.232")), makeData()) + assertNotEquals(makeData(dstPort = 4322), makeData()) + // .equals does not test .packet, as it should be generated from the other fields + assertNotEquals(makeData(tcpSeq = 136), makeData()) + assertNotEquals(makeData(tcpAck = 247), makeData()) + assertNotEquals(makeData(tcpWnd = 1235), makeData()) + assertNotEquals(makeData(tcpWndScale = 3), makeData()) + assertNotEquals(makeData(ipTos = 0x14), makeData()) + assertNotEquals(makeData(ipTtl = 11), makeData()) + + // Update above assertions if field is added + assertFieldCountEquals(5, KeepalivePacketData::class.java) + assertFieldCountEquals(6, TcpKeepalivePacketData::class.java) + } + + @Test + fun testParcelUnparcel() { + assertParcelSane(makeData(), fieldCount = 6) { a, b -> + // .equals() does not verify .packet + a == b && a.packet contentEquals b.packet + } + } + + @Test + fun testToString() { + val data = makeData() + val str = data.toString() + + assertTrue(str.contains(data.srcAddress.hostAddress)) + assertTrue(str.contains(data.srcPort.toString())) + assertTrue(str.contains(data.dstAddress.hostAddress)) + assertTrue(str.contains(data.dstPort.toString())) + // .packet not included in toString() + assertTrue(str.contains(data.getTcpSeq().toString())) + assertTrue(str.contains(data.getTcpAck().toString())) + assertTrue(str.contains(data.getTcpWindow().toString())) + assertTrue(str.contains(data.getTcpWindowScale().toString())) + assertTrue(str.contains(data.getIpTos().toString())) + assertTrue(str.contains(data.getIpTtl().toString())) + + // Update above assertions if field is added + assertFieldCountEquals(5, KeepalivePacketData::class.java) + assertFieldCountEquals(6, TcpKeepalivePacketData::class.java) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/UidRangeTest.java b/tests/common/java/android/net/UidRangeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1b1c95431d6f79ca8844e0285b65a518603684ef --- /dev/null +++ b/tests/common/java/android/net/UidRangeTest.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 android.net; + +import static android.os.UserHandle.MIN_SECONDARY_USER_ID; +import static android.os.UserHandle.SYSTEM; +import static android.os.UserHandle.USER_SYSTEM; +import static android.os.UserHandle.getUid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.os.Build; +import android.os.UserHandle; + +import androidx.test.filters.SmallTest; +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; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UidRangeTest { + + /* + * UidRange is no longer passed to netd. UID ranges between the framework and netd are passed as + * UidRangeParcel objects. + */ + + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + @Test + public void testSingleItemUidRangeAllowed() { + new UidRange(123, 123); + new UidRange(0, 0); + new UidRange(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + @Test + public void testNegativeUidsDisallowed() { + try { + new UidRange(-2, 100); + fail("Exception not thrown for negative start UID"); + } catch (IllegalArgumentException expected) { + } + + try { + new UidRange(-200, -100); + fail("Exception not thrown for negative stop UID"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testStopLessThanStartDisallowed() { + final int x = 4195000; + try { + new UidRange(x, x - 1); + fail("Exception not thrown for negative-length UID range"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testGetStartAndEndUser() throws Exception { + final UidRange uidRangeOfPrimaryUser = new UidRange( + getUid(USER_SYSTEM, 10000), getUid(USER_SYSTEM, 10100)); + final UidRange uidRangeOfSecondaryUser = new UidRange( + getUid(MIN_SECONDARY_USER_ID, 10000), getUid(MIN_SECONDARY_USER_ID, 10100)); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser()); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getEndUser()); + assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getStartUser()); + assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getEndUser()); + + final UidRange uidRangeForDifferentUsers = new UidRange( + getUid(USER_SYSTEM, 10000), getUid(MIN_SECONDARY_USER_ID, 10100)); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser()); + assertEquals(MIN_SECONDARY_USER_ID, uidRangeOfSecondaryUser.getEndUser()); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testCreateForUser() throws Exception { + final UidRange uidRangeOfPrimaryUser = UidRange.createForUser(SYSTEM); + final UidRange uidRangeOfSecondaryUser = UidRange.createForUser( + UserHandle.of(USER_SYSTEM + 1)); + assertTrue(uidRangeOfPrimaryUser.stop < uidRangeOfSecondaryUser.start); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getStartUser()); + assertEquals(USER_SYSTEM, uidRangeOfPrimaryUser.getEndUser()); + assertEquals(USER_SYSTEM + 1, uidRangeOfSecondaryUser.getStartUser()); + assertEquals(USER_SYSTEM + 1, uidRangeOfSecondaryUser.getEndUser()); + } +} diff --git a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f23ba26d00396beecf1986ef9dbecfe282084264 --- /dev/null +++ b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt @@ -0,0 +1,50 @@ +/* + * 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.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +private const val TEST_OWNER_UID = 123 +private const val TEST_IFACE = "test_tun0" +private val TEST_IFACE_LIST = listOf("wlan0", "rmnet_data0", "eth0") + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class UnderlyingNetworkInfoTest { + @Test + fun testParcelUnparcel() { + val testInfo = UnderlyingNetworkInfo(TEST_OWNER_UID, TEST_IFACE, TEST_IFACE_LIST) + assertEquals(TEST_OWNER_UID, testInfo.getOwnerUid()) + assertEquals(TEST_IFACE, testInfo.getInterface()) + assertEquals(TEST_IFACE_LIST, testInfo.getUnderlyingInterfaces()) + assertParcelSane(testInfo, 3) + + val emptyInfo = UnderlyingNetworkInfo(0, String(), listOf()) + assertEquals(0, emptyInfo.getOwnerUid()) + assertEquals(String(), emptyInfo.getInterface()) + assertEquals(listOf(), emptyInfo.getUnderlyingInterfaces()) + assertParcelSane(emptyInfo, 3) + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..88996d9252627cdb456dd4742c7670bd2ef399f7 --- /dev/null +++ b/tests/common/java/android/net/apf/ApfCapabilitiesTest.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.apf; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertArrayEquals; +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 android.content.Context; +import android.os.Build; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ApfCapabilitiesTest { + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getContext(); + } + + @Test + public void testConstructAndParcel() { + final ApfCapabilities caps = new ApfCapabilities(123, 456, 789); + assertEquals(123, caps.apfVersionSupported); + assertEquals(456, caps.maximumApfProgramSize); + assertEquals(789, caps.apfPacketFormat); + + assertParcelSane(caps, 3); + } + + @Test + public void testEquals() { + assertEquals(new ApfCapabilities(1, 2, 3), new ApfCapabilities(1, 2, 3)); + assertNotEquals(new ApfCapabilities(2, 2, 3), new ApfCapabilities(1, 2, 3)); + assertNotEquals(new ApfCapabilities(1, 3, 3), new ApfCapabilities(1, 2, 3)); + assertNotEquals(new ApfCapabilities(1, 2, 4), new ApfCapabilities(1, 2, 3)); + } + + @Test + public void testHasDataAccess() { + //hasDataAccess is only supported starting at apf version 4. + ApfCapabilities caps = new ApfCapabilities(1 /* apfVersionSupported */, 2, 3); + assertFalse(caps.hasDataAccess()); + + caps = new ApfCapabilities(4 /* apfVersionSupported */, 5, 6); + assertTrue(caps.hasDataAccess()); + } + + @Test + public void testGetApfDrop8023Frames() { + // Get com.android.internal.R.bool.config_apfDrop802_3Frames. The test cannot directly + // use R.bool.config_apfDrop802_3Frames because that is not a stable resource ID. + final int resId = mContext.getResources().getIdentifier("config_apfDrop802_3Frames", + "bool", "android"); + final boolean shouldDrop8023Frames = mContext.getResources().getBoolean(resId); + final boolean actual = ApfCapabilities.getApfDrop8023Frames(); + assertEquals(shouldDrop8023Frames, actual); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testGetApfDrop8023Frames_S() { + // IpClient does not call getApfDrop8023Frames() since S, so any customization of the return + // value on S+ is a configuration error as it will not be used by IpClient. + assertTrue("android.R.bool.config_apfDrop802_3Frames has been modified to false, but " + + "starting from S its value is not used by IpClient. If the modification is " + + "intentional, use a runtime resource overlay for the NetworkStack package to " + + "overlay com.android.networkstack.R.bool.config_apfDrop802_3Frames instead.", + ApfCapabilities.getApfDrop8023Frames()); + } + + @Test + public void testGetApfEtherTypeBlackList() { + // Get com.android.internal.R.array.config_apfEthTypeBlackList. The test cannot directly + // use R.array.config_apfEthTypeBlackList because that is not a stable resource ID. + final int resId = mContext.getResources().getIdentifier("config_apfEthTypeBlackList", + "array", "android"); + final int[] blacklistedEtherTypeArray = mContext.getResources().getIntArray(resId); + final int[] actual = ApfCapabilities.getApfEtherTypeBlackList(); + assertNotNull(actual); + assertTrue(Arrays.equals(blacklistedEtherTypeArray, actual)); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testGetApfEtherTypeBlackList_S() { + // IpClient does not call getApfEtherTypeBlackList() since S, so any customization of the + // return value on S+ is a configuration error as it will not be used by IpClient. + assertArrayEquals("android.R.array.config_apfEthTypeBlackList has been modified, but " + + "starting from S its value is not used by IpClient. If the modification " + + "is intentional, use a runtime resource overlay for the NetworkStack " + + "package to overlay " + + "com.android.networkstack.R.array.config_apfEthTypeDenyList instead.", + new int[] { 0x88a2, 0x88a4, 0x88b8, 0x88cd, 0x88e3 }, + ApfCapabilities.getApfEtherTypeBlackList()); + } +} diff --git a/tests/common/java/android/net/metrics/ApfProgramEventTest.kt b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b7b74097cc61b0f57f424cea07afc47cb1ba10e --- /dev/null +++ b/tests/common/java/android/net/metrics/ApfProgramEventTest.kt @@ -0,0 +1,72 @@ +/* + * 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.metrics; + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ApfProgramEventTest { + private infix fun Int.hasFlag(flag: Int) = (this and (1 shl flag)) != 0 + + @Test + fun testBuilderAndParcel() { + val apfProgramEvent = ApfProgramEvent.Builder() + .setLifetime(1) + .setActualLifetime(2) + .setFilteredRas(3) + .setCurrentRas(4) + .setProgramLength(5) + .setFlags(true, true) + .build() + + assertEquals(1, apfProgramEvent.lifetime) + assertEquals(2, apfProgramEvent.actualLifetime) + assertEquals(3, apfProgramEvent.filteredRas) + assertEquals(4, apfProgramEvent.currentRas) + assertEquals(5, apfProgramEvent.programLength) + assertEquals(ApfProgramEvent.flagsFor(true, true), apfProgramEvent.flags) + + assertParcelSane(apfProgramEvent, 6) + } + + @Test + fun testFlagsFor() { + var flags = ApfProgramEvent.flagsFor(false, false) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + + flags = ApfProgramEvent.flagsFor(true, false) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + + flags = ApfProgramEvent.flagsFor(false, true) + assertFalse(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + + flags = ApfProgramEvent.flagsFor(true, true) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_HAS_IPV4_ADDRESS) + assertTrue(flags hasFlag ApfProgramEvent.FLAG_MULTICAST_FILTER_ON) + } +} diff --git a/tests/common/java/android/net/metrics/ApfStatsTest.kt b/tests/common/java/android/net/metrics/ApfStatsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..46a8c8e5b5090b4c1e4d7918ced2981470890811 --- /dev/null +++ b/tests/common/java/android/net/metrics/ApfStatsTest.kt @@ -0,0 +1,57 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ApfStatsTest { + @Test + fun testBuilderAndParcel() { + val apfStats = ApfStats.Builder() + .setDurationMs(Long.MAX_VALUE) + .setReceivedRas(1) + .setMatchingRas(2) + .setDroppedRas(3) + .setZeroLifetimeRas(4) + .setParseErrors(5) + .setProgramUpdates(6) + .setProgramUpdatesAll(7) + .setProgramUpdatesAllowingMulticast(8) + .setMaxProgramSize(9) + .build() + + assertEquals(Long.MAX_VALUE, apfStats.durationMs) + assertEquals(1, apfStats.receivedRas) + assertEquals(2, apfStats.matchingRas) + assertEquals(3, apfStats.droppedRas) + assertEquals(4, apfStats.zeroLifetimeRas) + assertEquals(5, apfStats.parseErrors) + assertEquals(6, apfStats.programUpdates) + assertEquals(7, apfStats.programUpdatesAll) + assertEquals(8, apfStats.programUpdatesAllowingMulticast) + assertEquals(9, apfStats.maxProgramSize) + + assertParcelSane(apfStats, 10) + } +} diff --git a/tests/common/java/android/net/metrics/DhcpClientEventTest.kt b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d7a9c405024bce3c9b0b08fdc9b4188c99ae4dc --- /dev/null +++ b/tests/common/java/android/net/metrics/DhcpClientEventTest.kt @@ -0,0 +1,43 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +private const val FAKE_MESSAGE = "test" + +@RunWith(AndroidJUnit4::class) +@SmallTest +class DhcpClientEventTest { + @Test + fun testBuilderAndParcel() { + val dhcpClientEvent = DhcpClientEvent.Builder() + .setMsg(FAKE_MESSAGE) + .setDurationMs(Integer.MAX_VALUE) + .build() + + assertEquals(FAKE_MESSAGE, dhcpClientEvent.msg) + assertEquals(Integer.MAX_VALUE, dhcpClientEvent.durationMs) + + assertParcelSane(dhcpClientEvent, 2) + } +} diff --git a/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt b/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..236f72eafbdcfa99ec4d6e0fc691f94f6da45970 --- /dev/null +++ b/tests/common/java/android/net/metrics/DhcpErrorEventTest.kt @@ -0,0 +1,65 @@ +package android.net.metrics + +import android.net.metrics.DhcpErrorEvent.DHCP_INVALID_OPTION_LENGTH +import android.net.metrics.DhcpErrorEvent.errorCodeWithOption +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.parcelingRoundTrip +import java.lang.reflect.Modifier +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEST_ERROR_CODE = 12345 +//DHCP Optional Type: DHCP Subnet Mask (Copy from DhcpPacket.java due to it's protected) +private const val DHCP_SUBNET_MASK = 1 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class DhcpErrorEventTest { + + @Test + fun testConstructor() { + val event = DhcpErrorEvent(TEST_ERROR_CODE) + assertEquals(TEST_ERROR_CODE, event.errorCode) + } + + @Test + fun testParcelUnparcel() { + val event = DhcpErrorEvent(TEST_ERROR_CODE) + val parceled = parcelingRoundTrip(event) + assertEquals(TEST_ERROR_CODE, parceled.errorCode) + } + + @Test + fun testErrorCodeWithOption() { + val errorCode = errorCodeWithOption(DHCP_INVALID_OPTION_LENGTH, DHCP_SUBNET_MASK); + assertTrue((DHCP_INVALID_OPTION_LENGTH and errorCode) == DHCP_INVALID_OPTION_LENGTH); + assertTrue((DHCP_SUBNET_MASK and errorCode) == DHCP_SUBNET_MASK); + } + + @Test + fun testToString() { + val names = listOf("L2_ERROR", "L3_ERROR", "L4_ERROR", "DHCP_ERROR", "MISC_ERROR") + val errorFields = DhcpErrorEvent::class.java.declaredFields.filter { + it.type == Int::class.javaPrimitiveType + && Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers) + && it.name !in names + } + + errorFields.forEach { + val intValue = it.getInt(null) + val stringValue = DhcpErrorEvent(intValue).toString() + assertTrue("Invalid string for error 0x%08X (field %s): %s".format(intValue, it.name, + stringValue), + stringValue.contains(it.name)) + } + } + + @Test + fun testToString_InvalidErrorCode() { + assertNotNull(DhcpErrorEvent(TEST_ERROR_CODE).toString()) + } +} diff --git a/tests/common/java/android/net/metrics/IpConnectivityLogTest.java b/tests/common/java/android/net/metrics/IpConnectivityLogTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d4780d3a5d7b498b562904cee75416c285515244 --- /dev/null +++ b/tests/common/java/android/net/metrics/IpConnectivityLogTest.java @@ -0,0 +1,161 @@ +/* + * 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.metrics; + +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import android.net.ConnectivityMetricsEvent; +import android.net.IIpConnectivityMetrics; +import android.net.Network; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.BitUtils; + +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.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpConnectivityLogTest { + private static final int FAKE_NET_ID = 100; + private static final int[] FAKE_TRANSPORT_TYPES = BitUtils.unpackBits(TRANSPORT_WIFI); + private static final long FAKE_TIME_STAMP = System.currentTimeMillis(); + private static final String FAKE_INTERFACE_NAME = "test"; + private static final IpReachabilityEvent FAKE_EV = + new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED); + + @Mock IIpConnectivityMetrics mMockService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLoggingEvents() throws Exception { + IpConnectivityLog logger = new IpConnectivityLog(mMockService); + + assertTrue(logger.log(FAKE_EV)); + assertTrue(logger.log(FAKE_TIME_STAMP, FAKE_EV)); + assertTrue(logger.log(FAKE_NET_ID, FAKE_TRANSPORT_TYPES, FAKE_EV)); + assertTrue(logger.log(new Network(FAKE_NET_ID), FAKE_TRANSPORT_TYPES, FAKE_EV)); + assertTrue(logger.log(FAKE_INTERFACE_NAME, FAKE_EV)); + assertTrue(logger.log(makeExpectedEvent(FAKE_TIME_STAMP, FAKE_NET_ID, TRANSPORT_WIFI, + FAKE_INTERFACE_NAME))); + + List<ConnectivityMetricsEvent> got = verifyEvents(6); + assertEventsEqual(makeExpectedEvent(got.get(0).timestamp, 0, 0, null), got.get(0)); + assertEventsEqual(makeExpectedEvent(FAKE_TIME_STAMP, 0, 0, null), got.get(1)); + assertEventsEqual(makeExpectedEvent(got.get(2).timestamp, FAKE_NET_ID, + TRANSPORT_WIFI, null), got.get(2)); + assertEventsEqual(makeExpectedEvent(got.get(3).timestamp, FAKE_NET_ID, + TRANSPORT_WIFI, null), got.get(3)); + assertEventsEqual(makeExpectedEvent(got.get(4).timestamp, 0, 0, FAKE_INTERFACE_NAME), + got.get(4)); + assertEventsEqual(makeExpectedEvent(FAKE_TIME_STAMP, FAKE_NET_ID, + TRANSPORT_WIFI, FAKE_INTERFACE_NAME), got.get(5)); + } + + @Test + public void testLoggingEventsWithMultipleCallers() throws Exception { + IpConnectivityLog logger = new IpConnectivityLog(mMockService); + + final int nCallers = 10; + final int nEvents = 10; + for (int n = 0; n < nCallers; n++) { + final int i = n; + new Thread() { + public void run() { + for (int j = 0; j < nEvents; j++) { + assertTrue(logger.log(makeExpectedEvent( + FAKE_TIME_STAMP + i * 100 + j, + FAKE_NET_ID + i * 100 + j, + ((i + j) % 2 == 0) ? TRANSPORT_WIFI : TRANSPORT_CELLULAR, + FAKE_INTERFACE_NAME))); + } + } + }.start(); + } + + List<ConnectivityMetricsEvent> got = verifyEvents(nCallers * nEvents, 200); + Collections.sort(got, EVENT_COMPARATOR); + Iterator<ConnectivityMetricsEvent> iter = got.iterator(); + for (int i = 0; i < nCallers; i++) { + for (int j = 0; j < nEvents; j++) { + final long expectedTimestamp = FAKE_TIME_STAMP + i * 100 + j; + final int expectedNetId = FAKE_NET_ID + i * 100 + j; + final long expectedTransports = + ((i + j) % 2 == 0) ? TRANSPORT_WIFI : TRANSPORT_CELLULAR; + assertEventsEqual(makeExpectedEvent(expectedTimestamp, expectedNetId, + expectedTransports, FAKE_INTERFACE_NAME), iter.next()); + } + } + } + + private List<ConnectivityMetricsEvent> verifyEvents(int n, int timeoutMs) throws Exception { + ArgumentCaptor<ConnectivityMetricsEvent> captor = + ArgumentCaptor.forClass(ConnectivityMetricsEvent.class); + verify(mMockService, timeout(timeoutMs).times(n)).logEvent(captor.capture()); + return captor.getAllValues(); + } + + private List<ConnectivityMetricsEvent> verifyEvents(int n) throws Exception { + return verifyEvents(n, 10); + } + + + private ConnectivityMetricsEvent makeExpectedEvent(long timestamp, int netId, long transports, + String ifname) { + ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent(); + ev.timestamp = timestamp; + ev.data = FAKE_EV; + ev.netId = netId; + ev.transports = transports; + ev.ifname = ifname; + return ev; + } + + /** Outer equality for ConnectivityMetricsEvent to avoid overriding equals() and hashCode(). */ + private void assertEventsEqual(ConnectivityMetricsEvent expected, + ConnectivityMetricsEvent got) { + assertEquals(expected.data, got.data); + assertEquals(expected.timestamp, got.timestamp); + assertEquals(expected.netId, got.netId); + assertEquals(expected.transports, got.transports); + assertEquals(expected.ifname, got.ifname); + } + + static final Comparator<ConnectivityMetricsEvent> EVENT_COMPARATOR = + Comparator.comparingLong((ev) -> ev.timestamp); +} diff --git a/tests/common/java/android/net/metrics/IpManagerEventTest.kt b/tests/common/java/android/net/metrics/IpManagerEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..64be50837fc92ae2a34a0d1d9f301bd00e08364c --- /dev/null +++ b/tests/common/java/android/net/metrics/IpManagerEventTest.kt @@ -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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class IpManagerEventTest { + @Test + fun testConstructorAndParcel() { + (IpManagerEvent.PROVISIONING_OK..IpManagerEvent.ERROR_INTERFACE_NOT_FOUND).forEach { + val ipManagerEvent = IpManagerEvent(it, Long.MAX_VALUE) + assertEquals(it, ipManagerEvent.eventType) + assertEquals(Long.MAX_VALUE, ipManagerEvent.durationMs) + + assertParcelSane(ipManagerEvent, 2) + } + } +} diff --git a/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..55b5e492dd4732a577944367e72914948f32cf97 --- /dev/null +++ b/tests/common/java/android/net/metrics/IpReachabilityEventTest.kt @@ -0,0 +1,38 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class IpReachabilityEventTest { + @Test + fun testConstructorAndParcel() { + (IpReachabilityEvent.PROBE..IpReachabilityEvent.PROVISIONING_LOST_ORGANIC).forEach { + val ipReachabilityEvent = IpReachabilityEvent(it) + assertEquals(it, ipReachabilityEvent.eventType) + + assertParcelSane(ipReachabilityEvent, 1) + } + } +} diff --git a/tests/common/java/android/net/metrics/NetworkEventTest.kt b/tests/common/java/android/net/metrics/NetworkEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..41430b03a1eb79b2e02970f551ce0b58c4b0f797 --- /dev/null +++ b/tests/common/java/android/net/metrics/NetworkEventTest.kt @@ -0,0 +1,43 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetworkEventTest { + @Test + fun testConstructorAndParcel() { + (NetworkEvent.NETWORK_CONNECTED..NetworkEvent.NETWORK_PARTIAL_CONNECTIVITY).forEach { + var networkEvent = NetworkEvent(it) + assertEquals(it, networkEvent.eventType) + assertEquals(0, networkEvent.durationMs) + + networkEvent = NetworkEvent(it, Long.MAX_VALUE) + assertEquals(it, networkEvent.eventType) + assertEquals(Long.MAX_VALUE, networkEvent.durationMs) + + assertParcelSane(networkEvent, 2) + } + } +} diff --git a/tests/common/java/android/net/metrics/RaEventTest.kt b/tests/common/java/android/net/metrics/RaEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d9b720332fbe21064812306ab21d6fa53772b0cf --- /dev/null +++ b/tests/common/java/android/net/metrics/RaEventTest.kt @@ -0,0 +1,72 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +private const val NO_LIFETIME: Long = -1L + +@RunWith(AndroidJUnit4::class) +@SmallTest +class RaEventTest { + @Test + fun testConstructorAndParcel() { + var raEvent = RaEvent.Builder().build() + assertEquals(NO_LIFETIME, raEvent.routerLifetime) + assertEquals(NO_LIFETIME, raEvent.prefixValidLifetime) + assertEquals(NO_LIFETIME, raEvent.prefixPreferredLifetime) + assertEquals(NO_LIFETIME, raEvent.routeInfoLifetime) + assertEquals(NO_LIFETIME, raEvent.rdnssLifetime) + assertEquals(NO_LIFETIME, raEvent.dnsslLifetime) + + raEvent = RaEvent.Builder() + .updateRouterLifetime(1) + .updatePrefixValidLifetime(2) + .updatePrefixPreferredLifetime(3) + .updateRouteInfoLifetime(4) + .updateRdnssLifetime(5) + .updateDnsslLifetime(6) + .build() + assertEquals(1, raEvent.routerLifetime) + assertEquals(2, raEvent.prefixValidLifetime) + assertEquals(3, raEvent.prefixPreferredLifetime) + assertEquals(4, raEvent.routeInfoLifetime) + assertEquals(5, raEvent.rdnssLifetime) + assertEquals(6, raEvent.dnsslLifetime) + + raEvent = RaEvent.Builder() + .updateRouterLifetime(Long.MIN_VALUE) + .updateRouterLifetime(Long.MAX_VALUE) + .build() + assertEquals(Long.MIN_VALUE, raEvent.routerLifetime) + + raEvent = RaEvent(1, 2, 3, 4, 5, 6) + assertEquals(1, raEvent.routerLifetime) + assertEquals(2, raEvent.prefixValidLifetime) + assertEquals(3, raEvent.prefixPreferredLifetime) + assertEquals(4, raEvent.routeInfoLifetime) + assertEquals(5, raEvent.rdnssLifetime) + assertEquals(6, raEvent.dnsslLifetime) + + assertParcelSane(raEvent, 6) + } +} diff --git a/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..51c0d41bf4d5beeca8cc61992fd235eb51654c9e --- /dev/null +++ b/tests/common/java/android/net/metrics/ValidationProbeEventTest.kt @@ -0,0 +1,72 @@ +/* + * 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.metrics + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import java.lang.reflect.Modifier +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +private const val FIRST_VALIDATION: Int = 1 shl 8 +private const val REVALIDATION: Int = 2 shl 8 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ValidationProbeEventTest { + private infix fun Int.hasType(type: Int) = (type and this) == type + + @Test + fun testBuilderAndParcel() { + var validationProbeEvent = ValidationProbeEvent.Builder() + .setProbeType(ValidationProbeEvent.PROBE_DNS, false).build() + + assertTrue(validationProbeEvent.probeType hasType REVALIDATION) + + validationProbeEvent = ValidationProbeEvent.Builder() + .setDurationMs(Long.MAX_VALUE) + .setProbeType(ValidationProbeEvent.PROBE_DNS, true) + .setReturnCode(ValidationProbeEvent.DNS_SUCCESS) + .build() + + assertEquals(Long.MAX_VALUE, validationProbeEvent.durationMs) + assertTrue(validationProbeEvent.probeType hasType ValidationProbeEvent.PROBE_DNS) + assertTrue(validationProbeEvent.probeType hasType FIRST_VALIDATION) + assertEquals(ValidationProbeEvent.DNS_SUCCESS, validationProbeEvent.returnCode) + + assertParcelSane(validationProbeEvent, 3) + } + + @Test + fun testGetProbeName() { + val probeFields = ValidationProbeEvent::class.java.declaredFields.filter { + it.type == Int::class.javaPrimitiveType + && Modifier.isPublic(it.modifiers) && Modifier.isStatic(it.modifiers) + && it.name.contains("PROBE") + } + + probeFields.forEach { + val intValue = it.getInt(null) + val stringValue = ValidationProbeEvent.getProbeName(intValue) + assertEquals(it.name, stringValue) + } + + } +} diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b22e45db90abf757ea5ef3e35ef688c125c3489 --- /dev/null +++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.netstats + +import android.net.NetworkStats +import android.net.NetworkStats.DEFAULT_NETWORK_NO +import android.net.NetworkStats.DEFAULT_NETWORK_YES +import android.net.NetworkStats.Entry +import android.net.NetworkStats.IFACE_VT +import android.net.NetworkStats.METERED_NO +import android.net.NetworkStats.METERED_YES +import android.net.NetworkStats.ROAMING_NO +import android.net.NetworkStats.ROAMING_YES +import android.net.NetworkStats.SET_DEFAULT +import android.net.NetworkStats.SET_FOREGROUND +import android.net.NetworkStats.TAG_NONE +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.assertFieldCountEquals +import com.android.testutils.assertNetworkStatsEquals +import com.android.testutils.assertParcelingIsLossless +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +@SmallTest +class NetworkStatsApiTest { + @Rule + @JvmField + val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q) + + private val testStatsEmpty = NetworkStats(0L, 0) + + // Note that these variables need to be initialized outside of constructor, initialize + // here with methods that don't exist in Q devices will result in crash. + + // stats1 and stats2 will have some entries with common keys, which are expected to + // be merged if performing add on these 2 stats. + private lateinit var testStats1: NetworkStats + private lateinit var testStats2: NetworkStats + + // This is a result of adding stats1 and stats2, while the merging of common key items is + // subject to test later, this should not be initialized with for a loop to add stats1 + // and stats2 above. + private lateinit var testStats3: NetworkStats + + companion object { + private const val TEST_IFACE = "test0" + private const val TEST_UID1 = 1001 + private const val TEST_UID2 = 1002 + } + + @Before + fun setUp() { + testStats1 = NetworkStats(0L, 0) + // Entries which only appear in set1. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 31, 7, 24, 5, 8)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 25, 3, 47, 8, 2)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 37, 52, 1, 10, 4)) + // Entries which are common for set1 and set2. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 101, 2, 103, 4, 5)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 17, 2, 11, 1, 0)) + .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 40, 1, 0, 0, 8)) + .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 1, 6, 2, 0)) + assertEquals(8, testStats1.size()) + + testStats2 = NetworkStats(0L, 0) + // Entries which are common for set1 and set2. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45)) + .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7)) + .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0)) + // Entry which only appears in set2. + .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0)) + assertEquals(5, testStats2.size()) + + testStats3 = NetworkStats(0L, 9) + // Entries which are unique either in stats1 or stats2. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 101, 2, 103, 4, 5)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_YES, DEFAULT_NETWORK_NO, 31, 7, 24, 5, 8)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, 25, 3, 47, 8, 2)) + .addEntry(Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0)) + // Entries which are common for stats1 and stats2 are being merged. + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, 20, 3, 57, 40, 3)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 20, 17, 13, 32, 1)) + .addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 50, 113, 11, 11, 49)) + .addEntry(Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 51, 3, 3, 4, 15)) + .addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 7, 4, 8, 3, 0)) + assertEquals(9, testStats3.size()) + } + + @Test + fun testAddEntry() { + val expectedEntriesInStats2 = arrayOf( + Entry(TEST_IFACE, TEST_UID1, SET_DEFAULT, 0x80, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 3, 15, 2, 31, 1), + Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45), + Entry(TEST_IFACE, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 11, 2, 3, 4, 7), + Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 4, 3, 2, 1, 0), + Entry(IFACE_VT, TEST_UID2, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 2, 3, 7, 8, 0)) + + // While testStats* are already initialized with addEntry, verify content added + // matches expectation. + for (i in expectedEntriesInStats2.indices) { + val entry = testStats2.getValues(i, null) + assertEquals(expectedEntriesInStats2[i], entry) + } + + // Verify entry updated with addEntry. + val stats = testStats2.addEntry(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 12, -5, 7, 0, 9)) + assertEquals(Entry(IFACE_VT, TEST_UID1, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 16, -2, 9, 1, 9), + stats.getValues(3, null)) + } + + @Test + fun testAdd() { + var stats = NetworkStats(0L, 0) + assertNetworkStatsEquals(testStatsEmpty, stats) + stats = stats.add(testStats2) + assertNetworkStatsEquals(testStats2, stats) + stats = stats.add(testStats1) + // EMPTY + STATS2 + STATS1 = STATS3 + assertNetworkStatsEquals(testStats3, stats) + } + + @Test + fun testParcelUnparcel() { + assertParcelingIsLossless(testStatsEmpty) + assertParcelingIsLossless(testStats1) + assertParcelingIsLossless(testStats2) + assertFieldCountEquals(15, NetworkStats::class.java) + } + + @Test + fun testDescribeContents() { + assertEquals(0, testStatsEmpty.describeContents()) + assertEquals(0, testStats1.describeContents()) + assertEquals(0, testStats2.describeContents()) + assertEquals(0, testStats3.describeContents()) + } + + @Test + fun testSubtract() { + // STATS3 - STATS2 = STATS1 + assertNetworkStatsEquals(testStats1, testStats3.subtract(testStats2)) + // STATS3 - STATS1 = STATS2 + assertNetworkStatsEquals(testStats2, testStats3.subtract(testStats1)) + } + + @Test + fun testMethodsDontModifyReceiver() { + listOf(testStatsEmpty, testStats1, testStats2, testStats3).forEach { + val origStats = it.clone() + it.addEntry(Entry(TEST_IFACE, TEST_UID1, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 13, 61, 10, 1, 45)) + it.add(testStats3) + it.subtract(testStats1) + assertNetworkStatsEquals(origStats, it) + } + } +} \ No newline at end of file diff --git a/tests/common/java/android/net/util/SocketUtilsTest.kt b/tests/common/java/android/net/util/SocketUtilsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..aaf97f36889b9e72060593282897a26807f3f1e6 --- /dev/null +++ b/tests/common/java/android/net/util/SocketUtilsTest.kt @@ -0,0 +1,90 @@ +/* + * 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.os.Build +import android.system.NetlinkSocketAddress +import android.system.Os +import android.system.OsConstants.AF_INET +import android.system.OsConstants.ETH_P_ALL +import android.system.OsConstants.IPPROTO_UDP +import android.system.OsConstants.RTMGRP_NEIGH +import android.system.OsConstants.SOCK_DGRAM +import android.system.PacketSocketAddress +import androidx.test.filters.SmallTest +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.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +private const val TEST_INDEX = 123 +private const val TEST_PORT = 555 +private const val FF_BYTE = 0xff.toByte() + +@RunWith(AndroidJUnit4::class) +@SmallTest +class SocketUtilsTest { + @Rule @JvmField + val ignoreRule = DevSdkIgnoreRule() + + @Test + fun testMakeNetlinkSocketAddress() { + val nlAddress = SocketUtils.makeNetlinkSocketAddress(TEST_PORT, RTMGRP_NEIGH) + if (nlAddress is NetlinkSocketAddress) { + assertEquals(TEST_PORT, nlAddress.getPortId()) + assertEquals(RTMGRP_NEIGH, nlAddress.getGroupsMask()) + } else { + fail("Not NetlinkSocketAddress object") + } + } + + @Test + fun testMakePacketSocketAddress_Q() { + val pkAddress = SocketUtils.makePacketSocketAddress(ETH_P_ALL, TEST_INDEX) + assertTrue("Not PacketSocketAddress object", pkAddress is PacketSocketAddress) + + val pkAddress2 = SocketUtils.makePacketSocketAddress(TEST_INDEX, ByteArray(6) { FF_BYTE }) + assertTrue("Not PacketSocketAddress object", pkAddress2 is PacketSocketAddress) + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) + fun testMakePacketSocketAddress() { + val pkAddress = SocketUtils.makePacketSocketAddress( + ETH_P_ALL, TEST_INDEX, ByteArray(6) { FF_BYTE }) + assertTrue("Not PacketSocketAddress object", pkAddress is PacketSocketAddress) + } + + @Test + fun testCloseSocket() { + // Expect no exception happening with null object. + SocketUtils.closeSocket(null) + + val fd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + assertTrue(fd.valid()) + SocketUtils.closeSocket(fd) + assertFalse(fd.valid()) + // Expecting socket should be still invalid even closed socket again. + SocketUtils.closeSocket(fd) + assertFalse(fd.valid()) + } +} diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..806f805dd3cdab4ac58c6bde822a31ded226901b --- /dev/null +++ b/tests/deflake/Android.bp @@ -0,0 +1,35 @@ +// +// 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 { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_test_host { + name: "FrameworksNetDeflakeTest", + srcs: ["src/**/*.kt"], + libs: [ + "junit", + "tradefed", + ], + static_libs: [ + "kotlin-test", + "net-host-tests-utils", + ], + data: [":FrameworksNetTests"], + test_suites: ["device-tests"], +} diff --git a/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt b/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..62855255fec25426ebc0421df9a4509fc7db65e9 --- /dev/null +++ b/tests/deflake/src/com/android/server/net/FrameworksNetDeflakeTest.kt @@ -0,0 +1,28 @@ +/* + * 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.server.net + +import com.android.testutils.host.DeflakeHostTestBase +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner +import org.junit.runner.RunWith + +@RunWith(DeviceJUnit4ClassRunner::class) +class FrameworksNetDeflakeTest: DeflakeHostTestBase() { + override val runCount = 20 + override val testApkFilename = "FrameworksNetTests.apk" + override val testClasses = listOf("com.android.server.ConnectivityServiceTest") +} \ No newline at end of file diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..81f9b4879e8b721579950830d25e426e1aba1424 --- /dev/null +++ b/tests/integration/Android.bp @@ -0,0 +1,74 @@ +// +// 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 { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "FrameworksNetIntegrationTests", + defaults: ["framework-connectivity-test-defaults"], + platform_apis: true, + certificate: "platform", + srcs: [ + "src/**/*.kt", + "src/**/*.aidl", + ], + libs: [ + "android.test.mock", + ], + static_libs: [ + "NetworkStackApiStableLib", + "androidx.test.ext.junit", + "frameworks-net-integration-testutils", + "kotlin-reflect", + "mockito-target-extended-minus-junit4", + "net-tests-utils", + "service-connectivity", + "services.core", + "services.net", + "testables", + ], + test_suites: ["device-tests"], + use_embedded_native_libs: true, + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + // android_library does not include JNI libs: include NetworkStack dependencies here + "libnativehelper_compat_libc++", + "libnetworkstackutilsjni", + ], +} + +// Utilities for testing framework code both in integration and unit tests. +java_library { + name: "frameworks-net-integration-testutils", + defaults: ["framework-connectivity-test-defaults"], + srcs: ["util/**/*.java", "util/**/*.kt"], + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.rules", + "junit", + "net-tests-utils", + ], + libs: [ + "service-connectivity", + "services.core", + "services.net", + ], +} diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..2e1368935759b0fb779db49e3ca25b6c02aacf8f --- /dev/null +++ b/tests/integration/AndroidManifest.xml @@ -0,0 +1,73 @@ +<?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.server.net.integrationtests"> + + <!-- For ConnectivityService registerReceiverAsUser (receiving broadcasts) --> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> + <!-- PermissionMonitor sets network permissions for each user --> + <uses-permission android:name="android.permission.MANAGE_USERS"/> + <!-- ConnectivityService sends notifications to BatteryStats --> + <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS"/> + <!-- Reading network status --> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + <uses-permission android:name="android.permission.NETWORK_FACTORY"/> + <!-- Obtain LinkProperties callbacks with sensitive fields --> + <uses-permission android:name="android.permission.NETWORK_SETTINGS" /> + <uses-permission android:name="android.permission.NETWORK_STACK"/> + <uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY"/> + <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> + <!-- Reading DeviceConfig flags --> + <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/> + <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> + <!-- Querying the resources package --> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> + <application android:debuggable="true"> + <uses-library android:name="android.test.runner"/> + + <!-- This manifest is merged with the base manifest of the real NetworkStack app. + Remove the NetworkStackService from the base (real) manifest, and replace with a test + service that responds to the same intent --> + <service android:name=".TestNetworkStackService" + android:process="com.android.server.net.integrationtests.testnetworkstack" + android:exported="true"> + <intent-filter> + <action android:name="android.net.INetworkStackConnector.Test"/> + </intent-filter> + </service> + <service android:name=".NetworkStackInstrumentationService" + android:process="com.android.server.net.integrationtests.testnetworkstack" + android:exported="true"> + <intent-filter> + <action android:name=".INetworkStackInstrumentation"/> + </intent-filter> + </service> + <service android:name="com.android.server.connectivity.ipmemorystore.RegularMaintenanceJobService" + android:process="com.android.server.net.integrationtests.testnetworkstack" + android:permission="android.permission.BIND_JOB_SERVICE"/> + + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.net.integrationtests" + android:label="Frameworks Net Integration Tests"/> + +</manifest> diff --git a/tests/integration/res/values/config.xml b/tests/integration/res/values/config.xml new file mode 100644 index 0000000000000000000000000000000000000000..2c8046ffd781a6dc94b22f8c9b34363ad2c97896 --- /dev/null +++ b/tests/integration/res/values/config.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- + Override configuration for testing. The below settings use the config_ variants, which are + normally used by RROs to override the setting with highest priority. --> + <integer name="config_captive_portal_dns_probe_timeout">12500</integer> + <string name="config_captive_portal_http_url" translatable="false">http://test.android.com</string> + <string name="config_captive_portal_https_url" translatable="false">https://secure.test.android.com</string> + <string-array name="config_captive_portal_fallback_urls" translatable="false"> + <item>http://fallback1.android.com</item> + <item>http://fallback2.android.com</item> + </string-array> + <string-array name="config_captive_portal_fallback_probe_specs" translatable="false"> + </string-array> +</resources> diff --git a/tests/integration/src/android/net/TestNetworkStackClient.kt b/tests/integration/src/android/net/TestNetworkStackClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..61ef5bdca4872d7484da1104543e957eb51e131f --- /dev/null +++ b/tests/integration/src/android/net/TestNetworkStackClient.kt @@ -0,0 +1,75 @@ +/* + * 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.content.ComponentName +import android.content.Context +import android.content.Intent +import android.net.networkstack.NetworkStackClientBase +import android.os.IBinder +import com.android.server.net.integrationtests.TestNetworkStackService +import org.mockito.Mockito.any +import org.mockito.Mockito.spy +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import kotlin.test.fail + +const val TEST_ACTION_SUFFIX = ".Test" + +class TestNetworkStackClient(private val context: Context) : NetworkStackClientBase() { + // TODO: consider switching to TrackRecord for more expressive checks + private val lastCallbacks = HashMap<Network, INetworkMonitorCallbacks>() + private val moduleConnector = ConnectivityModuleConnector { _, action, _, _ -> + val intent = Intent(action) + val serviceName = TestNetworkStackService::class.qualifiedName + ?: fail("TestNetworkStackService name not found") + intent.component = ComponentName(context.packageName, serviceName) + return@ConnectivityModuleConnector intent + }.also { it.init(context) } + + fun start() { + moduleConnector.startModuleService( + INetworkStackConnector::class.qualifiedName + TEST_ACTION_SUFFIX, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) { connector -> + onNetworkStackConnected(INetworkStackConnector.Stub.asInterface(connector)) + } + } + + // base may be an instance of an inaccessible subclass, so non-spyable. + // Use a known open class that delegates to the original instance for all methods except + // asBinder. asBinder needs to use its own non-delegated implementation as otherwise it would + // return a binder token to a class that is not spied on. + open class NetworkMonitorCallbacksWrapper(private val base: INetworkMonitorCallbacks) : + INetworkMonitorCallbacks.Stub(), INetworkMonitorCallbacks by base { + // asBinder is implemented by both base class and delegate: specify explicitly + override fun asBinder(): IBinder { + return super.asBinder() + } + } + + override fun makeNetworkMonitor(network: Network, name: String?, cb: INetworkMonitorCallbacks) { + val cbSpy = spy(NetworkMonitorCallbacksWrapper(cb)) + lastCallbacks[network] = cbSpy + super.makeNetworkMonitor(network, name, cbSpy) + } + + fun verifyNetworkMonitorCreated(network: Network, timeoutMs: Long) { + val cb = lastCallbacks[network] + ?: fail("NetworkMonitor for network $network not requested") + verify(cb, timeout(timeoutMs)).onNetworkMonitorCreated(any()) + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e039ef072542623bbfbc114de9ff323793f054f3 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt @@ -0,0 +1,257 @@ +/* + * 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.server.net.integrationtests + +import android.app.usage.NetworkStatsManager +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.content.Context.BIND_IMPORTANT +import android.content.Intent +import android.content.ServiceConnection +import android.net.ConnectivityManager +import android.net.IDnsResolver +import android.net.INetd +import android.net.LinkProperties +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.NetworkRequest +import android.net.TestNetworkStackClient +import android.net.Uri +import android.net.metrics.IpConnectivityLog +import android.os.ConditionVariable +import android.os.IBinder +import android.os.SystemConfigManager +import android.os.UserHandle +import android.testing.TestableContext +import android.util.Log +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.ConnectivityService +import com.android.server.NetworkAgentWrapper +import com.android.server.TestNetIdManager +import com.android.server.connectivity.MockableSystemProperties +import com.android.server.connectivity.ProxyTracker +import com.android.testutils.TestableNetworkCallback +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.AdditionalAnswers +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.MockitoAnnotations +import org.mockito.Spy +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +const val SERVICE_BIND_TIMEOUT_MS = 5_000L +const val TEST_TIMEOUT_MS = 10_000L + +/** + * Test that exercises an instrumented version of ConnectivityService against an instrumented + * NetworkStack in a different test process. + */ +@RunWith(AndroidJUnit4::class) +class ConnectivityServiceIntegrationTest { + // lateinit used here for mocks as they need to be reinitialized between each test and the test + // should crash if they are used before being initialized. + @Mock + private lateinit var statsManager: NetworkStatsManager + @Mock + private lateinit var log: IpConnectivityLog + @Mock + private lateinit var netd: INetd + @Mock + private lateinit var dnsResolver: IDnsResolver + @Mock + private lateinit var systemConfigManager: SystemConfigManager + @Spy + private var context = TestableContext(realContext) + + // lateinit for these three 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 networkStackClient: TestNetworkStackClient + private lateinit var service: ConnectivityService + private lateinit var cm: ConnectivityManager + + companion object { + // lateinit for this binder token, as it must be initialized before any test code is run + // and use of it before init should crash the test. + private lateinit var nsInstrumentation: INetworkStackInstrumentation + private val bindingCondition = ConditionVariable(false) + + private val realContext get() = InstrumentationRegistry.getInstrumentation().context + private val httpProbeUrl get() = + realContext.getResources().getString(R.string.config_captive_portal_http_url) + private val httpsProbeUrl get() = + realContext.getResources().getString(R.string.config_captive_portal_https_url) + + private class InstrumentationServiceConnection : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.i("TestNetworkStack", "Service connected") + try { + if (service == null) fail("Error binding to NetworkStack instrumentation") + if (::nsInstrumentation.isInitialized) fail("Service already connected") + nsInstrumentation = INetworkStackInstrumentation.Stub.asInterface(service) + } finally { + bindingCondition.open() + } + } + + override fun onServiceDisconnected(name: ComponentName?) = Unit + } + + @BeforeClass + @JvmStatic + fun setUpClass() { + val intent = Intent(realContext, NetworkStackInstrumentationService::class.java) + intent.action = INetworkStackInstrumentation::class.qualifiedName + assertTrue(realContext.bindService(intent, InstrumentationServiceConnection(), + BIND_AUTO_CREATE or BIND_IMPORTANT), + "Error binding to instrumentation service") + assertTrue(bindingCondition.block(SERVICE_BIND_TIMEOUT_MS), + "Timed out binding to instrumentation service " + + "after $SERVICE_BIND_TIMEOUT_MS ms") + } + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + val asUserCtx = mock(Context::class.java, AdditionalAnswers.delegatesTo<Context>(context)) + doReturn(UserHandle.ALL).`when`(asUserCtx).user + doReturn(asUserCtx).`when`(context).createContextAsUser(eq(UserHandle.ALL), anyInt()) + doNothing().`when`(context).sendStickyBroadcast(any(), any()) + doReturn(Context.SYSTEM_CONFIG_SERVICE).`when`(context) + .getSystemServiceName(SystemConfigManager::class.java) + doReturn(systemConfigManager).`when`(context) + .getSystemService(Context.SYSTEM_CONFIG_SERVICE) + doReturn(IntArray(0)).`when`(systemConfigManager).getSystemPermissionUids(anyString()) + + networkStackClient = TestNetworkStackClient(realContext) + networkStackClient.start() + + service = TestConnectivityService(makeDependencies()) + cm = ConnectivityManager(context, service) + context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm) + context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager) + + service.systemReadyInternal() + } + + private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService( + context, dnsResolver, log, netd, deps) + + private fun makeDependencies(): ConnectivityService.Dependencies { + val deps = spy(ConnectivityService.Dependencies()) + doReturn(networkStackClient).`when`(deps).networkStack + doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any()) + doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties + doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager() + return deps + } + + @After + fun tearDown() { + nsInstrumentation.clearAllState() + } + + @Test + fun testValidation() { + val request = NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + val testCallback = TestableNetworkCallback() + + cm.registerNetworkCallback(request, testCallback) + nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204)) + nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204)) + + val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), null /* ncTemplate */, + context) + networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS) + + na.addCapability(NET_CAPABILITY_INTERNET) + na.connect() + + testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS) + assertEquals(2, nsInstrumentation.getRequestUrls().size) + } + + @Test + fun testCapportApi() { + val request = NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .build() + val testCb = TestableNetworkCallback() + val apiUrl = "https://capport.android.com" + + cm.registerNetworkCallback(request, testCb) + nsInstrumentation.addHttpResponse(HttpResponse( + apiUrl, + """ + |{ + | "captive": true, + | "user-portal-url": "https://login.capport.android.com", + | "venue-info-url": "https://venueinfo.capport.android.com" + |} + """.trimMargin())) + + // Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the + // HTTP probe as it should not be sent. + // Even if the HTTPS probe succeeds, a portal should be detected as the API takes precedence + // in that case. + nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204)) + + val lp = LinkProperties() + lp.captivePortalApiUrl = Uri.parse(apiUrl) + val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context) + networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS) + + na.addCapability(NET_CAPABILITY_INTERNET) + na.connect() + + testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS) + + val capportData = testCb.expectLinkPropertiesThat(na, TEST_TIMEOUT_MS) { + it.captivePortalData != null + }.lp.captivePortalData + assertNotNull(capportData) + assertTrue(capportData.isCaptive) + assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl) + assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl) + + val nc = testCb.expectCapabilitiesWith(NET_CAPABILITY_CAPTIVE_PORTAL, na, TEST_TIMEOUT_MS) + assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED)) + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl new file mode 100644 index 0000000000000000000000000000000000000000..9a2bcfea7641b4d154b2eeda65025015697cf939 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.aidl @@ -0,0 +1,19 @@ +/* + * 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.server.net.integrationtests; + +parcelable HttpResponse; \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..e2063138fef1b9341a0e17d90e02b3ee1e50cb3c --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/HttpResponse.kt @@ -0,0 +1,49 @@ +/* + * 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.server.net.integrationtests + +import android.os.Parcel +import android.os.Parcelable + +data class HttpResponse( + val requestUrl: String, + val responseCode: Int, + val content: String = "", + val redirectUrl: String? = null +) : Parcelable { + constructor(p: Parcel): this(p.readString(), p.readInt(), p.readString(), p.readString()) + constructor(requestUrl: String, contentBody: String): this( + requestUrl, + responseCode = 200, + content = contentBody, + redirectUrl = null) + + override fun writeToParcel(dest: Parcel, flags: Int) { + with(dest) { + writeString(requestUrl) + writeInt(responseCode) + writeString(content) + writeString(redirectUrl) + } + } + + override fun describeContents() = 0 + companion object CREATOR : Parcelable.Creator<HttpResponse> { + override fun createFromParcel(source: Parcel) = HttpResponse(source) + override fun newArray(size: Int) = arrayOfNulls<HttpResponse?>(size) + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl b/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.aidl new file mode 100644 index 0000000000000000000000000000000000000000..efc58add9cf5046b0ce7f9313cc6853ef01a5d63 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/INetworkStackInstrumentation.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 com.android.server.net.integrationtests; + +import com.android.server.net.integrationtests.HttpResponse; + +interface INetworkStackInstrumentation { + void clearAllState(); + void addHttpResponse(in HttpResponse response); + List<String> getRequestUrls(); +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt new file mode 100644 index 0000000000000000000000000000000000000000..e807952cec11d421fc9ed491b53e7cc9148dab57 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/NetworkStackInstrumentationService.kt @@ -0,0 +1,84 @@ +/* + * 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.server.net.integrationtests + +import android.app.Service +import android.content.Intent +import java.net.URL +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.collections.ArrayList +import kotlin.test.fail + +/** + * An instrumentation interface for the NetworkStack that allows controlling behavior to + * facilitate integration tests. + */ +class NetworkStackInstrumentationService : Service() { + override fun onBind(intent: Intent) = InstrumentationConnector.asBinder() + + object InstrumentationConnector : INetworkStackInstrumentation.Stub() { + private val httpResponses = ConcurrentHashMap<String, ConcurrentLinkedQueue<HttpResponse>>() + .run { + withDefault { key -> getOrPut(key) { ConcurrentLinkedQueue() } } + } + private val httpRequestUrls = Collections.synchronizedList(ArrayList<String>()) + + /** + * Called when an HTTP request is being processed by NetworkMonitor. Returns the response + * that should be simulated. + */ + fun processRequest(url: URL): HttpResponse { + val strUrl = url.toString() + httpRequestUrls.add(strUrl) + return httpResponses[strUrl]?.poll() + ?: fail("No mocked response for request: $strUrl. " + + "Mocked URL keys are: ${httpResponses.keys}") + } + + /** + * Clear all state of this connector. This is intended for use between two tests, so all + * state should be reset as if the connector was just created. + */ + override fun clearAllState() { + httpResponses.clear() + httpRequestUrls.clear() + } + + /** + * Add a response to a future HTTP request. + * + * <p>For any subsequent HTTP/HTTPS query, the first response with a matching URL will be + * used to mock the query response. + * + * <p>All requests that are expected to be sent must have a mock response: if an unexpected + * request is seen, the test will fail. + */ + override fun addHttpResponse(response: HttpResponse) { + httpResponses.getValue(response.requestUrl).add(response) + } + + /** + * Get the ordered list of request URLs that have been sent by NetworkMonitor, and were + * answered based on mock responses. + */ + override fun getRequestUrls(): List<String> { + return ArrayList(httpRequestUrls) + } + } +} \ No newline at end of file diff --git a/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt new file mode 100644 index 0000000000000000000000000000000000000000..eff66584d6c1b93f2be0d86f43a860a79dc4b534 --- /dev/null +++ b/tests/integration/src/com/android/server/net/integrationtests/TestNetworkStackService.kt @@ -0,0 +1,100 @@ +/* + * 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.server.net.integrationtests + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.INetworkMonitorCallbacks +import android.net.Network +import android.net.metrics.IpConnectivityLog +import android.net.util.SharedLog +import android.os.IBinder +import com.android.networkstack.netlink.TcpSocketTracker +import com.android.server.NetworkStackService +import com.android.server.NetworkStackService.NetworkMonitorConnector +import com.android.server.NetworkStackService.NetworkStackConnector +import com.android.server.connectivity.NetworkMonitor +import com.android.server.net.integrationtests.NetworkStackInstrumentationService.InstrumentationConnector +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import java.io.ByteArrayInputStream +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.nio.charset.StandardCharsets + +private const val TEST_NETID = 42 + +/** + * Android service that can return an [android.net.INetworkStackConnector] which can be instrumented + * through [NetworkStackInstrumentationService]. + * Useful in tests to create test instrumented NetworkStack components that can receive + * instrumentation commands through [NetworkStackInstrumentationService]. + */ +class TestNetworkStackService : Service() { + override fun onBind(intent: Intent): IBinder = TestNetworkStackConnector(makeTestContext()) + + private fun makeTestContext() = spy(applicationContext).also { + doReturn(mock(IBinder::class.java)).`when`(it).getSystemService(Context.NETD_SERVICE) + } + + private class TestPermissionChecker : NetworkStackService.PermissionChecker() { + override fun enforceNetworkStackCallingPermission() = Unit + } + + private class NetworkMonitorDeps(private val privateDnsBypassNetwork: Network) : + NetworkMonitor.Dependencies() { + override fun getPrivateDnsBypassNetwork(network: Network?) = privateDnsBypassNetwork + } + + private inner class TestNetworkStackConnector(context: Context) : NetworkStackConnector( + context, TestPermissionChecker(), NetworkStackService.Dependencies()) { + + private val network = Network(TEST_NETID) + private val privateDnsBypassNetwork = TestNetwork(TEST_NETID) + + private inner class TestNetwork(netId: Int) : Network(netId) { + override fun openConnection(url: URL): URLConnection { + val response = InstrumentationConnector.processRequest(url) + val responseBytes = response.content.toByteArray(StandardCharsets.UTF_8) + + val connection = mock(HttpURLConnection::class.java) + doReturn(response.responseCode).`when`(connection).responseCode + doReturn(responseBytes.size.toLong()).`when`(connection).contentLengthLong + doReturn(response.redirectUrl).`when`(connection).getHeaderField("location") + doReturn(ByteArrayInputStream(responseBytes)).`when`(connection).inputStream + return connection + } + } + + override fun makeNetworkMonitor( + network: Network, + name: String?, + cb: INetworkMonitorCallbacks + ) { + val nm = NetworkMonitor(this@TestNetworkStackService, cb, + this.network, + mock(IpConnectivityLog::class.java), mock(SharedLog::class.java), + mock(NetworkStackService.NetworkStackServiceManager::class.java), + NetworkMonitorDeps(privateDnsBypassNetwork), + mock(TcpSocketTracker::class.java)) + cb.onNetworkMonitorCreated(NetworkMonitorConnector(nm, TestPermissionChecker())) + } + } +} diff --git a/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt b/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..165fd3728281f84e91993fe24e568dae463b96e2 --- /dev/null +++ b/tests/integration/util/com/android/server/ConnectivityServiceTestUtils.kt @@ -0,0 +1,43 @@ +/* + * 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 + */ + +@file:JvmName("ConnectivityServiceTestUtils") + +package com.android.server + +import android.net.ConnectivityManager.TYPE_BLUETOOTH +import android.net.ConnectivityManager.TYPE_ETHERNET +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_NONE +import android.net.ConnectivityManager.TYPE_TEST +import android.net.ConnectivityManager.TYPE_VPN +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkCapabilities.TRANSPORT_VPN +import android.net.NetworkCapabilities.TRANSPORT_WIFI + +fun transportToLegacyType(transport: Int) = when (transport) { + TRANSPORT_BLUETOOTH -> TYPE_BLUETOOTH + TRANSPORT_CELLULAR -> TYPE_MOBILE + TRANSPORT_ETHERNET -> TYPE_ETHERNET + TRANSPORT_TEST -> TYPE_TEST + TRANSPORT_VPN -> TYPE_VPN + TRANSPORT_WIFI -> TYPE_WIFI + else -> TYPE_NONE +} \ No newline at end of file diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..17db17923f4d827e265341c54e4c4c3b44eeca10 --- /dev/null +++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java @@ -0,0 +1,388 @@ +/* + * 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.server; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; + +import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType; + +import static junit.framework.Assert.assertTrue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkScore; +import android.net.NetworkSpecifier; +import android.net.QosFilter; +import android.net.SocketKeepalive; +import android.os.ConditionVariable; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; +import android.util.Range; + +import com.android.net.module.util.ArrayTrackRecord; +import com.android.testutils.HandlerUtils; +import com.android.testutils.TestableNetworkCallback; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork { + private final NetworkCapabilities mNetworkCapabilities; + private final HandlerThread mHandlerThread; + private final Context mContext; + private final String mLogTag; + private final NetworkAgentConfig mNetworkAgentConfig; + + private final ConditionVariable mDisconnected = new ConditionVariable(); + private final ConditionVariable mPreventReconnectReceived = new ConditionVariable(); + private final AtomicBoolean mConnected = new AtomicBoolean(false); + private NetworkScore mScore; + private NetworkAgent mNetworkAgent; + private int mStartKeepaliveError = SocketKeepalive.ERROR_UNSUPPORTED; + private int mStopKeepaliveError = SocketKeepalive.NO_KEEPALIVE; + // Controls how test network agent is going to wait before responding to keepalive + // start/stop. Useful when simulate KeepaliveTracker is waiting for response from modem. + private long mKeepaliveResponseDelay = 0L; + private Integer mExpectedKeepaliveSlot = null; + private final ArrayTrackRecord<CallbackType>.ReadHead mCallbackHistory = + new ArrayTrackRecord<CallbackType>().newReadHead(); + + public NetworkAgentWrapper(int transport, LinkProperties linkProperties, + NetworkCapabilities ncTemplate, Context context) throws Exception { + final int type = transportToLegacyType(transport); + final String typeName = ConnectivityManager.getNetworkTypeName(type); + mNetworkCapabilities = (ncTemplate != null) ? ncTemplate : new NetworkCapabilities(); + mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + mNetworkCapabilities.addTransportType(transport); + switch (transport) { + case TRANSPORT_ETHERNET: + mScore = new NetworkScore.Builder().setLegacyInt(70).build(); + break; + case TRANSPORT_WIFI: + mScore = new NetworkScore.Builder().setLegacyInt(60).build(); + break; + case TRANSPORT_CELLULAR: + mScore = new NetworkScore.Builder().setLegacyInt(50).build(); + break; + case TRANSPORT_WIFI_AWARE: + mScore = new NetworkScore.Builder().setLegacyInt(20).build(); + break; + case TRANSPORT_VPN: + mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_VPN); + // VPNs deduce the SUSPENDED capability from their underlying networks and there + // is no public API to let VPN services set it. + mNetworkCapabilities.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + mScore = new NetworkScore.Builder().setLegacyInt(101).build(); + break; + default: + throw new UnsupportedOperationException("unimplemented network type"); + } + mContext = context; + mLogTag = "Mock-" + typeName; + mHandlerThread = new HandlerThread(mLogTag); + mHandlerThread.start(); + + // extraInfo is set to "" by default in NetworkAgentConfig. + final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : ""; + mNetworkAgentConfig = new NetworkAgentConfig.Builder() + .setLegacyType(type) + .setLegacyTypeName(typeName) + .setLegacyExtraInfo(extraInfo) + .build(); + mNetworkAgent = makeNetworkAgent(linkProperties, mNetworkAgentConfig); + } + + protected InstrumentedNetworkAgent makeNetworkAgent(LinkProperties linkProperties, + final NetworkAgentConfig nac) throws Exception { + return new InstrumentedNetworkAgent(this, linkProperties, nac); + } + + public static class InstrumentedNetworkAgent extends NetworkAgent { + private final NetworkAgentWrapper mWrapper; + private static final String PROVIDER_NAME = "InstrumentedNetworkAgentProvider"; + + public InstrumentedNetworkAgent(NetworkAgentWrapper wrapper, LinkProperties lp, + NetworkAgentConfig nac) { + super(wrapper.mContext, wrapper.mHandlerThread.getLooper(), wrapper.mLogTag, + wrapper.mNetworkCapabilities, lp, wrapper.mScore, nac, + new NetworkProvider(wrapper.mContext, wrapper.mHandlerThread.getLooper(), + PROVIDER_NAME)); + mWrapper = wrapper; + register(); + } + + @Override + public void unwanted() { + mWrapper.mDisconnected.open(); + } + + @Override + public void startSocketKeepalive(Message msg) { + int slot = msg.arg1; + if (mWrapper.mExpectedKeepaliveSlot != null) { + assertEquals((int) mWrapper.mExpectedKeepaliveSlot, slot); + } + mWrapper.mHandlerThread.getThreadHandler().postDelayed( + () -> onSocketKeepaliveEvent(slot, mWrapper.mStartKeepaliveError), + mWrapper.mKeepaliveResponseDelay); + } + + @Override + public void stopSocketKeepalive(Message msg) { + final int slot = msg.arg1; + mWrapper.mHandlerThread.getThreadHandler().postDelayed( + () -> onSocketKeepaliveEvent(slot, mWrapper.mStopKeepaliveError), + mWrapper.mKeepaliveResponseDelay); + } + + @Override + public void onQosCallbackRegistered(final int qosCallbackId, + final @NonNull QosFilter filter) { + Log.i(mWrapper.mLogTag, "onQosCallbackRegistered"); + mWrapper.mCallbackHistory.add( + new CallbackType.OnQosCallbackRegister(qosCallbackId, filter)); + } + + @Override + public void onQosCallbackUnregistered(final int qosCallbackId) { + Log.i(mWrapper.mLogTag, "onQosCallbackUnregistered"); + mWrapper.mCallbackHistory.add(new CallbackType.OnQosCallbackUnregister(qosCallbackId)); + } + + @Override + protected void preventAutomaticReconnect() { + mWrapper.mPreventReconnectReceived.open(); + } + + @Override + protected void addKeepalivePacketFilter(Message msg) { + Log.i(mWrapper.mLogTag, "Add keepalive packet filter."); + } + + @Override + protected void removeKeepalivePacketFilter(Message msg) { + Log.i(mWrapper.mLogTag, "Remove keepalive packet filter."); + } + } + + public void setScore(@NonNull final NetworkScore score) { + mScore = score; + mNetworkAgent.sendNetworkScore(score); + } + + public void adjustScore(int change) { + final int newLegacyScore = mScore.getLegacyInt() + change; + final NetworkScore.Builder builder = new NetworkScore.Builder() + .setLegacyInt(newLegacyScore); + if (mNetworkCapabilities.hasTransport(TRANSPORT_WIFI) && newLegacyScore < 50) { + builder.setExiting(true); + } + mScore = builder.build(); + mNetworkAgent.sendNetworkScore(mScore); + } + + public NetworkScore getScore() { + return mScore; + } + + public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) { + mNetworkAgent.explicitlySelected(explicitlySelected, acceptUnvalidated); + } + + public void addCapability(int capability) { + mNetworkCapabilities.addCapability(capability); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void removeCapability(int capability) { + mNetworkCapabilities.removeCapability(capability); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setUids(Set<Range<Integer>> uids) { + mNetworkCapabilities.setUids(uids); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setSignalStrength(int signalStrength) { + mNetworkCapabilities.setSignalStrength(signalStrength); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setNetworkSpecifier(NetworkSpecifier networkSpecifier) { + mNetworkCapabilities.setNetworkSpecifier(networkSpecifier); + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + + public void setNetworkCapabilities(NetworkCapabilities nc, boolean sendToConnectivityService) { + mNetworkCapabilities.set(nc); + if (sendToConnectivityService) { + mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities); + } + } + + public void connect() { + if (!mConnected.compareAndSet(false /* expect */, true /* update */)) { + // compareAndSet returns false when the value couldn't be updated because it did not + // match the expected value. + fail("Test NetworkAgents can only be connected once"); + } + mNetworkAgent.markConnected(); + } + + public void suspend() { + removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + } + + public void resume() { + addCapability(NET_CAPABILITY_NOT_SUSPENDED); + } + + public void disconnect() { + mNetworkAgent.unregister(); + } + + @Override + public Network getNetwork() { + return mNetworkAgent.getNetwork(); + } + + public void expectPreventReconnectReceived(long timeoutMs) { + assertTrue(mPreventReconnectReceived.block(timeoutMs)); + } + + public void expectDisconnected(long timeoutMs) { + assertTrue(mDisconnected.block(timeoutMs)); + } + + public void sendLinkProperties(LinkProperties lp) { + mNetworkAgent.sendLinkProperties(lp); + } + + public void setStartKeepaliveEvent(int reason) { + mStartKeepaliveError = reason; + } + + public void setStopKeepaliveEvent(int reason) { + mStopKeepaliveError = reason; + } + + public void setKeepaliveResponseDelay(long delay) { + mKeepaliveResponseDelay = delay; + } + + public void setExpectedKeepaliveSlot(Integer slot) { + mExpectedKeepaliveSlot = slot; + } + + public NetworkAgent getNetworkAgent() { + return mNetworkAgent; + } + + public NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities; + } + + public int getLegacyType() { + return mNetworkAgentConfig.getLegacyType(); + } + + public String getExtraInfo() { + return mNetworkAgentConfig.getLegacyExtraInfo(); + } + + public @NonNull ArrayTrackRecord<CallbackType>.ReadHead getCallbackHistory() { + return mCallbackHistory; + } + + public void waitForIdle(long timeoutMs) { + HandlerUtils.waitForIdle(mHandlerThread, timeoutMs); + } + + abstract static class CallbackType { + final int mQosCallbackId; + + protected CallbackType(final int qosCallbackId) { + mQosCallbackId = qosCallbackId; + } + + static class OnQosCallbackRegister extends CallbackType { + final QosFilter mFilter; + OnQosCallbackRegister(final int qosCallbackId, final QosFilter filter) { + super(qosCallbackId); + mFilter = filter; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OnQosCallbackRegister that = (OnQosCallbackRegister) o; + return mQosCallbackId == that.mQosCallbackId + && Objects.equals(mFilter, that.mFilter); + } + + @Override + public int hashCode() { + return Objects.hash(mQosCallbackId, mFilter); + } + } + + static class OnQosCallbackUnregister extends CallbackType { + OnQosCallbackUnregister(final int qosCallbackId) { + super(qosCallbackId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OnQosCallbackUnregister that = (OnQosCallbackUnregister) o; + return mQosCallbackId == that.mQosCallbackId; + } + + @Override + public int hashCode() { + return Objects.hash(mQosCallbackId); + } + } + } + + public boolean isBypassableVpn() { + return mNetworkAgentConfig.isBypassableVpn(); + } +} diff --git a/tests/integration/util/com/android/server/TestNetIdManager.kt b/tests/integration/util/com/android/server/TestNetIdManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..938a694e8ba98e64bb63b874db9784153c8fa933 --- /dev/null +++ b/tests/integration/util/com/android/server/TestNetIdManager.kt @@ -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 com.android.server + +import java.util.concurrent.atomic.AtomicInteger + +/** + * A [NetIdManager] that generates ID starting from [NetIdManager.MAX_NET_ID] and decreasing, rather + * than starting from [NetIdManager.MIN_NET_ID] and increasing. + * + * Useful for testing ConnectivityService, to minimize the risk of test ConnectivityService netIDs + * overlapping with netIDs used by the real ConnectivityService on the device. + * + * IDs may still overlap if many networks have been used on the device (so the "real" netIDs + * are close to MAX_NET_ID), but this is typically not the case when running unit tests. Also, there + * is no way to fully solve the overlap issue without altering ID allocation in non-test code, as + * the real ConnectivityService could start using netIds that have been used by the test in the + * past. + */ +class TestNetIdManager : NetIdManager() { + private val nextId = AtomicInteger(MAX_NET_ID) + override fun reserveNetId() = nextId.decrementAndGet() + override fun releaseNetId(id: Int) = Unit + fun peekNextNetId() = nextId.get() - 1 +} diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..801154029319742b572a9eb025dfec6331cbd3e6 --- /dev/null +++ b/tests/smoketest/Android.bp @@ -0,0 +1,27 @@ +// This test exists only because the jni_libs list for these tests is difficult to +// maintain: the test itself only depends on libnetworkstatsfactorytestjni, but the test +// fails to load that library unless *all* the dependencies of that library are explicitly +// listed in jni_libs. This means that whenever any of the dependencies changes the test +// starts failing and breaking presubmits in frameworks/base. We cannot easily put +// FrameworksNetTests into global presubmit because they are at times flaky, but this +// test is effectively empty beyond validating that the libraries load correctly, and +// thus should be stable enough to put in global presubmit. +// +// TODO: remove this hack when there is a better solution for jni_libs that includes +// dependent libraries. +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "FrameworksNetSmokeTests", + defaults: ["FrameworksNetTests-jni-defaults"], + srcs: ["java/SmokeTest.java"], + test_suites: ["device-tests"], + static_libs: [ + "androidx.test.rules", + "mockito-target-minus-junit4", + "services.core", + ], +} diff --git a/tests/smoketest/AndroidManifest.xml b/tests/smoketest/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1b9febb9f578d8309c013c863025d53cc6a3d55 --- /dev/null +++ b/tests/smoketest/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?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.frameworks.tests.net.smoketest"> + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.tests.net.smoketest" + android:label="Frameworks Networking Smoke Tests" /> +</manifest> diff --git a/tests/smoketest/AndroidTest.xml b/tests/smoketest/AndroidTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac366e4ac544f6e2c39c793f4be27decf51b7342 --- /dev/null +++ b/tests/smoketest/AndroidTest.xml @@ -0,0 +1,28 @@ +<?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="Runs Frameworks Networking Smoke Tests."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="FrameworksNetSmokeTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="FrameworksNetSmokeTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.tests.net.smoketest" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/smoketest/java/SmokeTest.java b/tests/smoketest/java/SmokeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7d6655fde15e0800bb82ac12edaee0218f071505 --- /dev/null +++ b/tests/smoketest/java/SmokeTest.java @@ -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 com.android.server.net; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class SmokeTest { + + @Test + public void testLoadJni() { + System.loadLibrary("networkstatsfactorytestjni"); + assertEquals(0, 0x00); + } +} diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..beae0cf7a35a4dfa70f87c932ad48836c509b25b --- /dev/null +++ b/tests/unit/Android.bp @@ -0,0 +1,89 @@ +//######################################################################## +// Build FrameworksNetTests package +//######################################################################## +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "Android-Apache-2.0" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "FrameworksNetTests-jni-defaults", + jni_libs: [ + "ld-android", + "libbacktrace", + "libbase", + "libbinder", + "libbpf", + "libbpf_android", + "libc++", + "libcgrouprc", + "libcrypto", + "libcutils", + "libdl_android", + "libhidl-gen-utils", + "libhidlbase", + "libjsoncpp", + "liblog", + "liblzma", + "libnativehelper", + "libnetdbpf", + "libnetdutils", + "libnetworkstatsfactorytestjni", + "libpackagelistparser", + "libpcre2", + "libprocessgroup", + "libselinux", + "libtinyxml2", + "libui", + "libunwindstack", + "libutils", + "libutilscallstack", + "libvndksupport", + "libziparchive", + "libz", + "netd_aidl_interface-V5-cpp", + ], +} + +android_test { + name: "FrameworksNetTests", + defaults: [ + "framework-connectivity-test-defaults", + "FrameworksNetTests-jni-defaults", + ], + srcs: [ + "java/**/*.java", + "java/**/*.kt", + ], + test_suites: ["device-tests"], + certificate: "platform", + jarjar_rules: "jarjar-rules.txt", + static_libs: [ + "androidx.test.rules", + "bouncycastle-repackaged-unbundled", + "FrameworksNetCommonTests", + "frameworks-base-testutils", + "frameworks-net-integration-testutils", + "framework-protos", + "mockito-target-minus-junit4", + "net-tests-utils", + "platform-test-annotations", + "service-connectivity-pre-jarjar", + "services.core", + "services.net", + ], + libs: [ + "android.net.ipsec.ike.stubs.module_lib", + "android.test.runner", + "android.test.base", + "android.test.mock", + "ServiceConnectivityResources", + ], + jni_libs: [ + "libservice-connectivity", + ], +} diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..4c60ccf60615687df5bd409fc37a1820229e004d --- /dev/null +++ b/tests/unit/AndroidManifest.xml @@ -0,0 +1,62 @@ +<?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.frameworks.tests.net"> + + <uses-permission android:name="android.permission.READ_LOGS" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.BROADCAST_STICKY" /> + <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> + <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" /> + <uses-permission android:name="android.permission.MANAGE_APP_TOKENS" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + <uses-permission android:name="android.permission.REAL_GET_TASKS" /> + <uses-permission android:name="android.permission.GET_DETAILED_TASKS" /> + <uses-permission android:name="android.permission.MANAGE_NETWORK_POLICY" /> + <uses-permission android:name="android.permission.READ_NETWORK_USAGE_HISTORY" /> + <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.MANAGE_USERS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> + <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" /> + <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /> + <uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" /> + <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" /> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> + <uses-permission android:name="android.permission.NETWORK_STACK" /> + <uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY" /> + <uses-permission android:name="android.permission.NETWORK_FACTORY" /> + <uses-permission android:name="android.permission.NETWORK_STATS_PROVIDER" /> + <uses-permission android:name="android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE" /> + + <application> + <uses-library android:name="android.test.runner" /> + <uses-library android:name="android.net.ipsec.ike" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.tests.net" + android:label="Frameworks Networking Tests" /> +</manifest> diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..939ae493b280ab48ba906992ab41cd93b7aefdc5 --- /dev/null +++ b/tests/unit/AndroidTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Runs Frameworks Networking Tests."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="FrameworksNetTests.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="FrameworksNetTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.tests.net" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/unit/jarjar-rules.txt b/tests/unit/jarjar-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca8867206dda10029dadabb03d5b8d895345aab9 --- /dev/null +++ b/tests/unit/jarjar-rules.txt @@ -0,0 +1,2 @@ +# Module library in frameworks/libs/net +rule com.android.net.module.util.** android.net.frameworktests.util.@1 diff --git a/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..899295a019d2a094584c9828ca964ac58a01afcf --- /dev/null +++ b/tests/unit/java/android/app/usage/NetworkStatsManagerTest.java @@ -0,0 +1,212 @@ +/* + * 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.app.usage; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.net.ConnectivityManager; +import android.net.INetworkStatsService; +import android.net.INetworkStatsSession; +import android.net.NetworkStats.Entry; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.RemoteException; + +import androidx.test.InstrumentationRegistry; +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 org.mockito.invocation.InvocationOnMock; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsManagerTest { + + private @Mock INetworkStatsService mService; + private @Mock INetworkStatsSession mStatsSession; + + private NetworkStatsManager mManager; + + // TODO: change to NetworkTemplate.MATCH_MOBILE once internal constant rename is merged to aosp. + private static final int MATCH_MOBILE_ALL = 1; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mManager = new NetworkStatsManager(InstrumentationRegistry.getContext(), mService); + } + + @Test + public void testQueryDetails() throws RemoteException { + final String subscriberId = "subid"; + final long startTime = 1; + final long endTime = 100; + final int uid1 = 10001; + final int uid2 = 10002; + final int uid3 = 10003; + + Entry uid1Entry1 = new Entry("if1", uid1, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 100, 10, 200, 20, 0); + + Entry uid1Entry2 = new Entry( + "if2", uid1, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 100, 10, 200, 20, 0); + + Entry uid2Entry1 = new Entry("if1", uid2, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 150, 10, 250, 20, 0); + + Entry uid2Entry2 = new Entry( + "if2", uid2, + android.net.NetworkStats.SET_DEFAULT, android.net.NetworkStats.TAG_NONE, + 150, 10, 250, 20, 0); + + NetworkStatsHistory history1 = new NetworkStatsHistory(10, 2); + history1.recordData(10, 20, uid1Entry1); + history1.recordData(20, 30, uid1Entry2); + + NetworkStatsHistory history2 = new NetworkStatsHistory(10, 2); + history1.recordData(30, 40, uid2Entry1); + history1.recordData(35, 45, uid2Entry2); + + + when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession); + when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2, uid3 }); + + when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class), + eq(uid1), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime))) + .then((InvocationOnMock inv) -> { + NetworkTemplate template = inv.getArgument(0); + assertEquals(MATCH_MOBILE_ALL, template.getMatchRule()); + assertEquals(subscriberId, template.getSubscriberId()); + return history1; + }); + + when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class), + eq(uid2), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime))) + .then((InvocationOnMock inv) -> { + NetworkTemplate template = inv.getArgument(0); + assertEquals(MATCH_MOBILE_ALL, template.getMatchRule()); + assertEquals(subscriberId, template.getSubscriberId()); + return history2; + }); + + + NetworkStats stats = mManager.queryDetails( + ConnectivityManager.TYPE_MOBILE, subscriberId, startTime, endTime); + + NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + + // First 2 buckets exactly match entry timings + assertTrue(stats.getNextBucket(bucket)); + assertEquals(10, bucket.getStartTimeStamp()); + assertEquals(20, bucket.getEndTimeStamp()); + assertBucketMatches(uid1Entry1, bucket); + + assertTrue(stats.getNextBucket(bucket)); + assertEquals(20, bucket.getStartTimeStamp()); + assertEquals(30, bucket.getEndTimeStamp()); + assertBucketMatches(uid1Entry2, bucket); + + // 30 -> 40: contains uid2Entry1 and half of uid2Entry2 + assertTrue(stats.getNextBucket(bucket)); + assertEquals(30, bucket.getStartTimeStamp()); + assertEquals(40, bucket.getEndTimeStamp()); + assertEquals(225, bucket.getRxBytes()); + assertEquals(15, bucket.getRxPackets()); + assertEquals(375, bucket.getTxBytes()); + assertEquals(30, bucket.getTxPackets()); + + // 40 -> 50: contains half of uid2Entry2 + assertTrue(stats.getNextBucket(bucket)); + assertEquals(40, bucket.getStartTimeStamp()); + assertEquals(50, bucket.getEndTimeStamp()); + assertEquals(75, bucket.getRxBytes()); + assertEquals(5, bucket.getRxPackets()); + assertEquals(125, bucket.getTxBytes()); + assertEquals(10, bucket.getTxPackets()); + + assertFalse(stats.hasNextBucket()); + } + + @Test + public void testQueryDetails_NoSubscriberId() throws RemoteException { + final long startTime = 1; + final long endTime = 100; + final int uid1 = 10001; + final int uid2 = 10002; + + when(mService.openSessionForUsageStats(anyInt(), anyString())).thenReturn(mStatsSession); + when(mStatsSession.getRelevantUids()).thenReturn(new int[] { uid1, uid2 }); + + NetworkStats stats = mManager.queryDetails( + ConnectivityManager.TYPE_MOBILE, null, startTime, endTime); + + when(mStatsSession.getHistoryIntervalForUid(any(NetworkTemplate.class), + anyInt(), anyInt(), anyInt(), anyInt(), anyLong(), anyLong())) + .thenReturn(new NetworkStatsHistory(10, 0)); + + verify(mStatsSession, times(1)).getHistoryIntervalForUid( + argThat((NetworkTemplate t) -> + // No subscriberId: MATCH_MOBILE_WILDCARD template + t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD), + eq(uid1), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)); + + verify(mStatsSession, times(1)).getHistoryIntervalForUid( + argThat((NetworkTemplate t) -> + // No subscriberId: MATCH_MOBILE_WILDCARD template + t.getMatchRule() == NetworkTemplate.MATCH_MOBILE_WILDCARD), + eq(uid2), eq(android.net.NetworkStats.SET_ALL), + eq(android.net.NetworkStats.TAG_NONE), + eq(NetworkStatsHistory.FIELD_ALL), eq(startTime), eq(endTime)); + + assertFalse(stats.hasNextBucket()); + } + + private void assertBucketMatches(Entry expected, NetworkStats.Bucket actual) { + assertEquals(expected.uid, actual.getUid()); + assertEquals(expected.rxBytes, actual.getRxBytes()); + assertEquals(expected.rxPackets, actual.getRxPackets()); + assertEquals(expected.txBytes, actual.getTxBytes()); + assertEquals(expected.txPackets, actual.getTxPackets()); + } +} diff --git a/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java b/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..06e9405a6a795a4d43a73bd3543487e99595bc19 --- /dev/null +++ b/tests/unit/java/android/net/ConnectivityDiagnosticsManagerTest.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsBinder; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.content.Context; +import android.os.PersistableBundle; + +import androidx.test.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; + +import java.util.concurrent.Executor; + +@RunWith(JUnit4.class) +public class ConnectivityDiagnosticsManagerTest { + private static final int NET_ID = 1; + private static final int DETECTION_METHOD = 2; + private static final long TIMESTAMP = 10L; + private static final String INTERFACE_NAME = "interface"; + private static final String BUNDLE_KEY = "key"; + private static final String BUNDLE_VALUE = "value"; + + private static final Executor INLINE_EXECUTOR = x -> x.run(); + + @Mock private IConnectivityManager mService; + @Mock private ConnectivityDiagnosticsCallback mCb; + + private Context mContext; + private ConnectivityDiagnosticsBinder mBinder; + private ConnectivityDiagnosticsManager mManager; + + private String mPackageName; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getContext(); + + mService = mock(IConnectivityManager.class); + mCb = mock(ConnectivityDiagnosticsCallback.class); + + mBinder = new ConnectivityDiagnosticsBinder(mCb, INLINE_EXECUTOR); + mManager = new ConnectivityDiagnosticsManager(mContext, mService); + + mPackageName = mContext.getOpPackageName(); + } + + @After + public void tearDown() { + // clear ConnectivityDiagnosticsManager callbacks map + ConnectivityDiagnosticsManager.sCallbacks.clear(); + } + + private ConnectivityReport createSampleConnectivityReport() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(INTERFACE_NAME); + + final NetworkCapabilities networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS); + + final PersistableBundle bundle = new PersistableBundle(); + bundle.putString(BUNDLE_KEY, BUNDLE_VALUE); + + return new ConnectivityReport( + new Network(NET_ID), TIMESTAMP, linkProperties, networkCapabilities, bundle); + } + + private ConnectivityReport createDefaultConnectivityReport() { + return new ConnectivityReport( + new Network(0), + 0L, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY); + } + + @Test + public void testPersistableBundleEquals() { + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals( + null, PersistableBundle.EMPTY)); + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals( + PersistableBundle.EMPTY, null)); + assertTrue( + ConnectivityDiagnosticsManager.persistableBundleEquals( + PersistableBundle.EMPTY, PersistableBundle.EMPTY)); + + final PersistableBundle a = new PersistableBundle(); + a.putString(BUNDLE_KEY, BUNDLE_VALUE); + + final PersistableBundle b = new PersistableBundle(); + b.putString(BUNDLE_KEY, BUNDLE_VALUE); + + final PersistableBundle c = new PersistableBundle(); + c.putString(BUNDLE_KEY, null); + + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals(PersistableBundle.EMPTY, a)); + assertFalse( + ConnectivityDiagnosticsManager.persistableBundleEquals(a, PersistableBundle.EMPTY)); + + assertTrue(ConnectivityDiagnosticsManager.persistableBundleEquals(a, b)); + assertTrue(ConnectivityDiagnosticsManager.persistableBundleEquals(b, a)); + + assertFalse(ConnectivityDiagnosticsManager.persistableBundleEquals(a, c)); + assertFalse(ConnectivityDiagnosticsManager.persistableBundleEquals(c, a)); + } + + @Test + public void testConnectivityReportEquals() { + final ConnectivityReport defaultReport = createDefaultConnectivityReport(); + final ConnectivityReport sampleReport = createSampleConnectivityReport(); + assertEquals(sampleReport, createSampleConnectivityReport()); + assertEquals(defaultReport, createDefaultConnectivityReport()); + + final LinkProperties linkProperties = sampleReport.getLinkProperties(); + final NetworkCapabilities networkCapabilities = sampleReport.getNetworkCapabilities(); + final PersistableBundle bundle = sampleReport.getAdditionalInfo(); + + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(NET_ID), + 0L, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + TIMESTAMP, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + 0L, + linkProperties, + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + TIMESTAMP, + new LinkProperties(), + networkCapabilities, + PersistableBundle.EMPTY)); + assertNotEquals( + createDefaultConnectivityReport(), + new ConnectivityReport( + new Network(0), + TIMESTAMP, + new LinkProperties(), + new NetworkCapabilities(), + bundle)); + } + + @Test + public void testConnectivityReportParcelUnparcel() { + assertParcelSane(createSampleConnectivityReport(), 5); + } + + private DataStallReport createSampleDataStallReport() { + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(INTERFACE_NAME); + + final PersistableBundle bundle = new PersistableBundle(); + bundle.putString(BUNDLE_KEY, BUNDLE_VALUE); + + final NetworkCapabilities networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_IMS); + + return new DataStallReport( + new Network(NET_ID), + TIMESTAMP, + DETECTION_METHOD, + linkProperties, + networkCapabilities, + bundle); + } + + private DataStallReport createDefaultDataStallReport() { + return new DataStallReport( + new Network(0), + 0L, + 0, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY); + } + + @Test + public void testDataStallReportEquals() { + final DataStallReport defaultReport = createDefaultDataStallReport(); + final DataStallReport sampleReport = createSampleDataStallReport(); + assertEquals(sampleReport, createSampleDataStallReport()); + assertEquals(defaultReport, createDefaultDataStallReport()); + + final LinkProperties linkProperties = sampleReport.getLinkProperties(); + final NetworkCapabilities networkCapabilities = sampleReport.getNetworkCapabilities(); + final PersistableBundle bundle = sampleReport.getStallDetails(); + + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(NET_ID), + 0L, + 0, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + TIMESTAMP, + 0, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + DETECTION_METHOD, + new LinkProperties(), + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + 0, + linkProperties, + new NetworkCapabilities(), + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + 0, + new LinkProperties(), + networkCapabilities, + PersistableBundle.EMPTY)); + assertNotEquals( + defaultReport, + new DataStallReport( + new Network(0), + 0L, + 0, + new LinkProperties(), + new NetworkCapabilities(), + bundle)); + } + + @Test + public void testDataStallReportParcelUnparcel() { + assertParcelSane(createSampleDataStallReport(), 6); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable() { + mBinder.onConnectivityReportAvailable(createSampleConnectivityReport()); + + // The callback will be invoked synchronously by inline executor. Immediately check the + // latch without waiting. + verify(mCb).onConnectivityReportAvailable(eq(createSampleConnectivityReport())); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() { + mBinder.onDataStallSuspected(createSampleDataStallReport()); + + // The callback will be invoked synchronously by inline executor. Immediately check the + // latch without waiting. + verify(mCb).onDataStallSuspected(eq(createSampleDataStallReport())); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnNetworkConnectivityReported() { + final Network n = new Network(NET_ID); + final boolean connectivity = true; + + mBinder.onNetworkConnectivityReported(n, connectivity); + + // The callback will be invoked synchronously by inline executor. Immediately check the + // latch without waiting. + verify(mCb).onNetworkConnectivityReported(eq(n), eq(connectivity)); + } + + @Test + public void testRegisterConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + + verify(mService).registerConnectivityDiagnosticsCallback( + any(ConnectivityDiagnosticsBinder.class), eq(request), eq(mPackageName)); + assertTrue(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb)); + } + + @Test + public void testRegisterDuplicateConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + + try { + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + fail("Duplicate callback registration should fail"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testUnregisterConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + + mManager.unregisterConnectivityDiagnosticsCallback(mCb); + + verify(mService).unregisterConnectivityDiagnosticsCallback( + any(ConnectivityDiagnosticsBinder.class)); + assertFalse(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb)); + + // verify that re-registering is successful + mManager.registerConnectivityDiagnosticsCallback(request, INLINE_EXECUTOR, mCb); + verify(mService, times(2)).registerConnectivityDiagnosticsCallback( + any(ConnectivityDiagnosticsBinder.class), eq(request), eq(mPackageName)); + assertTrue(ConnectivityDiagnosticsManager.sCallbacks.containsKey(mCb)); + } + + @Test + public void testUnregisterUnknownConnectivityDiagnosticsCallback() throws Exception { + mManager.unregisterConnectivityDiagnosticsCallback(mCb); + + verifyNoMoreInteractions(mService); + } +} diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c804e1003887288083aaad6ec336b6118fdda9da --- /dev/null +++ b/tests/unit/java/android/net/ConnectivityManagerTest.java @@ -0,0 +1,438 @@ +/* + * 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; + +import static android.net.ConnectivityManager.TYPE_NONE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST; +import static android.net.NetworkRequest.Type.REQUEST; +import static android.net.NetworkRequest.Type.TRACK_DEFAULT; +import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.net.ConnectivityManager.NetworkCallback; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Process; + +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; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityManagerTest { + + @Mock Context mCtx; + @Mock IConnectivityManager mService; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + static NetworkCapabilities verifyNetworkCapabilities( + int legacyType, int transportType, int... capabilities) { + final NetworkCapabilities nc = ConnectivityManager.networkCapabilitiesForType(legacyType); + assertNotNull(nc); + assertTrue(nc.hasTransport(transportType)); + for (int capability : capabilities) { + assertTrue(nc.hasCapability(capability)); + } + + return nc; + } + + static void verifyUnrestrictedNetworkCapabilities(int legacyType, int transportType) { + verifyNetworkCapabilities( + legacyType, + transportType, + NET_CAPABILITY_INTERNET, + NET_CAPABILITY_NOT_RESTRICTED, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + } + + static void verifyRestrictedMobileNetworkCapabilities(int legacyType, int capability) { + final NetworkCapabilities nc = verifyNetworkCapabilities( + legacyType, + TRANSPORT_CELLULAR, + capability, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + + @Test + public void testNetworkCapabilitiesForTypeMobile() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE, TRANSPORT_CELLULAR); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileCbs() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_CBS, NET_CAPABILITY_CBS); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileDun() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_DUN, NET_CAPABILITY_DUN); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileFota() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_FOTA, NET_CAPABILITY_FOTA); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileHipri() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_HIPRI, TRANSPORT_CELLULAR); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileIms() { + verifyRestrictedMobileNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_IMS, NET_CAPABILITY_IMS); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileMms() { + final NetworkCapabilities nc = verifyNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_MMS, + TRANSPORT_CELLULAR, + NET_CAPABILITY_MMS, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + } + + @Test + public void testNetworkCapabilitiesForTypeMobileSupl() { + final NetworkCapabilities nc = verifyNetworkCapabilities( + ConnectivityManager.TYPE_MOBILE_SUPL, + TRANSPORT_CELLULAR, + NET_CAPABILITY_SUPL, + NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + } + + @Test + public void testNetworkCapabilitiesForTypeWifi() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_WIFI, TRANSPORT_WIFI); + } + + @Test + public void testNetworkCapabilitiesForTypeWifiP2p() { + final NetworkCapabilities nc = verifyNetworkCapabilities( + ConnectivityManager.TYPE_WIFI_P2P, + TRANSPORT_WIFI, + NET_CAPABILITY_NOT_RESTRICTED, NET_CAPABILITY_NOT_VPN, + NET_CAPABILITY_TRUSTED, NET_CAPABILITY_WIFI_P2P); + + assertFalse(nc.hasCapability(NET_CAPABILITY_INTERNET)); + } + + @Test + public void testNetworkCapabilitiesForTypeBluetooth() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_BLUETOOTH, TRANSPORT_BLUETOOTH); + } + + @Test + public void testNetworkCapabilitiesForTypeEthernet() { + verifyUnrestrictedNetworkCapabilities( + ConnectivityManager.TYPE_ETHERNET, TRANSPORT_ETHERNET); + } + + @Test + public void testCallbackRelease() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + NetworkRequest request = makeRequest(1); + NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class); + Handler handler = new Handler(Looper.getMainLooper()); + ArgumentCaptor<Messenger> captor = ArgumentCaptor.forClass(Messenger.class); + + // register callback + when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(), + anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(request); + manager.requestNetwork(request, callback, handler); + + // callback triggers + captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_AVAILABLE)); + verify(callback, timeout(500).times(1)).onAvailable(any(Network.class), + any(NetworkCapabilities.class), any(LinkProperties.class), anyBoolean()); + + // unregister callback + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(request); + + // callback does not trigger anymore. + captor.getValue().send(makeMessage(request, ConnectivityManager.CALLBACK_LOSING)); + verify(callback, timeout(500).times(0)).onLosing(any(), anyInt()); + } + + @Test + public void testCallbackRecycling() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + NetworkRequest req1 = makeRequest(1); + NetworkRequest req2 = makeRequest(2); + NetworkCallback callback = mock(ConnectivityManager.NetworkCallback.class); + Handler handler = new Handler(Looper.getMainLooper()); + ArgumentCaptor<Messenger> captor = ArgumentCaptor.forClass(Messenger.class); + + // register callback + when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(), + anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req1); + manager.requestNetwork(req1, callback, handler); + + // callback triggers + captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_AVAILABLE)); + verify(callback, timeout(100).times(1)).onAvailable(any(Network.class), + any(NetworkCapabilities.class), any(LinkProperties.class), anyBoolean()); + + // unregister callback + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(req1); + + // callback does not trigger anymore. + captor.getValue().send(makeMessage(req1, ConnectivityManager.CALLBACK_LOSING)); + verify(callback, timeout(100).times(0)).onLosing(any(), anyInt()); + + // callback can be registered again + when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(), + anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req2); + manager.requestNetwork(req2, callback, handler); + + // callback triggers + captor.getValue().send(makeMessage(req2, ConnectivityManager.CALLBACK_LOST)); + verify(callback, timeout(100).times(1)).onLost(any()); + + // unregister callback + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(req2); + } + + // TODO: turn on this test when request callback 1:1 mapping is enforced + //@Test + private void noDoubleCallbackRegistration() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + NetworkRequest request = makeRequest(1); + NetworkCallback callback = new ConnectivityManager.NetworkCallback(); + ApplicationInfo info = new ApplicationInfo(); + // TODO: update version when starting to enforce 1:1 mapping + info.targetSdkVersion = VERSION_CODES.N_MR1 + 1; + + when(mCtx.getApplicationInfo()).thenReturn(info); + when(mService.requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), + anyInt(), any(), nullable(String.class))).thenReturn(request); + + Handler handler = new Handler(Looper.getMainLooper()); + manager.requestNetwork(request, callback, handler); + + // callback is already registered, reregistration should fail. + Class<IllegalArgumentException> wantException = IllegalArgumentException.class; + expectThrowable(() -> manager.requestNetwork(request, callback), wantException); + + manager.unregisterNetworkCallback(callback); + verify(mService, times(1)).releaseNetworkRequest(request); + + // unregistering the callback should make it registrable again. + manager.requestNetwork(request, callback); + } + + @Test + public void testArgumentValidation() throws Exception { + ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + + NetworkRequest request = mock(NetworkRequest.class); + NetworkCallback callback = mock(NetworkCallback.class); + Handler handler = mock(Handler.class); + NetworkCallback nullCallback = null; + PendingIntent nullIntent = null; + + mustFail(() -> manager.requestNetwork(null, callback)); + mustFail(() -> manager.requestNetwork(request, nullCallback)); + mustFail(() -> manager.requestNetwork(request, callback, null)); + mustFail(() -> manager.requestNetwork(request, callback, -1)); + mustFail(() -> manager.requestNetwork(request, nullIntent)); + + mustFail(() -> manager.requestBackgroundNetwork(null, callback, handler)); + mustFail(() -> manager.requestBackgroundNetwork(request, null, handler)); + mustFail(() -> manager.requestBackgroundNetwork(request, callback, null)); + + mustFail(() -> manager.registerNetworkCallback(null, callback, handler)); + mustFail(() -> manager.registerNetworkCallback(request, null, handler)); + mustFail(() -> manager.registerNetworkCallback(request, callback, null)); + mustFail(() -> manager.registerNetworkCallback(request, nullIntent)); + + mustFail(() -> manager.registerDefaultNetworkCallback(null, handler)); + mustFail(() -> manager.registerDefaultNetworkCallback(callback, null)); + + mustFail(() -> manager.registerSystemDefaultNetworkCallback(null, handler)); + mustFail(() -> manager.registerSystemDefaultNetworkCallback(callback, null)); + + mustFail(() -> manager.registerBestMatchingNetworkCallback(null, callback, handler)); + mustFail(() -> manager.registerBestMatchingNetworkCallback(request, null, handler)); + mustFail(() -> manager.registerBestMatchingNetworkCallback(request, callback, null)); + + mustFail(() -> manager.unregisterNetworkCallback(nullCallback)); + mustFail(() -> manager.unregisterNetworkCallback(nullIntent)); + mustFail(() -> manager.releaseNetworkRequest(nullIntent)); + } + + static void mustFail(Runnable fn) { + try { + fn.run(); + fail(); + } catch (Exception expected) { + } + } + + @Test + public void testRequestType() throws Exception { + final String testPkgName = "MyPackage"; + final String testAttributionTag = "MyTag"; + final ConnectivityManager manager = new ConnectivityManager(mCtx, mService); + when(mCtx.getOpPackageName()).thenReturn(testPkgName); + when(mCtx.getAttributionTag()).thenReturn(testAttributionTag); + final NetworkRequest request = makeRequest(1); + final NetworkCallback callback = new ConnectivityManager.NetworkCallback(); + + manager.requestNetwork(request, callback); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities), + eq(REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + // Verify that register network callback does not calls requestNetwork at all. + manager.registerNetworkCallback(request, callback); + verify(mService, never()).requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), + anyInt(), anyInt(), any(), any()); + verify(mService).listenForNetwork(eq(request.networkCapabilities), any(), any(), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + + manager.registerDefaultNetworkCallback(callback); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null), + eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + manager.registerDefaultNetworkCallbackForUid(42, callback, handler); + verify(mService).requestNetwork(eq(42), eq(null), + eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + + manager.requestBackgroundNetwork(request, callback, handler); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities), + eq(BACKGROUND_REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + + manager.registerSystemDefaultNetworkCallback(callback, handler); + verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null), + eq(TRACK_SYSTEM_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(), + eq(testPkgName), eq(testAttributionTag)); + reset(mService); + } + + static Message makeMessage(NetworkRequest req, int messageType) { + Bundle bundle = new Bundle(); + bundle.putParcelable(NetworkRequest.class.getSimpleName(), req); + // Pass default objects as we don't care which get passed here + bundle.putParcelable(Network.class.getSimpleName(), new Network(1)); + bundle.putParcelable(NetworkCapabilities.class.getSimpleName(), new NetworkCapabilities()); + bundle.putParcelable(LinkProperties.class.getSimpleName(), new LinkProperties()); + Message msg = Message.obtain(); + msg.what = messageType; + msg.setData(bundle); + return msg; + } + + static NetworkRequest makeRequest(int requestId) { + NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + return new NetworkRequest(request.networkCapabilities, ConnectivityManager.TYPE_NONE, + requestId, NetworkRequest.Type.NONE); + } + + static void expectThrowable(Runnable block, Class<? extends Throwable> throwableType) { + try { + block.run(); + } catch (Throwable t) { + if (t.getClass().equals(throwableType)) { + return; + } + fail("expected exception of type " + throwableType + ", but was " + t.getClass()); + } + fail("expected exception of type " + throwableType); + } +} diff --git a/tests/unit/java/android/net/Ikev2VpnProfileTest.java b/tests/unit/java/android/net/Ikev2VpnProfileTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0707ef3ed1d63257a1a6d3a22f41f05c2f624c50 --- /dev/null +++ b/tests/unit/java/android/net/Ikev2VpnProfileTest.java @@ -0,0 +1,447 @@ +/* + * 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 org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.test.mock.MockContext; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.net.VpnProfile; +import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator; +import com.android.net.module.util.ProxyUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.security.auth.x500.X500Principal; + +/** Unit tests for {@link Ikev2VpnProfile.Builder}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class Ikev2VpnProfileTest { + private static final String SERVER_ADDR_STRING = "1.2.3.4"; + private static final String IDENTITY_STRING = "Identity"; + private static final String USERNAME_STRING = "username"; + private static final String PASSWORD_STRING = "pa55w0rd"; + private static final String EXCL_LIST = "exclList"; + private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); + private static final int TEST_MTU = 1300; + + private final MockContext mMockContext = + new MockContext() { + @Override + public String getOpPackageName() { + return "fooPackage"; + } + }; + private final ProxyInfo mProxy = ProxyInfo.buildDirectProxy( + SERVER_ADDR_STRING, -1, ProxyUtils.exclusionStringAsList(EXCL_LIST)); + + private X509Certificate mUserCert; + private X509Certificate mServerRootCa; + private PrivateKey mPrivateKey; + + @Before + public void setUp() throws Exception { + mServerRootCa = generateRandomCertAndKeyPair().cert; + + final CertificateAndKey userCertKey = generateRandomCertAndKeyPair(); + mUserCert = userCertKey.cert; + mPrivateKey = userCertKey.key; + } + + private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() { + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING); + + builder.setBypassable(true); + builder.setProxy(mProxy); + builder.setMaxMtu(TEST_MTU); + builder.setMetered(true); + + return builder; + } + + @Test + public void testBuildValidProfileWithOptions() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + // Check non-auth parameters correctly stored + assertEquals(SERVER_ADDR_STRING, profile.getServerAddr()); + assertEquals(IDENTITY_STRING, profile.getUserIdentity()); + assertEquals(mProxy, profile.getProxyInfo()); + assertTrue(profile.isBypassable()); + assertTrue(profile.isMetered()); + assertEquals(TEST_MTU, profile.getMaxMtu()); + assertEquals(Ikev2VpnProfile.DEFAULT_ALGORITHMS, profile.getAllowedAlgorithms()); + } + + @Test + public void testBuildUsernamePasswordProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertEquals(USERNAME_STRING, profile.getUsername()); + assertEquals(PASSWORD_STRING, profile.getPassword()); + assertEquals(mServerRootCa, profile.getServerRootCaCert()); + + assertNull(profile.getPresharedKey()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildDigitalSignatureProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertEquals(profile.getUserCert(), mUserCert); + assertEquals(mPrivateKey, profile.getRsaPrivateKey()); + assertEquals(profile.getServerRootCaCert(), mServerRootCa); + + assertNull(profile.getPresharedKey()); + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + } + + @Test + public void testBuildPresharedKeyProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final Ikev2VpnProfile profile = builder.build(); + assertNotNull(profile); + + assertArrayEquals(PSK_BYTES, profile.getPresharedKey()); + + assertNull(profile.getServerRootCaCert()); + assertNull(profile.getUsername()); + assertNull(profile.getPassword()); + assertNull(profile.getRsaPrivateKey()); + assertNull(profile.getUserCert()); + } + + @Test + public void testBuildWithAllowedAlgorithmsAead() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + + List<String> allowedAlgorithms = + Arrays.asList( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305); + builder.setAllowedAlgorithms(allowedAlgorithms); + + final Ikev2VpnProfile profile = builder.build(); + assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms()); + } + + @Test + public void testBuildWithAllowedAlgorithmsNormal() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + builder.setAuthPsk(PSK_BYTES); + + List<String> allowedAlgorithms = + Arrays.asList( + IpSecAlgorithm.AUTH_HMAC_SHA512, + IpSecAlgorithm.AUTH_AES_XCBC, + IpSecAlgorithm.AUTH_AES_CMAC, + IpSecAlgorithm.CRYPT_AES_CBC, + IpSecAlgorithm.CRYPT_AES_CTR); + builder.setAllowedAlgorithms(allowedAlgorithms); + + final Ikev2VpnProfile profile = builder.build(); + assertEquals(allowedAlgorithms, profile.getAllowedAlgorithms()); + } + + @Test + public void testSetAllowedAlgorithmsEmptyList() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setAllowedAlgorithms(new ArrayList<>()); + fail("Expected exception due to no valid algorithm set"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetAllowedAlgorithmsInvalidList() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + List<String> allowedAlgorithms = new ArrayList<>(); + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA256)); + fail("Expected exception due to missing encryption"); + } catch (IllegalArgumentException expected) { + } + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.CRYPT_AES_CBC)); + fail("Expected exception due to missing authentication"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetAllowedAlgorithmsInsecureAlgorithm() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + List<String> allowedAlgorithms = new ArrayList<>(); + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_MD5)); + fail("Expected exception due to insecure algorithm"); + } catch (IllegalArgumentException expected) { + } + + try { + builder.setAllowedAlgorithms(Arrays.asList(IpSecAlgorithm.AUTH_HMAC_SHA1)); + fail("Expected exception due to insecure algorithm"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildNoAuthMethodSet() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.build(); + fail("Expected exception due to lack of auth method"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildInvalidMtu() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + try { + builder.setMaxMtu(500); + fail("Expected exception due to too-small MTU"); + } catch (IllegalArgumentException expected) { + } + } + + private void verifyVpnProfileCommon(VpnProfile profile) { + assertEquals(SERVER_ADDR_STRING, profile.server); + assertEquals(IDENTITY_STRING, profile.ipsecIdentifier); + assertEquals(mProxy, profile.proxy); + assertTrue(profile.isBypassable); + assertTrue(profile.isMetered); + assertEquals(TEST_MTU, profile.maxMtu); + } + + @Test + public void testPskConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final VpnProfile profile = builder.build().toVpnProfile(); + + verifyVpnProfileCommon(profile); + assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret); + + // Check nothing else is set + assertEquals("", profile.username); + assertEquals("", profile.password); + assertEquals("", profile.ipsecUserCert); + assertEquals("", profile.ipsecCaCert); + } + + @Test + public void testUsernamePasswordConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + + verifyVpnProfileCommon(profile); + assertEquals(USERNAME_STRING, profile.username); + assertEquals(PASSWORD_STRING, profile.password); + assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); + + // Check nothing else is set + assertEquals("", profile.ipsecUserCert); + assertEquals("", profile.ipsecSecret); + } + + @Test + public void testRsaConvertToVpnProfile() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + + final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE + + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded()); + verifyVpnProfileCommon(profile); + assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert); + assertEquals( + expectedSecret, + profile.ipsecSecret); + assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); + + // Check nothing else is set + assertEquals("", profile.username); + assertEquals("", profile.password); + } + + @Test + public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.username = USERNAME_STRING; + profile.password = PASSWORD_STRING; + profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa); + profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert); + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getUsername()); + assertNull(result.getPassword()); + assertNull(result.getUserCert()); + assertNull(result.getRsaPrivateKey()); + assertNull(result.getServerRootCaCert()); + } + + @Test + public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.ipsecSecret = new String(PSK_BYTES); + profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert); + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getPresharedKey()); + assertNull(result.getUserCert()); + assertNull(result.getRsaPrivateKey()); + } + + @Test + public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final VpnProfile profile = builder.build().toVpnProfile(); + profile.username = USERNAME_STRING; + profile.password = PASSWORD_STRING; + + final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile); + assertNull(result.getUsername()); + assertNull(result.getPassword()); + assertNull(result.getPresharedKey()); + } + + @Test + public void testPskConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthPsk(PSK_BYTES); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testUsernamePasswordConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + @Test + public void testRsaConversionIsLossless() throws Exception { + final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions(); + + builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); + final Ikev2VpnProfile ikeProfile = builder.build(); + + assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile())); + } + + 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/unit/java/android/net/IpMemoryStoreTest.java b/tests/unit/java/android/net/IpMemoryStoreTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0b13800bc5c956ef96a2f0e46e142e4de2c00b77 --- /dev/null +++ b/tests/unit/java/android/net/IpMemoryStoreTest.java @@ -0,0 +1,332 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.ipmemorystore.Blob; +import android.net.ipmemorystore.IOnStatusListener; +import android.net.ipmemorystore.NetworkAttributes; +import android.net.ipmemorystore.NetworkAttributesParcelable; +import android.net.ipmemorystore.Status; +import android.net.networkstack.ModuleNetworkStackClient; +import android.os.RemoteException; + +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.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.UnknownHostException; +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpMemoryStoreTest { + private static final String TAG = IpMemoryStoreTest.class.getSimpleName(); + private static final String TEST_CLIENT_ID = "testClientId"; + private static final String TEST_DATA_NAME = "testData"; + private static final String TEST_OTHER_DATA_NAME = TEST_DATA_NAME + "Other"; + private static final byte[] TEST_BLOB_DATA = new byte[] { -3, 6, 8, -9, 12, + -128, 0, 89, 112, 91, -34 }; + private static final NetworkAttributes TEST_NETWORK_ATTRIBUTES = buildTestNetworkAttributes( + "hint", 219); + + @Mock + Context mMockContext; + @Mock + ModuleNetworkStackClient mModuleNetworkStackClient; + @Mock + IIpMemoryStore mMockService; + @Mock + IOnStatusListener mIOnStatusListener; + IpMemoryStore mStore; + + @Captor + ArgumentCaptor<IIpMemoryStoreCallbacks> mCbCaptor; + @Captor + ArgumentCaptor<NetworkAttributesParcelable> mNapCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + private void startIpMemoryStore(boolean supplyService) { + if (supplyService) { + doAnswer(invocation -> { + ((IIpMemoryStoreCallbacks) invocation.getArgument(0)) + .onIpMemoryStoreFetched(mMockService); + return null; + }).when(mModuleNetworkStackClient).fetchIpMemoryStore(any()); + } else { + doNothing().when(mModuleNetworkStackClient).fetchIpMemoryStore(mCbCaptor.capture()); + } + mStore = new IpMemoryStore(mMockContext) { + @Override + protected ModuleNetworkStackClient getModuleNetworkStackClient(Context ctx) { + return mModuleNetworkStackClient; + } + }; + } + + private static NetworkAttributes buildTestNetworkAttributes(String hint, int mtu) { + return new NetworkAttributes.Builder() + .setCluster(hint) + .setMtu(mtu) + .build(); + } + + @Test + public void testNetworkAttributes() throws Exception { + startIpMemoryStore(true /* supplyService */); + final String l2Key = "fakeKey"; + + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + verify(mMockService, times(1)).storeNetworkAttributes(eq(l2Key), + mNapCaptor.capture(), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(TEST_NETWORK_ATTRIBUTES, attr); + }); + + verify(mMockService, times(1)).retrieveNetworkAttributes(eq(l2Key), any()); + } + + @Test + public void testPrivateData() throws RemoteException { + startIpMemoryStore(true /* supplyService */); + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> { + assertTrue("Store not successful : " + status.resultCode, status.isSuccess()); + }); + verify(mMockService, times(1)).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + verify(mMockService, times(1)).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + } + + @Test + public void testFindL2Key() + throws UnknownHostException, RemoteException, Exception { + startIpMemoryStore(true /* supplyService */); + final String l2Key = "fakeKey"; + + mStore.findL2Key(TEST_NETWORK_ATTRIBUTES, + (status, key) -> { + assertTrue("Retrieve network sameness not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + }); + verify(mMockService, times(1)).findL2Key(mNapCaptor.capture(), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testIsSameNetwork() throws UnknownHostException, RemoteException { + startIpMemoryStore(true /* supplyService */); + final String l2Key1 = "fakeKey1"; + final String l2Key2 = "fakeKey2"; + + mStore.isSameNetwork(l2Key1, l2Key2, + (status, answer) -> { + assertFalse("Retrieve network sameness suspiciously successful : " + + status.resultCode, status.isSuccess()); + assertEquals(Status.ERROR_ILLEGAL_ARGUMENT, status.resultCode); + assertNull(answer); + }); + verify(mMockService, times(1)).isSameNetwork(eq(l2Key1), eq(l2Key2), any()); + } + + @Test + public void testEnqueuedIpMsRequests() throws Exception { + startIpMemoryStore(false /* supplyService */); + + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + // enqueue multiple ipms requests + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(TEST_NETWORK_ATTRIBUTES, attr); + }); + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + + // get ipms service ready + mCbCaptor.getValue().onIpMemoryStoreFetched(mMockService); + + InOrder inOrder = inOrder(mMockService); + + inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), any()); + inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any()); + inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testEnqueuedIpMsRequestsWithException() throws Exception { + startIpMemoryStore(true /* supplyService */); + doThrow(RemoteException.class).when(mMockService).retrieveNetworkAttributes(any(), any()); + + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + // enqueue multiple ipms requests + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + assertTrue("Retrieve network attributes not successful : " + + status.resultCode, status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(TEST_NETWORK_ATTRIBUTES, attr); + }); + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + + // verify the rest of the queue is still processed in order even if the remote exception + // occurs when calling one or more requests + InOrder inOrder = inOrder(mMockService); + + inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), any()); + inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testEnqueuedIpMsRequestsCallbackFunctionWithException() throws Exception { + startIpMemoryStore(true /* supplyService */); + + final Blob b = new Blob(); + b.data = TEST_BLOB_DATA; + final String l2Key = "fakeKey"; + + // enqueue multiple ipms requests + mStore.storeNetworkAttributes(l2Key, TEST_NETWORK_ATTRIBUTES, + status -> assertTrue("Store not successful : " + status.resultCode, + status.isSuccess())); + mStore.retrieveNetworkAttributes(l2Key, + (status, key, attr) -> { + throw new RuntimeException("retrieveNetworkAttributes test"); + }); + mStore.storeBlob(l2Key, TEST_CLIENT_ID, TEST_DATA_NAME, b, + status -> { + throw new RuntimeException("storeBlob test"); + }); + mStore.retrieveBlob(l2Key, TEST_CLIENT_ID, TEST_OTHER_DATA_NAME, + (status, key, name, data) -> { + assertTrue("Retrieve blob status not successful : " + status.resultCode, + status.isSuccess()); + assertEquals(l2Key, key); + assertEquals(name, TEST_DATA_NAME); + assertTrue(Arrays.equals(b.data, data.data)); + }); + + // verify the rest of the queue is still processed in order even if when one or more + // callback throw the remote exception + InOrder inOrder = inOrder(mMockService); + + inOrder.verify(mMockService).storeNetworkAttributes(eq(l2Key), mNapCaptor.capture(), + any()); + inOrder.verify(mMockService).retrieveNetworkAttributes(eq(l2Key), any()); + inOrder.verify(mMockService).storeBlob(eq(l2Key), eq(TEST_CLIENT_ID), eq(TEST_DATA_NAME), + eq(b), any()); + inOrder.verify(mMockService).retrieveBlob(eq(l2Key), eq(TEST_CLIENT_ID), + eq(TEST_OTHER_DATA_NAME), any()); + assertEquals(TEST_NETWORK_ATTRIBUTES, new NetworkAttributes(mNapCaptor.getValue())); + } + + @Test + public void testFactoryReset() throws RemoteException { + startIpMemoryStore(true /* supplyService */); + mStore.factoryReset(); + verify(mMockService, times(1)).factoryReset(); + } +} diff --git a/tests/unit/java/android/net/IpSecAlgorithmTest.java b/tests/unit/java/android/net/IpSecAlgorithmTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5bd2214774122a1efe6c6a75116836b500153c79 --- /dev/null +++ b/tests/unit/java/android/net/IpSecAlgorithmTest.java @@ -0,0 +1,226 @@ +/* + * 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; + +import static android.net.IpSecAlgorithm.ALGO_TO_REQUIRED_FIRST_SDK; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.content.res.Resources; +import android.os.Build; +import android.os.Parcel; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.CollectionUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Random; +import java.util.Set; + +/** Unit tests for {@link IpSecAlgorithm}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecAlgorithmTest { + private static final byte[] KEY_MATERIAL; + + private final Resources mMockResources = mock(Resources.class); + + static { + KEY_MATERIAL = new byte[128]; + new Random().nextBytes(KEY_MATERIAL); + }; + + private static byte[] generateKey(int keyLenInBits) { + return Arrays.copyOf(KEY_MATERIAL, keyLenInBits / 8); + } + + @Test + public void testNoTruncLen() throws Exception { + Entry<String, Integer>[] authAndAeadList = + new Entry[] { + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_MD5, 128), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA1, 160), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA256, 256), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA384, 384), + new SimpleEntry<>(IpSecAlgorithm.AUTH_HMAC_SHA512, 512), + new SimpleEntry<>(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, 224), + }; + + // Expect auth and aead algorithms to throw errors if trunclen is omitted. + for (Entry<String, Integer> algData : authAndAeadList) { + try { + new IpSecAlgorithm( + algData.getKey(), Arrays.copyOf(KEY_MATERIAL, algData.getValue() / 8)); + fail("Expected exception on unprovided auth trunclen"); + } catch (IllegalArgumentException expected) { + } + } + + // Ensure crypt works with no truncation length supplied. + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, 256 / 8)); + } + + private void checkAuthKeyAndTruncLenValidation(String algoName, int keyLen, int truncLen) + throws Exception { + new IpSecAlgorithm(algoName, generateKey(keyLen), truncLen); + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen)); + fail("Expected exception on unprovided auth trunclen"); + } catch (IllegalArgumentException pass) { + } + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen + 8), truncLen); + fail("Invalid key length not validated"); + } catch (IllegalArgumentException pass) { + } + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen), truncLen + 1); + fail("Invalid truncation length not validated"); + } catch (IllegalArgumentException pass) { + } + } + + private void checkCryptKeyLenValidation(String algoName, int keyLen) throws Exception { + new IpSecAlgorithm(algoName, generateKey(keyLen)); + + try { + new IpSecAlgorithm(algoName, generateKey(keyLen + 8)); + fail("Invalid key length not validated"); + } catch (IllegalArgumentException pass) { + } + } + + @Test + public void testValidationForAlgosAddedInS() throws Exception { + if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) { + return; + } + + for (int len : new int[] {160, 224, 288}) { + checkCryptKeyLenValidation(IpSecAlgorithm.CRYPT_AES_CTR, len); + } + checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_AES_XCBC, 128, 96); + checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_AES_CMAC, 128, 96); + checkAuthKeyAndTruncLenValidation(IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, 288, 128); + } + + @Test + public void testTruncLenValidation() throws Exception { + for (int truncLen : new int[] {256, 512}) { + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, + Arrays.copyOf(KEY_MATERIAL, 512 / 8), + truncLen); + } + + for (int truncLen : new int[] {255, 513}) { + try { + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, + Arrays.copyOf(KEY_MATERIAL, 512 / 8), + truncLen); + fail("Invalid truncation length not validated"); + } catch (IllegalArgumentException pass) { + } + } + } + + @Test + public void testLenValidation() throws Exception { + for (int len : new int[] {128, 192, 256}) { + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, len / 8)); + } + try { + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, Arrays.copyOf(KEY_MATERIAL, 384 / 8)); + fail("Invalid key length not validated"); + } catch (IllegalArgumentException pass) { + } + } + + @Test + public void testAlgoNameValidation() throws Exception { + try { + new IpSecAlgorithm("rot13", Arrays.copyOf(KEY_MATERIAL, 128 / 8)); + fail("Invalid algorithm name not validated"); + } catch (IllegalArgumentException pass) { + } + } + + @Test + public void testParcelUnparcel() throws Exception { + IpSecAlgorithm init = + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_SHA512, Arrays.copyOf(KEY_MATERIAL, 512 / 8), 256); + + Parcel p = Parcel.obtain(); + p.setDataPosition(0); + init.writeToParcel(p, 0); + + p.setDataPosition(0); + IpSecAlgorithm fin = IpSecAlgorithm.CREATOR.createFromParcel(p); + assertTrue("Parcel/Unparcel failed!", IpSecAlgorithm.equals(init, fin)); + p.recycle(); + } + + private static Set<String> getMandatoryAlgos() { + return CollectionUtils.filter( + ALGO_TO_REQUIRED_FIRST_SDK.keySet(), + i -> Build.VERSION.DEVICE_INITIAL_SDK_INT >= ALGO_TO_REQUIRED_FIRST_SDK.get(i)); + } + + private static Set<String> getOptionalAlgos() { + return CollectionUtils.filter( + ALGO_TO_REQUIRED_FIRST_SDK.keySet(), + i -> Build.VERSION.DEVICE_INITIAL_SDK_INT < ALGO_TO_REQUIRED_FIRST_SDK.get(i)); + } + + @Test + public void testGetSupportedAlgorithms() throws Exception { + assertTrue(IpSecAlgorithm.getSupportedAlgorithms().containsAll(getMandatoryAlgos())); + assertTrue(ALGO_TO_REQUIRED_FIRST_SDK.keySet().containsAll( + IpSecAlgorithm.getSupportedAlgorithms())); + } + + @Test + public void testLoadAlgos() throws Exception { + final Set<String> optionalAlgoSet = getOptionalAlgos(); + final String[] optionalAlgos = optionalAlgoSet.toArray(new String[0]); + + doReturn(optionalAlgos).when(mMockResources) + .getStringArray(com.android.internal.R.array.config_optionalIpSecAlgorithms); + + final Set<String> enabledAlgos = new HashSet<>(IpSecAlgorithm.loadAlgos(mMockResources)); + final Set<String> expectedAlgos = ALGO_TO_REQUIRED_FIRST_SDK.keySet(); + + assertEquals(expectedAlgos, enabledAlgos); + } +} diff --git a/tests/unit/java/android/net/IpSecConfigTest.java b/tests/unit/java/android/net/IpSecConfigTest.java new file mode 100644 index 0000000000000000000000000000000000000000..25e225ef303ac6d747764439e333e51bba4d07f3 --- /dev/null +++ b/tests/unit/java/android/net/IpSecConfigTest.java @@ -0,0 +1,103 @@ +/* + * 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; + +import static com.android.testutils.ParcelUtils.assertParcelSane; +import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link IpSecConfig}. */ +@SmallTest +@RunWith(JUnit4.class) +public class IpSecConfigTest { + + @Test + public void testDefaults() throws Exception { + IpSecConfig c = new IpSecConfig(); + assertEquals(IpSecTransform.MODE_TRANSPORT, c.getMode()); + assertEquals("", c.getSourceAddress()); + assertEquals("", c.getDestinationAddress()); + assertNull(c.getNetwork()); + assertEquals(IpSecTransform.ENCAP_NONE, c.getEncapType()); + assertEquals(IpSecManager.INVALID_RESOURCE_ID, c.getEncapSocketResourceId()); + assertEquals(0, c.getEncapRemotePort()); + assertEquals(0, c.getNattKeepaliveInterval()); + assertNull(c.getEncryption()); + assertNull(c.getAuthentication()); + assertEquals(IpSecManager.INVALID_RESOURCE_ID, c.getSpiResourceId()); + assertEquals(0, c.getXfrmInterfaceId()); + } + + private IpSecConfig getSampleConfig() { + IpSecConfig c = new IpSecConfig(); + c.setMode(IpSecTransform.MODE_TUNNEL); + c.setSourceAddress("0.0.0.0"); + c.setDestinationAddress("1.2.3.4"); + c.setSpiResourceId(1984); + c.setEncryption( + new IpSecAlgorithm( + IpSecAlgorithm.CRYPT_AES_CBC, + new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF})); + c.setAuthentication( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_HMAC_MD5, + new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0}, + 128)); + c.setAuthenticatedEncryption( + new IpSecAlgorithm( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + new byte[] { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0, 1, 2, 3, 4 + }, + 128)); + c.setEncapType(android.system.OsConstants.UDP_ENCAP_ESPINUDP); + c.setEncapSocketResourceId(7); + c.setEncapRemotePort(22); + c.setNattKeepaliveInterval(42); + c.setMarkValue(12); + c.setMarkMask(23); + c.setXfrmInterfaceId(34); + + return c; + } + + @Test + public void testCopyConstructor() { + IpSecConfig original = getSampleConfig(); + IpSecConfig copy = new IpSecConfig(original); + + assertEquals(original, copy); + assertNotSame(original, copy); + } + + @Test + public void testParcelUnparcel() { + assertParcelingIsLossless(new IpSecConfig()); + + IpSecConfig c = getSampleConfig(); + assertParcelSane(c, 15); + } +} diff --git a/tests/unit/java/android/net/IpSecManagerTest.java b/tests/unit/java/android/net/IpSecManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..730e2d56bd780b35097188bab3948ee9b2ca3aae --- /dev/null +++ b/tests/unit/java/android/net/IpSecManagerTest.java @@ -0,0 +1,305 @@ +/* + * 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; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.system.Os; +import android.test.mock.MockContext; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.IpSecService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +/** Unit tests for {@link IpSecManager}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecManagerTest { + + private static final int TEST_UDP_ENCAP_PORT = 34567; + private static final int DROID_SPI = 0xD1201D; + private static final int DUMMY_RESOURCE_ID = 0x1234; + + private static final InetAddress GOOGLE_DNS_4; + private static final String VTI_INTF_NAME = "ipsec_test"; + private static final InetAddress VTI_LOCAL_ADDRESS; + private static final LinkAddress VTI_INNER_ADDRESS = new LinkAddress("10.0.1.1/24"); + + static { + try { + // Google Public DNS Addresses; + GOOGLE_DNS_4 = InetAddress.getByName("8.8.8.8"); + VTI_LOCAL_ADDRESS = InetAddress.getByName("8.8.4.4"); + } catch (UnknownHostException e) { + throw new RuntimeException("Could not resolve DNS Addresses", e); + } + } + + private IpSecService mMockIpSecService; + private IpSecManager mIpSecManager; + private MockContext mMockContext = new MockContext() { + @Override + public String getOpPackageName() { + return "fooPackage"; + } + }; + + @Before + public void setUp() throws Exception { + mMockIpSecService = mock(IpSecService.class); + mIpSecManager = new IpSecManager(mMockContext, mMockIpSecService); + } + + /* + * Allocate a specific SPI + * Close SPIs + */ + @Test + public void testAllocSpi() throws Exception { + IpSecSpiResponse spiResp = + new IpSecSpiResponse(IpSecManager.Status.OK, DUMMY_RESOURCE_ID, DROID_SPI); + when(mMockIpSecService.allocateSecurityParameterIndex( + eq(GOOGLE_DNS_4.getHostAddress()), + eq(DROID_SPI), + anyObject())) + .thenReturn(spiResp); + + IpSecManager.SecurityParameterIndex droidSpi = + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4, DROID_SPI); + assertEquals(DROID_SPI, droidSpi.getSpi()); + + droidSpi.close(); + + verify(mMockIpSecService).releaseSecurityParameterIndex(DUMMY_RESOURCE_ID); + } + + @Test + public void testAllocRandomSpi() throws Exception { + IpSecSpiResponse spiResp = + new IpSecSpiResponse(IpSecManager.Status.OK, DUMMY_RESOURCE_ID, DROID_SPI); + when(mMockIpSecService.allocateSecurityParameterIndex( + eq(GOOGLE_DNS_4.getHostAddress()), + eq(IpSecManager.INVALID_SECURITY_PARAMETER_INDEX), + anyObject())) + .thenReturn(spiResp); + + IpSecManager.SecurityParameterIndex randomSpi = + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4); + + assertEquals(DROID_SPI, randomSpi.getSpi()); + + randomSpi.close(); + + verify(mMockIpSecService).releaseSecurityParameterIndex(DUMMY_RESOURCE_ID); + } + + /* + * Throws resource unavailable exception + */ + @Test + public void testAllocSpiResUnavailableException() throws Exception { + IpSecSpiResponse spiResp = + new IpSecSpiResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE, 0, 0); + when(mMockIpSecService.allocateSecurityParameterIndex( + anyString(), anyInt(), anyObject())) + .thenReturn(spiResp); + + try { + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4); + fail("ResourceUnavailableException was not thrown"); + } catch (IpSecManager.ResourceUnavailableException e) { + } + } + + /* + * Throws spi unavailable exception + */ + @Test + public void testAllocSpiSpiUnavailableException() throws Exception { + IpSecSpiResponse spiResp = new IpSecSpiResponse(IpSecManager.Status.SPI_UNAVAILABLE, 0, 0); + when(mMockIpSecService.allocateSecurityParameterIndex( + anyString(), anyInt(), anyObject())) + .thenReturn(spiResp); + + try { + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4); + fail("ResourceUnavailableException was not thrown"); + } catch (IpSecManager.ResourceUnavailableException e) { + } + } + + /* + * Should throw exception when request spi 0 in IpSecManager + */ + @Test + public void testRequestAllocInvalidSpi() throws Exception { + try { + mIpSecManager.allocateSecurityParameterIndex(GOOGLE_DNS_4, 0); + fail("Able to allocate invalid spi"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testOpenEncapsulationSocket() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + new IpSecUdpEncapResponse( + IpSecManager.Status.OK, + DUMMY_RESOURCE_ID, + TEST_UDP_ENCAP_PORT, + Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)); + when(mMockIpSecService.openUdpEncapsulationSocket(eq(TEST_UDP_ENCAP_PORT), anyObject())) + .thenReturn(udpEncapResp); + + IpSecManager.UdpEncapsulationSocket encapSocket = + mIpSecManager.openUdpEncapsulationSocket(TEST_UDP_ENCAP_PORT); + assertNotNull(encapSocket.getFileDescriptor()); + assertEquals(TEST_UDP_ENCAP_PORT, encapSocket.getPort()); + + encapSocket.close(); + + verify(mMockIpSecService).closeUdpEncapsulationSocket(DUMMY_RESOURCE_ID); + } + + @Test + public void testApplyTransportModeTransformEnsuresSocketCreation() throws Exception { + Socket socket = new Socket(); + IpSecConfig dummyConfig = new IpSecConfig(); + IpSecTransform dummyTransform = new IpSecTransform(null, dummyConfig); + + // Even if underlying SocketImpl is not initalized, this should force the init, and + // thereby succeed. + mIpSecManager.applyTransportModeTransform( + socket, IpSecManager.DIRECTION_IN, dummyTransform); + + // Check to make sure the FileDescriptor is non-null + assertNotNull(socket.getFileDescriptor$()); + } + + @Test + public void testRemoveTransportModeTransformsForcesSocketCreation() throws Exception { + Socket socket = new Socket(); + + // Even if underlying SocketImpl is not initalized, this should force the init, and + // thereby succeed. + mIpSecManager.removeTransportModeTransforms(socket); + + // Check to make sure the FileDescriptor is non-null + assertNotNull(socket.getFileDescriptor$()); + } + + @Test + public void testOpenEncapsulationSocketOnRandomPort() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + new IpSecUdpEncapResponse( + IpSecManager.Status.OK, + DUMMY_RESOURCE_ID, + TEST_UDP_ENCAP_PORT, + Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)); + + when(mMockIpSecService.openUdpEncapsulationSocket(eq(0), anyObject())) + .thenReturn(udpEncapResp); + + IpSecManager.UdpEncapsulationSocket encapSocket = + mIpSecManager.openUdpEncapsulationSocket(); + + assertNotNull(encapSocket.getFileDescriptor()); + assertEquals(TEST_UDP_ENCAP_PORT, encapSocket.getPort()); + + encapSocket.close(); + + verify(mMockIpSecService).closeUdpEncapsulationSocket(DUMMY_RESOURCE_ID); + } + + @Test + public void testOpenEncapsulationSocketWithInvalidPort() throws Exception { + try { + mIpSecManager.openUdpEncapsulationSocket(IpSecManager.INVALID_SECURITY_PARAMETER_INDEX); + fail("IllegalArgumentException was not thrown"); + } catch (IllegalArgumentException e) { + } + } + + // TODO: add test when applicable transform builder interface is available + + private IpSecManager.IpSecTunnelInterface createAndValidateVti(int resourceId, String intfName) + throws Exception { + IpSecTunnelInterfaceResponse dummyResponse = + new IpSecTunnelInterfaceResponse(IpSecManager.Status.OK, resourceId, intfName); + when(mMockIpSecService.createTunnelInterface( + eq(VTI_LOCAL_ADDRESS.getHostAddress()), eq(GOOGLE_DNS_4.getHostAddress()), + anyObject(), anyObject(), anyString())) + .thenReturn(dummyResponse); + + IpSecManager.IpSecTunnelInterface tunnelIntf = mIpSecManager.createIpSecTunnelInterface( + VTI_LOCAL_ADDRESS, GOOGLE_DNS_4, mock(Network.class)); + + assertNotNull(tunnelIntf); + return tunnelIntf; + } + + @Test + public void testCreateVti() throws Exception { + IpSecManager.IpSecTunnelInterface tunnelIntf = + createAndValidateVti(DUMMY_RESOURCE_ID, VTI_INTF_NAME); + + assertEquals(VTI_INTF_NAME, tunnelIntf.getInterfaceName()); + + tunnelIntf.close(); + verify(mMockIpSecService).deleteTunnelInterface(eq(DUMMY_RESOURCE_ID), anyString()); + } + + @Test + public void testAddRemoveAddressesFromVti() throws Exception { + IpSecManager.IpSecTunnelInterface tunnelIntf = + createAndValidateVti(DUMMY_RESOURCE_ID, VTI_INTF_NAME); + + tunnelIntf.addAddress(VTI_INNER_ADDRESS.getAddress(), + VTI_INNER_ADDRESS.getPrefixLength()); + verify(mMockIpSecService) + .addAddressToTunnelInterface( + eq(DUMMY_RESOURCE_ID), eq(VTI_INNER_ADDRESS), anyString()); + + tunnelIntf.removeAddress(VTI_INNER_ADDRESS.getAddress(), + VTI_INNER_ADDRESS.getPrefixLength()); + verify(mMockIpSecService) + .addAddressToTunnelInterface( + eq(DUMMY_RESOURCE_ID), eq(VTI_INNER_ADDRESS), anyString()); + } +} diff --git a/tests/unit/java/android/net/IpSecTransformTest.java b/tests/unit/java/android/net/IpSecTransformTest.java new file mode 100644 index 0000000000000000000000000000000000000000..424f23dbbaf63494b30aef571c57b6faa320fb91 --- /dev/null +++ b/tests/unit/java/android/net/IpSecTransformTest.java @@ -0,0 +1,62 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link IpSecTransform}. */ +@SmallTest +@RunWith(JUnit4.class) +public class IpSecTransformTest { + + @Test + public void testCreateTransformCopiesConfig() { + // Create a config with a few parameters to make sure it's not empty + IpSecConfig config = new IpSecConfig(); + config.setSourceAddress("0.0.0.0"); + config.setDestinationAddress("1.2.3.4"); + config.setSpiResourceId(1984); + + IpSecTransform preModification = new IpSecTransform(null, config); + + config.setSpiResourceId(1985); + IpSecTransform postModification = new IpSecTransform(null, config); + + assertNotEquals(preModification, postModification); + } + + @Test + public void testCreateTransformsWithSameConfigEqual() { + // Create a config with a few parameters to make sure it's not empty + IpSecConfig config = new IpSecConfig(); + config.setSourceAddress("0.0.0.0"); + config.setDestinationAddress("1.2.3.4"); + config.setSpiResourceId(1984); + + IpSecTransform config1 = new IpSecTransform(null, config); + IpSecTransform config2 = new IpSecTransform(null, config); + + assertEquals(config1, config2); + } +} diff --git a/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fc739fbfac617507b48a23d94e2801a4d446f589 --- /dev/null +++ b/tests/unit/java/android/net/KeepalivePacketDataUtilTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 com.android.testutils.ParcelUtils.assertParcelingIsLossless; + +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.util.KeepalivePacketDataUtil; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.net.InetAddress; +import java.nio.ByteBuffer; + +@RunWith(JUnit4.class) +public final class KeepalivePacketDataUtilTest { + private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 1}; + private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 5}; + + @Before + public void setUp() {} + + @Test + public void testFromTcpKeepaliveStableParcelable() throws Exception { + final int srcPort = 1234; + final int dstPort = 4321; + final int seq = 0x11111111; + final int ack = 0x22222222; + final int wnd = 8000; + final int wndScale = 2; + final int tos = 4; + final int ttl = 64; + TcpKeepalivePacketData resultData = null; + final TcpKeepalivePacketDataParcelable testInfo = new TcpKeepalivePacketDataParcelable(); + testInfo.srcAddress = IPV4_KEEPALIVE_SRC_ADDR; + testInfo.srcPort = srcPort; + testInfo.dstAddress = IPV4_KEEPALIVE_DST_ADDR; + testInfo.dstPort = dstPort; + testInfo.seq = seq; + testInfo.ack = ack; + testInfo.rcvWnd = wnd; + testInfo.rcvWndScale = wndScale; + testInfo.tos = tos; + testInfo.ttl = ttl; + try { + resultData = KeepalivePacketDataUtil.fromStableParcelable(testInfo); + } catch (InvalidPacketException e) { + fail("InvalidPacketException: " + e); + } + + assertEquals(InetAddress.getByAddress(testInfo.srcAddress), resultData.getSrcAddress()); + assertEquals(InetAddress.getByAddress(testInfo.dstAddress), resultData.getDstAddress()); + assertEquals(testInfo.srcPort, resultData.getSrcPort()); + assertEquals(testInfo.dstPort, resultData.getDstPort()); + assertEquals(testInfo.seq, resultData.tcpSeq); + assertEquals(testInfo.ack, resultData.tcpAck); + assertEquals(testInfo.rcvWnd, resultData.tcpWindow); + assertEquals(testInfo.rcvWndScale, resultData.tcpWindowScale); + assertEquals(testInfo.tos, resultData.ipTos); + assertEquals(testInfo.ttl, resultData.ipTtl); + + assertParcelingIsLossless(resultData); + + final byte[] packet = resultData.getPacket(); + // IP version and IHL + assertEquals(packet[0], 0x45); + // TOS + assertEquals(packet[1], tos); + // TTL + assertEquals(packet[8], ttl); + // Source IP address. + byte[] ip = new byte[4]; + ByteBuffer buf = ByteBuffer.wrap(packet, 12, 4); + buf.get(ip); + assertArrayEquals(ip, IPV4_KEEPALIVE_SRC_ADDR); + // Destination IP address. + buf = ByteBuffer.wrap(packet, 16, 4); + buf.get(ip); + assertArrayEquals(ip, IPV4_KEEPALIVE_DST_ADDR); + + buf = ByteBuffer.wrap(packet, 20, 12); + // Source port. + assertEquals(buf.getShort(), srcPort); + // Destination port. + assertEquals(buf.getShort(), dstPort); + // Sequence number. + assertEquals(buf.getInt(), seq); + // Ack. + assertEquals(buf.getInt(), ack); + // Window size. + buf = ByteBuffer.wrap(packet, 34, 2); + assertEquals(buf.getShort(), wnd >> wndScale); + } + + //TODO: add ipv6 test when ipv6 supported + + @Test + public void testToTcpKeepaliveStableParcelable() throws Exception { + final int srcPort = 1234; + final int dstPort = 4321; + final int sequence = 0x11111111; + final int ack = 0x22222222; + final int wnd = 48_000; + final int wndScale = 2; + final int tos = 4; + final int ttl = 64; + final TcpKeepalivePacketDataParcelable testInfo = new TcpKeepalivePacketDataParcelable(); + testInfo.srcAddress = IPV4_KEEPALIVE_SRC_ADDR; + testInfo.srcPort = srcPort; + testInfo.dstAddress = IPV4_KEEPALIVE_DST_ADDR; + testInfo.dstPort = dstPort; + testInfo.seq = sequence; + testInfo.ack = ack; + testInfo.rcvWnd = wnd; + testInfo.rcvWndScale = wndScale; + testInfo.tos = tos; + testInfo.ttl = ttl; + TcpKeepalivePacketData testData = null; + TcpKeepalivePacketDataParcelable resultData = null; + testData = KeepalivePacketDataUtil.fromStableParcelable(testInfo); + resultData = KeepalivePacketDataUtil.toStableParcelable(testData); + assertArrayEquals(resultData.srcAddress, IPV4_KEEPALIVE_SRC_ADDR); + assertArrayEquals(resultData.dstAddress, IPV4_KEEPALIVE_DST_ADDR); + assertEquals(resultData.srcPort, srcPort); + assertEquals(resultData.dstPort, dstPort); + assertEquals(resultData.seq, sequence); + assertEquals(resultData.ack, ack); + assertEquals(resultData.rcvWnd, wnd); + assertEquals(resultData.rcvWndScale, wndScale); + assertEquals(resultData.tos, tos); + assertEquals(resultData.ttl, ttl); + + final String expected = "" + + "android.net.TcpKeepalivePacketDataParcelable{srcAddress: [10, 0, 0, 1]," + + " srcPort: 1234, dstAddress: [10, 0, 0, 5], dstPort: 4321, seq: 286331153," + + " ack: 572662306, rcvWnd: 48000, rcvWndScale: 2, tos: 4, ttl: 64}"; + assertEquals(expected, resultData.toString()); + } + + @Test + public void testParseTcpKeepalivePacketData() throws Exception { + final int srcPort = 1234; + final int dstPort = 4321; + final int sequence = 0x11111111; + final int ack = 0x22222222; + final int wnd = 4800; + final int wndScale = 2; + final int tos = 4; + final int ttl = 64; + final TcpKeepalivePacketDataParcelable testParcel = new TcpKeepalivePacketDataParcelable(); + testParcel.srcAddress = IPV4_KEEPALIVE_SRC_ADDR; + testParcel.srcPort = srcPort; + testParcel.dstAddress = IPV4_KEEPALIVE_DST_ADDR; + testParcel.dstPort = dstPort; + testParcel.seq = sequence; + testParcel.ack = ack; + testParcel.rcvWnd = wnd; + testParcel.rcvWndScale = wndScale; + testParcel.tos = tos; + testParcel.ttl = ttl; + + final KeepalivePacketData testData = + KeepalivePacketDataUtil.fromStableParcelable(testParcel); + final TcpKeepalivePacketDataParcelable parsedParcelable = + KeepalivePacketDataUtil.parseTcpKeepalivePacketData(testData); + final TcpKeepalivePacketData roundTripData = + KeepalivePacketDataUtil.fromStableParcelable(parsedParcelable); + + // Generated packet is the same, but rcvWnd / wndScale will differ if scale is non-zero + assertTrue(testData.getPacket().length > 0); + assertArrayEquals(testData.getPacket(), roundTripData.getPacket()); + + testParcel.rcvWndScale = 0; + final KeepalivePacketData noScaleTestData = + KeepalivePacketDataUtil.fromStableParcelable(testParcel); + final TcpKeepalivePacketDataParcelable noScaleParsedParcelable = + KeepalivePacketDataUtil.parseTcpKeepalivePacketData(noScaleTestData); + final TcpKeepalivePacketData noScaleRoundTripData = + KeepalivePacketDataUtil.fromStableParcelable(noScaleParsedParcelable); + assertEquals(noScaleTestData, noScaleRoundTripData); + assertTrue(noScaleTestData.getPacket().length > 0); + assertArrayEquals(noScaleTestData.getPacket(), noScaleRoundTripData.getPacket()); + } +} diff --git a/tests/unit/java/android/net/MacAddressTest.java b/tests/unit/java/android/net/MacAddressTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6de31f6b4be12b391000576359364f25b68551da --- /dev/null +++ b/tests/unit/java/android/net/MacAddressTest.java @@ -0,0 +1,312 @@ +/* + * 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. + */ + +package android.net; + +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 androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.MacAddressUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet6Address; +import java.util.Arrays; +import java.util.Random; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class MacAddressTest { + + static class AddrTypeTestCase { + byte[] addr; + int expectedType; + + static AddrTypeTestCase of(int expectedType, int... addr) { + AddrTypeTestCase t = new AddrTypeTestCase(); + t.expectedType = expectedType; + t.addr = toByteArray(addr); + return t; + } + } + + @Test + public void testMacAddrTypes() { + AddrTypeTestCase[] testcases = { + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN), + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 0), + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 1, 2, 3, 4, 5), + AddrTypeTestCase.of(MacAddress.TYPE_UNKNOWN, 1, 2, 3, 4, 5, 6, 7), + AddrTypeTestCase.of(MacAddress.TYPE_UNICAST, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0), + AddrTypeTestCase.of(MacAddress.TYPE_BROADCAST, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff), + AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 1, 2, 3, 4, 5, 6), + AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 11, 22, 33, 44, 55, 66), + AddrTypeTestCase.of(MacAddress.TYPE_MULTICAST, 33, 33, 0xaa, 0xbb, 0xcc, 0xdd) + }; + + for (AddrTypeTestCase t : testcases) { + int got = MacAddress.macAddressType(t.addr); + String msg = String.format("expected type of %s to be %s, but got %s", + Arrays.toString(t.addr), t.expectedType, got); + assertEquals(msg, t.expectedType, got); + + if (got != MacAddress.TYPE_UNKNOWN) { + assertEquals(got, MacAddress.fromBytes(t.addr).getAddressType()); + } + } + } + + @Test + public void testToOuiString() { + String[][] macs = { + {"07:00:d3:56:8a:c4", "07:00:d3"}, + {"33:33:aa:bb:cc:dd", "33:33:aa"}, + {"06:00:00:00:00:00", "06:00:00"}, + {"07:00:d3:56:8a:c4", "07:00:d3"} + }; + + for (String[] pair : macs) { + String mac = pair[0]; + String expected = pair[1]; + assertEquals(expected, MacAddress.fromString(mac).toOuiString()); + } + } + + @Test + public void testHexPaddingWhenPrinting() { + String[] macs = { + "07:00:d3:56:8a:c4", + "33:33:aa:bb:cc:dd", + "06:00:00:00:00:00", + "07:00:d3:56:8a:c4" + }; + + for (String mac : macs) { + assertEquals(mac, MacAddress.fromString(mac).toString()); + assertEquals(mac, + MacAddress.stringAddrFromByteAddr(MacAddress.byteAddrFromStringAddr(mac))); + } + } + + @Test + public void testIsMulticastAddress() { + MacAddress[] multicastAddresses = { + MacAddress.BROADCAST_ADDRESS, + MacAddress.fromString("07:00:d3:56:8a:c4"), + MacAddress.fromString("33:33:aa:bb:cc:dd"), + }; + MacAddress[] unicastAddresses = { + MacAddress.ALL_ZEROS_ADDRESS, + MacAddress.fromString("00:01:44:55:66:77"), + MacAddress.fromString("08:00:22:33:44:55"), + MacAddress.fromString("06:00:00:00:00:00"), + }; + + for (MacAddress mac : multicastAddresses) { + String msg = mac.toString() + " expected to be a multicast address"; + assertTrue(msg, MacAddressUtils.isMulticastAddress(mac)); + } + for (MacAddress mac : unicastAddresses) { + String msg = mac.toString() + " expected not to be a multicast address"; + assertFalse(msg, MacAddressUtils.isMulticastAddress(mac)); + } + } + + @Test + public void testIsLocallyAssignedAddress() { + MacAddress[] localAddresses = { + MacAddress.fromString("06:00:00:00:00:00"), + MacAddress.fromString("07:00:d3:56:8a:c4"), + MacAddress.fromString("33:33:aa:bb:cc:dd"), + }; + MacAddress[] universalAddresses = { + MacAddress.fromString("00:01:44:55:66:77"), + MacAddress.fromString("08:00:22:33:44:55"), + }; + + for (MacAddress mac : localAddresses) { + String msg = mac.toString() + " expected to be a locally assigned address"; + assertTrue(msg, mac.isLocallyAssigned()); + } + for (MacAddress mac : universalAddresses) { + String msg = mac.toString() + " expected not to be globally unique address"; + assertFalse(msg, mac.isLocallyAssigned()); + } + } + + @Test + public void testMacAddressConversions() { + final int iterations = 10000; + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddressUtils.createRandomUnicastAddress(); + + String stringRepr = mac.toString(); + byte[] bytesRepr = mac.toByteArray(); + + assertEquals(mac, MacAddress.fromString(stringRepr)); + assertEquals(mac, MacAddress.fromBytes(bytesRepr)); + + assertEquals(mac, MacAddress.fromString(MacAddress.stringAddrFromByteAddr(bytesRepr))); + assertEquals(mac, MacAddress.fromBytes(MacAddress.byteAddrFromStringAddr(stringRepr))); + } + } + + @Test + public void testMacAddressRandomGeneration() { + final int iterations = 1000; + final String expectedAndroidOui = "da:a1:19"; + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddress.createRandomUnicastAddressWithGoogleBase(); + String stringRepr = mac.toString(); + + assertTrue(stringRepr + " expected to be a locally assigned address", + mac.isLocallyAssigned()); + assertTrue(stringRepr + " expected to begin with " + expectedAndroidOui, + stringRepr.startsWith(expectedAndroidOui)); + } + + final Random r = new Random(); + final String anotherOui = "24:5f:78"; + final String expectedLocalOui = "26:5f:78"; + final MacAddress base = MacAddress.fromString(anotherOui + ":0:0:0"); + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddressUtils.createRandomUnicastAddress(base, r); + String stringRepr = mac.toString(); + + assertTrue(stringRepr + " expected to be a locally assigned address", + mac.isLocallyAssigned()); + assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType()); + assertTrue(stringRepr + " expected to begin with " + expectedLocalOui, + stringRepr.startsWith(expectedLocalOui)); + } + + for (int i = 0; i < iterations; i++) { + MacAddress mac = MacAddressUtils.createRandomUnicastAddress(); + String stringRepr = mac.toString(); + + assertTrue(stringRepr + " expected to be a locally assigned address", + mac.isLocallyAssigned()); + assertEquals(MacAddress.TYPE_UNICAST, mac.getAddressType()); + } + } + + @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() { + MacAddress mac = MacAddress.fromString("52:74:f2:b1:a8:7f"); + 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}; + Inet6Address llv6 = mac.getLinkLocalIpv6FromEui48Mac(); + assertTrue(llv6.isLinkLocalAddress()); + assertArrayEquals(inet6ll, llv6.getAddress()); + } + + static byte[] toByteArray(int... in) { + byte[] out = new byte[in.length]; + for (int i = 0; i < in.length; i++) { + out[i] = (byte) in[i]; + } + return out; + } +} diff --git a/tests/unit/java/android/net/NetworkIdentityTest.kt b/tests/unit/java/android/net/NetworkIdentityTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb2b85c1457835b0b373f432d06527229cf9ae9d --- /dev/null +++ b/tests/unit/java/android/net/NetworkIdentityTest.kt @@ -0,0 +1,54 @@ +/* + * 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.net.NetworkIdentity.OEM_NONE +import android.net.NetworkIdentity.OEM_PAID +import android.net.NetworkIdentity.OEM_PRIVATE +import android.net.NetworkIdentity.getOemBitfield +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import kotlin.test.assertEquals + +@RunWith(JUnit4::class) +class NetworkIdentityTest { + @Test + fun testGetOemBitfield() { + val oemNone = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, false) + } + val oemPaid = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, true) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, false) + } + val oemPrivate = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, false) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, true) + } + val oemAll = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, true) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, true) + } + + assertEquals(getOemBitfield(oemNone), OEM_NONE) + assertEquals(getOemBitfield(oemPaid), OEM_PAID) + assertEquals(getOemBitfield(oemPrivate), OEM_PRIVATE) + assertEquals(getOemBitfield(oemAll), OEM_PAID or OEM_PRIVATE) + } +} diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..13558cd51c28faea754d7111d147bf9f684e1dfe --- /dev/null +++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java @@ -0,0 +1,599 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.net.NetworkStatsHistory.DataStreamUtils.readVarLong; +import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLong; +import static android.net.NetworkStatsHistory.Entry.UNKNOWN; +import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkStatsHistory.FIELD_OPERATIONS; +import static android.net.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_RX_PACKETS; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; +import static android.net.TrafficStats.GB_IN_BYTES; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.SECOND_IN_MILLIS; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; +import static android.text.format.DateUtils.YEAR_IN_MILLIS; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.tests.net.R; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.Random; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsHistoryTest { + private static final String TAG = "NetworkStatsHistoryTest"; + + private static final long TEST_START = 1194220800000L; + + private NetworkStatsHistory stats; + + @After + public void tearDown() throws Exception { + if (stats != null) { + assertConsistent(stats); + } + } + + @Test + public void testReadOriginalVersion() throws Exception { + final Context context = InstrumentationRegistry.getContext(); + final DataInputStream in = + new DataInputStream(context.getResources().openRawResource(R.raw.history_v1)); + + NetworkStatsHistory.Entry entry = null; + try { + final NetworkStatsHistory history = new NetworkStatsHistory(in); + assertEquals(15 * SECOND_IN_MILLIS, history.getBucketDuration()); + + entry = history.getValues(0, entry); + assertEquals(29143L, entry.rxBytes); + assertEquals(6223L, entry.txBytes); + + entry = history.getValues(history.size() - 1, entry); + assertEquals(1476L, entry.rxBytes); + assertEquals(838L, entry.txBytes); + + entry = history.getValues(Long.MIN_VALUE, Long.MAX_VALUE, entry); + assertEquals(332401L, entry.rxBytes); + assertEquals(64314L, entry.txBytes); + + } finally { + in.close(); + } + } + + @Test + public void testRecordSingleBucket() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record data into narrow window to get single bucket + stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + assertEquals(1, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS, 1024L, 10L, 2048L, 20L, 2L); + } + + @Test + public void testRecordEqualBuckets() throws Exception { + final long bucketDuration = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(bucketDuration); + + // split equally across two buckets + final long recordStart = TEST_START + (bucketDuration / 2); + stats.recordData(recordStart, recordStart + bucketDuration, + new NetworkStats.Entry(1024L, 10L, 128L, 2L, 2L)); + + assertEquals(2, stats.size()); + assertValues(stats, 0, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L); + assertValues(stats, 1, HOUR_IN_MILLIS / 2, 512L, 5L, 64L, 1L, 1L); + } + + @Test + public void testRecordTouchingBuckets() throws Exception { + final long BUCKET_SIZE = 15 * MINUTE_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // split almost completely into middle bucket, but with a few minutes + // overlap into neighboring buckets. total record is 20 minutes. + final long recordStart = (TEST_START + BUCKET_SIZE) - MINUTE_IN_MILLIS; + final long recordEnd = (TEST_START + (BUCKET_SIZE * 2)) + (MINUTE_IN_MILLIS * 4); + stats.recordData(recordStart, recordEnd, + new NetworkStats.Entry(1000L, 2000L, 5000L, 10000L, 100L)); + + assertEquals(3, stats.size()); + // first bucket should have (1/20 of value) + assertValues(stats, 0, MINUTE_IN_MILLIS, 50L, 100L, 250L, 500L, 5L); + // second bucket should have (15/20 of value) + assertValues(stats, 1, 15 * MINUTE_IN_MILLIS, 750L, 1500L, 3750L, 7500L, 75L); + // final bucket should have (4/20 of value) + assertValues(stats, 2, 4 * MINUTE_IN_MILLIS, 200L, 400L, 1000L, 2000L, 20L); + } + + @Test + public void testRecordGapBuckets() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record some data today and next week with large gap + final long firstStart = TEST_START; + final long lastStart = TEST_START + WEEK_IN_MILLIS; + stats.recordData(firstStart, firstStart + SECOND_IN_MILLIS, + new NetworkStats.Entry(128L, 2L, 256L, 4L, 1L)); + stats.recordData(lastStart, lastStart + SECOND_IN_MILLIS, + new NetworkStats.Entry(64L, 1L, 512L, 8L, 2L)); + + // we should have two buckets, far apart from each other + assertEquals(2, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L); + assertValues(stats, 1, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L); + + // now record something in middle, spread across two buckets + final long middleStart = TEST_START + DAY_IN_MILLIS; + final long middleEnd = middleStart + (HOUR_IN_MILLIS * 2); + stats.recordData(middleStart, middleEnd, + new NetworkStats.Entry(2048L, 4L, 2048L, 4L, 2L)); + + // now should have four buckets, with new record in middle two buckets + assertEquals(4, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS, 128L, 2L, 256L, 4L, 1L); + assertValues(stats, 1, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L); + assertValues(stats, 2, HOUR_IN_MILLIS, 1024L, 2L, 1024L, 2L, 1L); + assertValues(stats, 3, SECOND_IN_MILLIS, 64L, 1L, 512L, 8L, 2L); + } + + @Test + public void testRecordOverlapBuckets() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record some data in one bucket, and another overlapping buckets + stats.recordData(TEST_START, TEST_START + SECOND_IN_MILLIS, + new NetworkStats.Entry(256L, 2L, 256L, 2L, 1L)); + final long midStart = TEST_START + (HOUR_IN_MILLIS / 2); + stats.recordData(midStart, midStart + HOUR_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 1024L, 10L, 10L)); + + // should have two buckets, with some data mixed together + assertEquals(2, stats.size()); + assertValues(stats, 0, SECOND_IN_MILLIS + (HOUR_IN_MILLIS / 2), 768L, 7L, 768L, 7L, 6L); + assertValues(stats, 1, (HOUR_IN_MILLIS / 2), 512L, 5L, 512L, 5L, 5L); + } + + @Test + public void testRecordEntireGapIdentical() throws Exception { + // first, create two separate histories far apart + final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats1.recordData(TEST_START, TEST_START + 2 * HOUR_IN_MILLIS, 2000L, 1000L); + + final long TEST_START_2 = TEST_START + DAY_IN_MILLIS; + final NetworkStatsHistory stats2 = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats2.recordData(TEST_START_2, TEST_START_2 + 2 * HOUR_IN_MILLIS, 1000L, 500L); + + // combine together with identical bucket size + stats = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats.recordEntireHistory(stats1); + stats.recordEntireHistory(stats2); + + // first verify that totals match up + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 3000L, 1500L); + + // now inspect internal buckets + assertValues(stats, 0, 1000L, 500L); + assertValues(stats, 1, 1000L, 500L); + assertValues(stats, 2, 500L, 250L); + assertValues(stats, 3, 500L, 250L); + } + + @Test + public void testRecordEntireOverlapVaryingBuckets() throws Exception { + // create history just over hour bucket boundary + final NetworkStatsHistory stats1 = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats1.recordData(TEST_START, TEST_START + MINUTE_IN_MILLIS * 60, 600L, 600L); + + final long TEST_START_2 = TEST_START + MINUTE_IN_MILLIS; + final NetworkStatsHistory stats2 = new NetworkStatsHistory(MINUTE_IN_MILLIS); + stats2.recordData(TEST_START_2, TEST_START_2 + MINUTE_IN_MILLIS * 5, 50L, 50L); + + // combine together with minute bucket size + stats = new NetworkStatsHistory(MINUTE_IN_MILLIS); + stats.recordEntireHistory(stats1); + stats.recordEntireHistory(stats2); + + // first verify that totals match up + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L); + + // now inspect internal buckets + assertValues(stats, 0, 10L, 10L); + assertValues(stats, 1, 20L, 20L); + assertValues(stats, 2, 20L, 20L); + assertValues(stats, 3, 20L, 20L); + assertValues(stats, 4, 20L, 20L); + assertValues(stats, 5, 20L, 20L); + assertValues(stats, 6, 10L, 10L); + + // now combine using 15min buckets + stats = new NetworkStatsHistory(HOUR_IN_MILLIS / 4); + stats.recordEntireHistory(stats1); + stats.recordEntireHistory(stats2); + + // first verify that totals match up + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 650L, 650L); + + // and inspect buckets + assertValues(stats, 0, 200L, 200L); + assertValues(stats, 1, 150L, 150L); + assertValues(stats, 2, 150L, 150L); + assertValues(stats, 3, 150L, 150L); + } + + @Test + public void testRemove() throws Exception { + stats = new NetworkStatsHistory(HOUR_IN_MILLIS); + + // record some data across 24 buckets + stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 24L, 24L); + assertEquals(24, stats.size()); + + // try removing invalid data; should be no change + stats.removeBucketsBefore(0 - DAY_IN_MILLIS); + assertEquals(24, stats.size()); + + // try removing far before buckets; should be no change + stats.removeBucketsBefore(TEST_START - YEAR_IN_MILLIS); + assertEquals(24, stats.size()); + + // try removing just moments into first bucket; should be no change + // since that bucket contains data beyond the cutoff + stats.removeBucketsBefore(TEST_START + SECOND_IN_MILLIS); + assertEquals(24, stats.size()); + + // try removing single bucket + stats.removeBucketsBefore(TEST_START + HOUR_IN_MILLIS); + assertEquals(23, stats.size()); + + // try removing multiple buckets + stats.removeBucketsBefore(TEST_START + (4 * HOUR_IN_MILLIS)); + assertEquals(20, stats.size()); + + // try removing all buckets + stats.removeBucketsBefore(TEST_START + YEAR_IN_MILLIS); + assertEquals(0, stats.size()); + } + + @Test + public void testTotalData() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + // record uniform data across day + stats.recordData(TEST_START, TEST_START + DAY_IN_MILLIS, 2400L, 4800L); + + // verify that total outside range is 0 + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START - DAY_IN_MILLIS, 0L, 0L); + + // verify total in first hour + assertValues(stats, TEST_START, TEST_START + HOUR_IN_MILLIS, 100L, 200L); + + // verify total across 1.5 hours + assertValues(stats, TEST_START, TEST_START + (long) (1.5 * HOUR_IN_MILLIS), 150L, 300L); + + // verify total beyond end + assertValues(stats, TEST_START + (23 * HOUR_IN_MILLIS), TEST_START + WEEK_IN_MILLIS, 100L, 200L); + + // verify everything total + assertValues(stats, TEST_START - WEEK_IN_MILLIS, TEST_START + WEEK_IN_MILLIS, 2400L, 4800L); + + } + + @Test + public void testFuzzing() throws Exception { + try { + // fuzzing with random events, looking for crashes + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final Random r = new Random(); + for (int i = 0; i < 500; i++) { + stats = new NetworkStatsHistory(r.nextLong()); + for (int j = 0; j < 10000; j++) { + if (r.nextBoolean()) { + // add range + final long start = r.nextLong(); + final long end = start + r.nextInt(); + entry.rxBytes = nextPositiveLong(r); + entry.rxPackets = nextPositiveLong(r); + entry.txBytes = nextPositiveLong(r); + entry.txPackets = nextPositiveLong(r); + entry.operations = nextPositiveLong(r); + stats.recordData(start, end, entry); + } else { + // trim something + stats.removeBucketsBefore(r.nextLong()); + } + } + assertConsistent(stats); + } + } catch (Throwable e) { + Log.e(TAG, String.valueOf(stats)); + throw new RuntimeException(e); + } + } + + private static long nextPositiveLong(Random r) { + final long value = r.nextLong(); + return value < 0 ? -value : value; + } + + @Test + public void testIgnoreFields() throws Exception { + final NetworkStatsHistory history = new NetworkStatsHistory( + MINUTE_IN_MILLIS, 0, FIELD_RX_BYTES | FIELD_TX_BYTES); + + history.recordData(0, MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + history.recordData(0, 2 * MINUTE_IN_MILLIS, + new NetworkStats.Entry(2L, 2L, 2L, 2L, 2L)); + + assertFullValues(history, UNKNOWN, 1026L, UNKNOWN, 2050L, UNKNOWN, UNKNOWN); + } + + @Test + public void testIgnoreFieldsRecordIn() throws Exception { + final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL); + final NetworkStatsHistory partial = new NetworkStatsHistory( + MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS); + + full.recordData(0, MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + partial.recordEntireHistory(full); + + assertFullValues(partial, UNKNOWN, UNKNOWN, 10L, UNKNOWN, UNKNOWN, 4L); + } + + @Test + public void testIgnoreFieldsRecordOut() throws Exception { + final NetworkStatsHistory full = new NetworkStatsHistory(MINUTE_IN_MILLIS, 0, FIELD_ALL); + final NetworkStatsHistory partial = new NetworkStatsHistory( + MINUTE_IN_MILLIS, 0, FIELD_RX_PACKETS | FIELD_OPERATIONS); + + partial.recordData(0, MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + full.recordEntireHistory(partial); + + assertFullValues(full, MINUTE_IN_MILLIS, 0L, 10L, 0L, 0L, 4L); + } + + @Test + public void testSerialize() throws Exception { + final NetworkStatsHistory before = new NetworkStatsHistory(MINUTE_IN_MILLIS, 40, FIELD_ALL); + before.recordData(0, 4 * MINUTE_IN_MILLIS, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 4L)); + before.recordData(DAY_IN_MILLIS, DAY_IN_MILLIS + MINUTE_IN_MILLIS, + new NetworkStats.Entry(10L, 20L, 30L, 40L, 50L)); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + before.writeToStream(new DataOutputStream(out)); + out.close(); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + final NetworkStatsHistory after = new NetworkStatsHistory(new DataInputStream(in)); + + // must have identical totals before and after + assertFullValues(before, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L); + assertFullValues(after, 5 * MINUTE_IN_MILLIS, 1034L, 30L, 2078L, 60L, 54L); + } + + @Test + public void testVarLong() throws Exception { + assertEquals(0L, performVarLong(0L)); + assertEquals(-1L, performVarLong(-1L)); + assertEquals(1024L, performVarLong(1024L)); + assertEquals(-1024L, performVarLong(-1024L)); + assertEquals(40 * MB_IN_BYTES, performVarLong(40 * MB_IN_BYTES)); + assertEquals(512 * GB_IN_BYTES, performVarLong(512 * GB_IN_BYTES)); + assertEquals(Long.MIN_VALUE, performVarLong(Long.MIN_VALUE)); + assertEquals(Long.MAX_VALUE, performVarLong(Long.MAX_VALUE)); + assertEquals(Long.MIN_VALUE + 40, performVarLong(Long.MIN_VALUE + 40)); + assertEquals(Long.MAX_VALUE - 40, performVarLong(Long.MAX_VALUE - 40)); + } + + @Test + public void testIndexBeforeAfter() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + final long FIRST_START = TEST_START; + final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS); + final long SECOND_START = TEST_START + WEEK_IN_MILLIS; + final long SECOND_END = SECOND_START + HOUR_IN_MILLIS; + final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS); + final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS); + + stats.recordData(FIRST_START, FIRST_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(SECOND_START, SECOND_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(THIRD_START, THIRD_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + // should have buckets: 2+1+2 + assertEquals(5, stats.size()); + + assertIndexBeforeAfter(stats, 0, 0, Long.MIN_VALUE); + assertIndexBeforeAfter(stats, 0, 1, FIRST_START); + assertIndexBeforeAfter(stats, 0, 1, FIRST_START + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 0, 2, FIRST_START + HOUR_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, FIRST_START + HOUR_IN_MILLIS + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, FIRST_END - MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, FIRST_END); + assertIndexBeforeAfter(stats, 1, 2, FIRST_END + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 2, SECOND_START - MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 1, 3, SECOND_START); + assertIndexBeforeAfter(stats, 2, 3, SECOND_END); + assertIndexBeforeAfter(stats, 2, 3, SECOND_END + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 2, 3, THIRD_START - MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 2, 4, THIRD_START); + assertIndexBeforeAfter(stats, 3, 4, THIRD_START + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 3, 4, THIRD_START + HOUR_IN_MILLIS); + assertIndexBeforeAfter(stats, 4, 4, THIRD_END); + assertIndexBeforeAfter(stats, 4, 4, THIRD_END + MINUTE_IN_MILLIS); + assertIndexBeforeAfter(stats, 4, 4, Long.MAX_VALUE); + } + + @Test + public void testIntersects() throws Exception { + final long BUCKET_SIZE = HOUR_IN_MILLIS; + stats = new NetworkStatsHistory(BUCKET_SIZE); + + final long FIRST_START = TEST_START; + final long FIRST_END = FIRST_START + (2 * HOUR_IN_MILLIS); + final long SECOND_START = TEST_START + WEEK_IN_MILLIS; + final long SECOND_END = SECOND_START + HOUR_IN_MILLIS; + final long THIRD_START = TEST_START + (2 * WEEK_IN_MILLIS); + final long THIRD_END = THIRD_START + (2 * HOUR_IN_MILLIS); + + stats.recordData(FIRST_START, FIRST_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(SECOND_START, SECOND_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + stats.recordData(THIRD_START, THIRD_END, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + assertFalse(stats.intersects(10, 20)); + assertFalse(stats.intersects(TEST_START + YEAR_IN_MILLIS, TEST_START + YEAR_IN_MILLIS + 1)); + assertFalse(stats.intersects(Long.MAX_VALUE, Long.MIN_VALUE)); + + assertTrue(stats.intersects(Long.MIN_VALUE, Long.MAX_VALUE)); + assertTrue(stats.intersects(10, TEST_START + YEAR_IN_MILLIS)); + assertTrue(stats.intersects(TEST_START, TEST_START)); + assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, TEST_START + DAY_IN_MILLIS + 1)); + assertTrue(stats.intersects(TEST_START + DAY_IN_MILLIS, Long.MAX_VALUE)); + assertTrue(stats.intersects(TEST_START + 1, Long.MAX_VALUE)); + + assertFalse(stats.intersects(Long.MIN_VALUE, TEST_START - 1)); + assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START)); + assertTrue(stats.intersects(Long.MIN_VALUE, TEST_START + 1)); + } + + @Test + public void testSetValues() throws Exception { + stats = new NetworkStatsHistory(HOUR_IN_MILLIS); + stats.recordData(TEST_START, TEST_START + 1, + new NetworkStats.Entry(1024L, 10L, 2048L, 20L, 2L)); + + assertEquals(1024L + 2048L, stats.getTotalBytes()); + + final NetworkStatsHistory.Entry entry = stats.getValues(0, null); + entry.rxBytes /= 2; + entry.txBytes *= 2; + stats.setValues(0, entry); + + assertEquals(512L + 4096L, stats.getTotalBytes()); + } + + private static void assertIndexBeforeAfter( + NetworkStatsHistory stats, int before, int after, long time) { + assertEquals("unexpected before", before, stats.getIndexBefore(time)); + assertEquals("unexpected after", after, stats.getIndexAfter(time)); + } + + private static long performVarLong(long before) throws Exception { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeVarLong(new DataOutputStream(out), before); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + return readVarLong(new DataInputStream(in)); + } + + private static void assertConsistent(NetworkStatsHistory stats) { + // verify timestamps are monotonic + long lastStart = Long.MIN_VALUE; + NetworkStatsHistory.Entry entry = null; + for (int i = 0; i < stats.size(); i++) { + entry = stats.getValues(i, entry); + assertTrue(lastStart < entry.bucketStart); + lastStart = entry.bucketStart; + } + } + + private static void assertValues( + NetworkStatsHistory stats, int index, long rxBytes, long txBytes) { + final NetworkStatsHistory.Entry entry = stats.getValues(index, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + } + + private static void assertValues( + NetworkStatsHistory stats, long start, long end, long rxBytes, long txBytes) { + final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + } + + private static void assertValues(NetworkStatsHistory stats, int index, long activeTime, + long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { + final NetworkStatsHistory.Entry entry = stats.getValues(index, null); + assertEquals("unexpected activeTime", activeTime, entry.activeTime); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } + + private static void assertFullValues(NetworkStatsHistory stats, long activeTime, long rxBytes, + long rxPackets, long txBytes, long txPackets, long operations) { + assertValues(stats, Long.MIN_VALUE, Long.MAX_VALUE, activeTime, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + private static void assertValues(NetworkStatsHistory stats, long start, long end, + long activeTime, long rxBytes, long rxPackets, long txBytes, long txPackets, + long operations) { + final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null); + assertEquals("unexpected activeTime", activeTime, entry.activeTime); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } +} diff --git a/tests/unit/java/android/net/NetworkStatsTest.java b/tests/unit/java/android/net/NetworkStatsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..23d5a7e5d5f89092e8ef16d8093542b5fd7f6ca7 --- /dev/null +++ b/tests/unit/java/android/net/NetworkStatsTest.java @@ -0,0 +1,1024 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.INTERFACES_ALL; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DBG_VPN_IN; +import static android.net.NetworkStats.SET_DBG_VPN_OUT; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_ALL; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.os.Process; +import android.util.ArrayMap; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.google.android.collect.Sets; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.HashSet; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsTest { + + private static final String TEST_IFACE = "test0"; + private static final String TEST_IFACE2 = "test2"; + private static final int TEST_UID = 1001; + private static final long TEST_START = 1194220800000L; + + @Test + public void testFindIndex() throws Exception { + final NetworkStats stats = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 0L, 0L, 10) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12); + + assertEquals(4, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_YES, DEFAULT_NETWORK_YES)); + assertEquals(3, stats.findIndex(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + assertEquals(2, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_NO, DEFAULT_NETWORK_YES)); + assertEquals(1, stats.findIndex(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + assertEquals(0, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_YES)); + assertEquals(-1, stats.findIndex(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + assertEquals(-1, stats.findIndex(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO)); + } + + @Test + public void testFindIndexHinted() { + final NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 0L, 0L, 10) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1024L, 8L, 0L, 0L, 10) + .insertEntry(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 1024L, 8L, 11) + .insertEntry(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 12); + + // verify that we correctly find across regardless of hinting + for (int hint = 0; hint < stats.size(); hint++) { + assertEquals(0, stats.findIndexHinted(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(1, stats.findIndexHinted(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, hint)); + assertEquals(2, stats.findIndexHinted(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(3, stats.findIndexHinted(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, hint)); + assertEquals(4, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(5, stats.findIndexHinted(TEST_IFACE2, 101, SET_DEFAULT, 0xF00D, + METERED_YES, ROAMING_NO, DEFAULT_NETWORK_NO, hint)); + assertEquals(6, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(7, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_YES, DEFAULT_NETWORK_NO, hint)); + assertEquals(-1, stats.findIndexHinted(TEST_IFACE, 6, SET_DEFAULT, TAG_NONE, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_YES, hint)); + assertEquals(-1, stats.findIndexHinted(TEST_IFACE2, 102, SET_DEFAULT, TAG_NONE, + METERED_YES, ROAMING_YES, DEFAULT_NETWORK_YES, hint)); + } + } + + @Test + public void testAddEntryGrow() throws Exception { + final NetworkStats stats = new NetworkStats(TEST_START, 4); + + assertEquals(0, stats.size()); + assertEquals(4, stats.internalSize()); + + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 1L, 2L, 2L, 3); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 2L, 2L, 2L, 2L, 4); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 3L, 3L, 2L, 2L, 5); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_NO, 3L, 3L, 2L, 2L, 5); + + assertEquals(4, stats.size()); + assertEquals(4, stats.internalSize()); + + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4L, 40L, 4L, 40L, 7); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 5L, 50L, 4L, 40L, 8); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 6L, 60L, 5L, 50L, 10); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 7L, 70L, 5L, 50L, 11); + stats.insertEntry(TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_NO, 7L, 70L, 5L, 50L, 11); + + assertEquals(9, stats.size()); + assertTrue(stats.internalSize() >= 9); + + assertValues(stats, 0, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 1L, 2L, 2L, 3); + assertValues(stats, 1, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 2L, 2L, 2L, 2L, 4); + assertValues(stats, 2, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 3L, 3L, 2L, 2L, 5); + assertValues(stats, 3, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_YES, DEFAULT_NETWORK_NO, 3L, 3L, 2L, 2L, 5); + assertValues(stats, 4, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4L, 40L, 4L, 40L, 7); + assertValues(stats, 5, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 5L, 50L, 4L, 40L, 8); + assertValues(stats, 6, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 6L, 60L, 5L, 50L, 10); + assertValues(stats, 7, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 7L, 70L, 5L, 50L, 11); + assertValues(stats, 8, TEST_IFACE, TEST_UID, SET_DEFAULT, TAG_NONE, METERED_YES, + ROAMING_YES, DEFAULT_NETWORK_NO, 7L, 70L, 5L, 50L, 11); + } + + @Test + public void testCombineExisting() throws Exception { + final NetworkStats stats = new NetworkStats(TEST_START, 10); + + stats.insertEntry(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 10); + stats.insertEntry(TEST_IFACE, 1001, SET_DEFAULT, 0xff, 128L, 1L, 128L, 1L, 2); + stats.combineValues(TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, -128L, -1L, + -128L, -1L, -1); + + assertValues(stats, 0, TEST_IFACE, 1001, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 384L, 3L, 128L, 1L, 9); + assertValues(stats, 1, TEST_IFACE, 1001, SET_DEFAULT, 0xff, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 1L, 128L, 1L, 2); + + // now try combining that should create row + stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3); + assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 1L, 128L, 1L, 3); + stats.combineValues(TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 3); + assertValues(stats, 2, TEST_IFACE, 5005, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 256L, 2L, 256L, 2L, 6); + } + + @Test + public void testSubtractIdenticalData() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats after = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats result = after.subtract(before); + + // identical data should result in zero delta + assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + } + + @Test + public void testSubtractIdenticalRows() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats after = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1025L, 9L, 2L, 1L, 15) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 3L, 1L, 1028L, 9L, 20); + + final NetworkStats result = after.subtract(before); + + // expect delta between measurements + assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1L, 1L, 2L, 1L, 4); + assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 3L, 1L, 4L, 1L, 8); + } + + @Test + public void testSubtractNewRows() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12); + + final NetworkStats after = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 1024L, 8L, 0L, 0L, 11) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 1024L, 8L, 12) + .insertEntry(TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, 1024L, 8L, 1024L, 8L, 20); + + final NetworkStats result = after.subtract(before); + + // its okay to have new rows + assertValues(result, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + assertValues(result, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0); + assertValues(result, 2, TEST_IFACE, 102, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1024L, 8L, 1024L, 8L, 20); + } + + @Test + public void testSubtractMissingRows() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 1024L, 0L, 0L, 0L, 0) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2048L, 0L, 0L, 0L, 0); + + final NetworkStats after = new NetworkStats(TEST_START, 1) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 2049L, 2L, 3L, 4L, 0); + + final NetworkStats result = after.subtract(before); + + // should silently drop omitted rows + assertEquals(1, result.size()); + assertValues(result, 0, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 2L, 3L, 4L, 0); + assertEquals(4L, result.getTotalBytes()); + } + + @Test + public void testTotalBytes() throws Exception { + final NetworkStats iface = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, UID_ALL, SET_DEFAULT, TAG_NONE, 128L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, 256L, 0L, 0L, 0L, 0L); + assertEquals(384L, iface.getTotalBytes()); + + final NetworkStats uidSet = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_FOREGROUND, TAG_NONE, 32L, 0L, 0L, 0L, 0L); + assertEquals(96L, uidSet.getTotalBytes()); + + final NetworkStats uidTag = new NetworkStats(TEST_START, 6) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 16L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L); + assertEquals(64L, uidTag.getTotalBytes()); + + final NetworkStats uidMetered = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + assertEquals(96L, uidMetered.getTotalBytes()); + + final NetworkStats uidRoaming = new NetworkStats(TEST_START, 3) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + assertEquals(96L, uidRoaming.getTotalBytes()); + } + + @Test + public void testGroupedByIfaceEmpty() throws Exception { + final NetworkStats uidStats = new NetworkStats(TEST_START, 3); + final NetworkStats grouped = uidStats.groupedByIface(); + + assertEquals(0, uidStats.size()); + assertEquals(0, grouped.size()); + } + + @Test + public void testGroupedByIfaceAll() throws Exception { + final NetworkStats uidStats = new NetworkStats(TEST_START, 3) + .insertEntry(IFACE_ALL, 100, SET_ALL, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L) + .insertEntry(IFACE_ALL, 101, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 2L, 20L) + .insertEntry(IFACE_ALL, 101, SET_ALL, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L); + final NetworkStats grouped = uidStats.groupedByIface(); + + assertEquals(3, uidStats.size()); + assertEquals(1, grouped.size()); + + assertValues(grouped, 0, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 6L, 0L); + } + + @Test + public void testGroupedByIface() throws Exception { + final NetworkStats uidStats = new NetworkStats(TEST_START, 7) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 4L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L); + + final NetworkStats grouped = uidStats.groupedByIface(); + + assertEquals(7, uidStats.size()); + + assertEquals(2, grouped.size()); + assertValues(grouped, 0, TEST_IFACE, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 384L, 24L, 0L, 2L, 0L); + assertValues(grouped, 1, TEST_IFACE2, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 1024L, 64L, 0L, 0L, 0L); + } + + @Test + public void testAddAllValues() { + final NetworkStats first = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + + final NetworkStats second = new NetworkStats(TEST_START, 2) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 32L, 0L, 0L, 0L, 0L); + + first.combineAllValues(second); + + assertEquals(4, first.size()); + assertValues(first, 0, TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 0L, 0L, 0L, 0L); + assertValues(first, 1, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L); + assertValues(first, 2, TEST_IFACE, 100, SET_FOREGROUND, TAG_NONE, METERED_YES, ROAMING_YES, + DEFAULT_NETWORK_YES, 64L, 0L, 0L, 0L, 0L); + assertValues(first, 3, TEST_IFACE2, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 0L, 0L, 0L, 0L); + } + + @Test + public void testGetTotal() { + final NetworkStats stats = new NetworkStats(TEST_START, 7) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 4L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 512L,32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_YES, + DEFAULT_NETWORK_NO, 128L, 8L, 0L, 0L, 0L); + + assertValues(stats.getTotal(null), 1408L, 88L, 0L, 2L, 20L); + assertValues(stats.getTotal(null, 100), 1280L, 80L, 0L, 2L, 20L); + assertValues(stats.getTotal(null, 101), 128L, 8L, 0L, 0L, 0L); + + final HashSet<String> ifaces = Sets.newHashSet(); + assertValues(stats.getTotal(null, ifaces), 0L, 0L, 0L, 0L, 0L); + + ifaces.add(TEST_IFACE2); + assertValues(stats.getTotal(null, ifaces), 1024L, 64L, 0L, 0L, 0L); + } + + @Test + public void testRemoveUids() throws Exception { + final NetworkStats before = new NetworkStats(TEST_START, 3); + + // Test 0 item stats. + NetworkStats after = before.clone(); + after.removeUids(new int[0]); + assertEquals(0, after.size()); + after.removeUids(new int[] {100}); + assertEquals(0, after.size()); + + // Test 1 item stats. + before.insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, 1L, 128L, 0L, 2L, 20L); + after = before.clone(); + after.removeUids(new int[0]); + assertEquals(1, after.size()); + assertValues(after, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + after.removeUids(new int[] {99}); + assertEquals(0, after.size()); + + // Append remaining test items. + before.insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 2L, 64L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 4L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 16L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 16L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 32L, 4L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 64L, 2L, 0L, 0L, 0L); + assertEquals(7, before.size()); + + // Test remove with empty uid list. + after = before.clone(); + after.removeUids(new int[0]); + assertValues(after.getTotalIncludingTags(null), 127L, 254L, 0L, 4L, 40L); + + // Test remove uids don't exist in stats. + after.removeUids(new int[] {98, 0, Integer.MIN_VALUE, Integer.MAX_VALUE}); + assertValues(after.getTotalIncludingTags(null), 127L, 254L, 0L, 4L, 40L); + + // Test remove all uids. + after.removeUids(new int[] {99, 100, 100, 101}); + assertEquals(0, after.size()); + + // Test remove in the middle. + after = before.clone(); + after.removeUids(new int[] {100}); + assertEquals(3, after.size()); + assertValues(after, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + assertValues(after, 1, TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 32L, 4L, 0L, 0L, 0L); + assertValues(after, 2, TEST_IFACE, 101, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 64L, 2L, 0L, 0L, 0L); + } + + @Test + public void testRemoveEmptyEntries() throws Exception { + // Test empty stats. + final NetworkStats statsEmpty = new NetworkStats(TEST_START, 3); + assertEquals(0, statsEmpty.removeEmptyEntries().size()); + + // Test stats with non-zero entry. + final NetworkStats statsNonZero = new NetworkStats(TEST_START, 1) + .insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + assertEquals(1, statsNonZero.size()); + final NetworkStats expectedNonZero = statsNonZero.removeEmptyEntries(); + assertEquals(1, expectedNonZero.size()); + assertValues(expectedNonZero, 0, TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1L, 128L, 0L, 2L, 20L); + + // Test stats with empty entry. + final NetworkStats statsZero = new NetworkStats(TEST_START, 1) + .insertEntry(TEST_IFACE, 99, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + assertEquals(1, statsZero.size()); + final NetworkStats expectedZero = statsZero.removeEmptyEntries(); + assertEquals(1, statsZero.size()); // Assert immutable. + assertEquals(0, expectedZero.size()); + + // Test stats with multiple entries. + final NetworkStats statsMultiple = new NetworkStats(TEST_START, 0) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 2L, 64L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 4L, 32L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, 0xF00D, 8L, 0L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE2, 100, SET_FOREGROUND, TAG_NONE, 0L, 8L, 0L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 0L, 0L, 4L, 0L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 2L, 0L) + .insertEntry(TEST_IFACE, 101, SET_DEFAULT, 0xF00D, 0L, 0L, 0L, 0L, 1L); + assertEquals(9, statsMultiple.size()); + final NetworkStats expectedMultiple = statsMultiple.removeEmptyEntries(); + assertEquals(9, statsMultiple.size()); // Assert immutable. + assertEquals(7, expectedMultiple.size()); + assertValues(expectedMultiple.getTotalIncludingTags(null), 14L, 104L, 4L, 4L, 21L); + + // Test stats with multiple empty entries. + assertEquals(statsMultiple.size(), statsMultiple.subtract(statsMultiple).size()); + assertEquals(0, statsMultiple.subtract(statsMultiple).removeEmptyEntries().size()); + } + + @Test + public void testClone() throws Exception { + final NetworkStats original = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L); + + // make clone and mutate original + final NetworkStats clone = original.clone(); + original.insertEntry(TEST_IFACE, 101, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 0L, 0L); + + assertEquals(3, original.size()); + assertEquals(2, clone.size()); + + assertEquals(128L + 512L + 128L, original.getTotalBytes()); + assertEquals(128L + 512L, clone.getTotalBytes()); + } + + @Test + public void testAddWhenEmpty() throws Exception { + final NetworkStats red = new NetworkStats(TEST_START, -1); + final NetworkStats blue = new NetworkStats(TEST_START, 5) + .insertEntry(TEST_IFACE, 100, SET_DEFAULT, TAG_NONE, 128L, 8L, 0L, 2L, 20L) + .insertEntry(TEST_IFACE2, 100, SET_DEFAULT, TAG_NONE, 512L, 32L, 0L, 0L, 0L); + + // We're mostly checking that we don't crash + red.combineAllValues(blue); + } + + @Test + public void testMigrateTun() throws Exception { + final int tunUid = 10030; + final String tunIface = "tun0"; + final String underlyingIface = "wlan0"; + final int testTag1 = 8888; + NetworkStats delta = new NetworkStats(TEST_START, 17) + .insertEntry(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 39605L, 46L, 12259L, 55L, 0L) + .insertEntry(tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L) + .insertEntry(tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 72667L, 197L, 43909L, 241L, 0L) + .insertEntry(tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 9297L, 17L, 4128L, 21L, 0L) + // VPN package also uses some traffic through unprotected network. + .insertEntry(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4983L, 10L, 1801L, 12L, 0L) + .insertEntry(tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L) + // Tag entries + .insertEntry(tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 21691L, 41L, 13820L, 51L, 0L) + .insertEntry(tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1281L, 2L, 665L, 2L, 0L) + // Irrelevant entries + .insertEntry(TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1685L, 5L, 2070L, 6L, 0L) + // Underlying Iface entries + .insertEntry(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 5178L, 8L, 2139L, 11L, 0L) + .insertEntry(underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L) + .insertEntry(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 149873L, 287L, 59217L /* smaller than sum(tun0) */, + 299L /* smaller than sum(tun0) */, 0L) + .insertEntry(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + + delta.migrateTun(tunUid, tunIface, Arrays.asList(underlyingIface)); + assertEquals(20, delta.size()); + + // tunIface and TEST_IFACE entries are not changed. + assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 39605L, 46L, 12259L, 55L, 0L); + assertValues(delta, 1, tunIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + assertValues(delta, 2, tunIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 72667L, 197L, 43909L, 241L, 0L); + assertValues(delta, 3, tunIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 9297L, 17L, 4128L, 21L, 0L); + assertValues(delta, 4, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 4983L, 10L, 1801L, 12L, 0L); + assertValues(delta, 5, tunIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + assertValues(delta, 6, tunIface, 10120, SET_DEFAULT, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 21691L, 41L, 13820L, 51L, 0L); + assertValues(delta, 7, tunIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1281L, 2L, 665L, 2L, 0L); + assertValues(delta, 8, TEST_IFACE, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1685L, 5L, 2070L, 6L, 0L); + + // Existing underlying Iface entries are updated + assertValues(delta, 9, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 44783L, 54L, 14178L, 62L, 0L); + assertValues(delta, 10, underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + + // VPN underlying Iface entries are updated + assertValues(delta, 11, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 28304L, 27L, 1L, 2L, 0L); + assertValues(delta, 12, underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); + + // New entries are added for new application's underlying Iface traffic + assertContains(delta, underlyingIface, 10120, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 72667L, 197L, 43123L, 227L, 0L); + assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 9297L, 17L, 4054, 19L, 0L); + assertContains(delta, underlyingIface, 10120, SET_DEFAULT, testTag1, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 21691L, 41L, 13572L, 48L, 0L); + assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, testTag1, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 1281L, 2L, 653L, 1L, 0L); + + // New entries are added for debug purpose + assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 39605L, 46L, 12039, 51, 0); + assertContains(delta, underlyingIface, 10120, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 81964, 214, 47177, 246, 0); + assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 121569, 260, 59216, 297, 0); + + } + + // Tests a case where all of the data received by the tun0 interface is echo back into the tun0 + // interface by the vpn app before it's sent out of the underlying interface. The VPN app should + // not be charged for the echoed data but it should still be charged for any extra data it sends + // via the underlying interface. + @Test + public void testMigrateTun_VpnAsLoopback() { + final int tunUid = 10030; + final String tunIface = "tun0"; + final String underlyingIface = "wlan0"; + NetworkStats delta = new NetworkStats(TEST_START, 9) + // 2 different apps sent/receive data via tun0. + .insertEntry(tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L) + .insertEntry(tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L) + // VPN package resends data through the tunnel (with exaggerated overhead) + .insertEntry(tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 240000, 100L, 120000L, 60L, 0L) + // 1 app already has some traffic on the underlying interface, the other doesn't yet + .insertEntry(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 1000L, 10L, 2000L, 20L, 0L) + // Traffic through the underlying interface via the vpn app. + // This test should redistribute this data correctly. + .insertEntry(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 75500L, 37L, 130000L, 70L, 0L); + + delta.migrateTun(tunUid, tunIface, Arrays.asList(underlyingIface)); + assertEquals(9, delta.size()); + + // tunIface entries should not be changed. + assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + assertValues(delta, 1, tunIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L); + assertValues(delta, 2, tunIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 240000L, 100L, 120000L, 60L, 0L); + + // Existing underlying Iface entries are updated + assertValues(delta, 3, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 51000L, 35L, 102000L, 70L, 0L); + + // VPN underlying Iface entries are updated + assertValues(delta, 4, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 25000L, 10L, 29800L, 15L, 0L); + + // New entries are added for new application's underlying Iface traffic + assertContains(delta, underlyingIface, 20100, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 500L, 2L, 200L, 5L, 0L); + + // New entries are added for debug purpose + assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + assertContains(delta, underlyingIface, 20100, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, 500, 2L, 200L, 5L, 0L); + assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 50500L, 27L, 100200L, 55, 0); + } + + @Test + public void testFilter_NoFilter() { + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3); + + stats.filter(UID_ALL, INTERFACES_ALL, TAG_ALL); + assertEquals(3, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry2, stats.getValues(1, null)); + assertEquals(entry3, stats.getValues(2, null)); + } + + @Test + public void testFilter_UidFilter() { + final int testUid = 10101; + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", testUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", testUid, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3); + + stats.filter(testUid, INTERFACES_ALL, TAG_ALL); + assertEquals(2, stats.size()); + assertEquals(entry2, stats.getValues(0, null)); + assertEquals(entry3, stats.getValues(1, null)); + } + + @Test + public void testFilter_InterfaceFilter() { + final String testIf1 = "testif1"; + final String testIf2 = "testif2"; + NetworkStats.Entry entry1 = new NetworkStats.Entry( + testIf1, 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "otherif", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + testIf1, 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry4 = new NetworkStats.Entry( + testIf2, 10101, SET_DEFAULT, 123, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 4) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3) + .insertEntry(entry4); + + stats.filter(UID_ALL, new String[] { testIf1, testIf2 }, TAG_ALL); + assertEquals(3, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry3, stats.getValues(1, null)); + assertEquals(entry4, stats.getValues(2, null)); + } + + @Test + public void testFilter_EmptyInterfaceFilter() { + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "if1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "if2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2); + + stats.filter(UID_ALL, new String[] { }, TAG_ALL); + assertEquals(0, stats.size()); + } + + @Test + public void testFilter_TagFilter() { + final int testTag = 123; + final int otherTag = 456; + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, testTag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, testTag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, otherTag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3); + + stats.filter(UID_ALL, INTERFACES_ALL, testTag); + assertEquals(2, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry2, stats.getValues(1, null)); + } + + @Test + public void testFilterDebugEntries() { + NetworkStats.Entry entry1 = new NetworkStats.Entry( + "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry2 = new NetworkStats.Entry( + "test2", 10101, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry3 = new NetworkStats.Entry( + "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats.Entry entry4 = new NetworkStats.Entry( + "test2", 10101, SET_DBG_VPN_OUT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L); + + NetworkStats stats = new NetworkStats(TEST_START, 4) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3) + .insertEntry(entry4); + + stats.filterDebugEntries(); + + assertEquals(2, stats.size()); + assertEquals(entry1, stats.getValues(0, null)); + assertEquals(entry3, stats.getValues(1, null)); + } + + @Test + public void testApply464xlatAdjustments() { + final String v4Iface = "v4-wlan0"; + final String baseIface = "wlan0"; + final String otherIface = "other"; + final int appUid = 10001; + final int rootUid = Process.ROOT_UID; + ArrayMap<String, String> stackedIface = new ArrayMap<>(); + stackedIface.put(v4Iface, baseIface); + + // Ipv4 traffic sent/received by an app on stacked interface. + final NetworkStats.Entry appEntry = new NetworkStats.Entry( + v4Iface, appUid, SET_DEFAULT, TAG_NONE, + 30501490 /* rxBytes */, + 22401 /* rxPackets */, + 876235 /* txBytes */, + 13805 /* txPackets */, + 0 /* operations */); + + // Traffic measured for the root uid on the base interface. + final NetworkStats.Entry rootUidEntry = new NetworkStats.Entry( + baseIface, rootUid, SET_DEFAULT, TAG_NONE, + 163577 /* rxBytes */, + 187 /* rxPackets */, + 17607 /* txBytes */, + 97 /* txPackets */, + 0 /* operations */); + + final NetworkStats.Entry otherEntry = new NetworkStats.Entry( + otherIface, appUid, SET_DEFAULT, TAG_NONE, + 2600 /* rxBytes */, + 2 /* rxPackets */, + 3800 /* txBytes */, + 3 /* txPackets */, + 0 /* operations */); + + final NetworkStats stats = new NetworkStats(TEST_START, 3) + .insertEntry(appEntry) + .insertEntry(rootUidEntry) + .insertEntry(otherEntry); + + stats.apply464xlatAdjustments(stackedIface); + + assertEquals(3, stats.size()); + final NetworkStats.Entry expectedAppUid = new NetworkStats.Entry( + v4Iface, appUid, SET_DEFAULT, TAG_NONE, + 30949510, + 22401, + 1152335, + 13805, + 0); + final NetworkStats.Entry expectedRootUid = new NetworkStats.Entry( + baseIface, 0, SET_DEFAULT, TAG_NONE, + 163577, + 187, + 17607, + 97, + 0); + assertEquals(expectedAppUid, stats.getValues(0, null)); + assertEquals(expectedRootUid, stats.getValues(1, null)); + assertEquals(otherEntry, stats.getValues(2, null)); + } + + @Test + public void testApply464xlatAdjustments_noStackedIface() { + NetworkStats.Entry firstEntry = new NetworkStats.Entry( + "if1", 10002, SET_DEFAULT, TAG_NONE, + 2600 /* rxBytes */, + 2 /* rxPackets */, + 3800 /* txBytes */, + 3 /* txPackets */, + 0 /* operations */); + NetworkStats.Entry secondEntry = new NetworkStats.Entry( + "if2", 10002, SET_DEFAULT, TAG_NONE, + 5000 /* rxBytes */, + 3 /* rxPackets */, + 6000 /* txBytes */, + 4 /* txPackets */, + 0 /* operations */); + + NetworkStats stats = new NetworkStats(TEST_START, 2) + .insertEntry(firstEntry) + .insertEntry(secondEntry); + + // Empty map: no adjustment + stats.apply464xlatAdjustments(new ArrayMap<>()); + + assertEquals(2, stats.size()); + assertEquals(firstEntry, stats.getValues(0, null)); + assertEquals(secondEntry, stats.getValues(1, null)); + } + + private static void assertContains(NetworkStats stats, String iface, int uid, int set, + int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + int index = stats.findIndex(iface, uid, set, tag, metered, roaming, defaultNetwork); + assertTrue(index != -1); + assertValues(stats, index, iface, uid, set, tag, metered, roaming, defaultNetwork, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private static void assertValues(NetworkStats stats, int index, String iface, int uid, int set, + int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + final NetworkStats.Entry entry = stats.getValues(index, null); + assertValues(entry, iface, uid, set, tag, metered, roaming, defaultNetwork); + assertValues(entry, rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private static void assertValues( + NetworkStats.Entry entry, String iface, int uid, int set, int tag, int metered, + int roaming, int defaultNetwork) { + assertEquals(iface, entry.iface); + assertEquals(uid, entry.uid); + assertEquals(set, entry.set); + assertEquals(tag, entry.tag); + assertEquals(metered, entry.metered); + assertEquals(roaming, entry.roaming); + assertEquals(defaultNetwork, entry.defaultNetwork); + } + + private static void assertValues(NetworkStats.Entry entry, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + assertEquals(rxBytes, entry.rxBytes); + assertEquals(rxPackets, entry.rxPackets); + assertEquals(txBytes, entry.txBytes); + assertEquals(txPackets, entry.txPackets); + assertEquals(operations, entry.operations); + } + +} diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb39a0c819f2afdcaba6c1ba0be5a6ee8f868952 --- /dev/null +++ b/tests/unit/java/android/net/NetworkTemplateTest.kt @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.content.Context +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.NetworkIdentity.SUBTYPE_COMBINED +import android.net.NetworkIdentity.OEM_NONE +import android.net.NetworkIdentity.OEM_PAID +import android.net.NetworkIdentity.OEM_PRIVATE +import android.net.NetworkIdentity.buildNetworkIdentity +import android.net.NetworkStats.DEFAULT_NETWORK_ALL +import android.net.NetworkStats.METERED_ALL +import android.net.NetworkStats.ROAMING_ALL +import android.net.NetworkTemplate.MATCH_MOBILE +import android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD +import android.net.NetworkTemplate.MATCH_WIFI +import android.net.NetworkTemplate.MATCH_WIFI_WILDCARD +import android.net.NetworkTemplate.WIFI_NETWORKID_ALL +import android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA +import android.net.NetworkTemplate.NETWORK_TYPE_ALL +import android.net.NetworkTemplate.OEM_MANAGED_ALL +import android.net.NetworkTemplate.OEM_MANAGED_NO +import android.net.NetworkTemplate.OEM_MANAGED_YES +import android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT +import android.net.NetworkTemplate.buildTemplateWifi +import android.net.NetworkTemplate.buildTemplateWifiWildcard +import android.net.NetworkTemplate.buildTemplateCarrierMetered +import android.net.NetworkTemplate.buildTemplateMobileWithRatType +import android.telephony.TelephonyManager +import com.android.testutils.assertParcelSane +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +private const val TEST_IMSI1 = "imsi1" +private const val TEST_IMSI2 = "imsi2" +private const val TEST_SSID1 = "ssid1" +private const val TEST_SSID2 = "ssid2" + +@RunWith(JUnit4::class) +class NetworkTemplateTest { + private val mockContext = mock(Context::class.java) + + private fun buildMobileNetworkState(subscriberId: String): NetworkStateSnapshot = + buildNetworkState(TYPE_MOBILE, subscriberId = subscriberId) + private fun buildWifiNetworkState(subscriberId: String?, ssid: String?): NetworkStateSnapshot = + buildNetworkState(TYPE_WIFI, subscriberId = subscriberId, ssid = ssid) + + private fun buildNetworkState( + type: Int, + subscriberId: String? = null, + ssid: String? = null, + oemManaged: Int = OEM_NONE, + metered: Boolean = true + ): NetworkStateSnapshot { + val lp = LinkProperties() + val caps = NetworkCapabilities().apply { + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !metered) + setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true) + setSSID(ssid) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID, + (oemManaged and OEM_PAID) == OEM_PAID) + setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, + (oemManaged and OEM_PRIVATE) == OEM_PRIVATE) + } + return NetworkStateSnapshot(mock(Network::class.java), caps, lp, subscriberId, type) + } + + private fun NetworkTemplate.assertMatches(ident: NetworkIdentity) = + assertTrue(matches(ident), "$this does not match $ident") + + private fun NetworkTemplate.assertDoesNotMatch(ident: NetworkIdentity) = + assertFalse(matches(ident), "$this should match $ident") + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testWifiWildcardMatches() { + val templateWifiWildcard = buildTemplateWifiWildcard() + + val identMobileImsi1 = buildNetworkIdentity(mockContext, + buildMobileNetworkState(TEST_IMSI1), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifiImsiNullSsid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + val identWifiImsi1Ssid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0) + + templateWifiWildcard.assertDoesNotMatch(identMobileImsi1) + templateWifiWildcard.assertMatches(identWifiImsiNullSsid1) + templateWifiWildcard.assertMatches(identWifiImsi1Ssid1) + } + + @Test + fun testWifiMatches() { + val templateWifiSsid1 = buildTemplateWifi(TEST_SSID1) + val templateWifiSsid1ImsiNull = buildTemplateWifi(TEST_SSID1, null) + val templateWifiSsid1Imsi1 = buildTemplateWifi(TEST_SSID1, TEST_IMSI1) + val templateWifiSsidAllImsi1 = buildTemplateWifi(WIFI_NETWORKID_ALL, TEST_IMSI1) + + val identMobile1 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI1), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifiImsiNullSsid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + val identWifiImsi1Ssid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0) + val identWifiImsi2Ssid1 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI2, TEST_SSID1), true, 0) + val identWifiImsi1Ssid2 = buildNetworkIdentity( + mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID2), true, 0) + + // Verify that template with SSID only matches any subscriberId and specific SSID. + templateWifiSsid1.assertDoesNotMatch(identMobile1) + templateWifiSsid1.assertMatches(identWifiImsiNullSsid1) + templateWifiSsid1.assertMatches(identWifiImsi1Ssid1) + templateWifiSsid1.assertMatches(identWifiImsi2Ssid1) + templateWifiSsid1.assertDoesNotMatch(identWifiImsi1Ssid2) + + // Verify that template with SSID1 and null imsi matches any network with + // SSID1 and null imsi. + templateWifiSsid1ImsiNull.assertDoesNotMatch(identMobile1) + templateWifiSsid1ImsiNull.assertMatches(identWifiImsiNullSsid1) + templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi1Ssid1) + templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi2Ssid1) + templateWifiSsid1ImsiNull.assertDoesNotMatch(identWifiImsi1Ssid2) + + // Verify that template with SSID1 and imsi1 matches any network with + // SSID1 and imsi1. + templateWifiSsid1Imsi1.assertDoesNotMatch(identMobile1) + templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsiNullSsid1) + templateWifiSsid1Imsi1.assertMatches(identWifiImsi1Ssid1) + templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsi2Ssid1) + templateWifiSsid1Imsi1.assertDoesNotMatch(identWifiImsi1Ssid2) + + // Verify that template with SSID all and imsi1 matches any network with + // any SSID and imsi1. + templateWifiSsidAllImsi1.assertDoesNotMatch(identMobile1) + templateWifiSsidAllImsi1.assertDoesNotMatch(identWifiImsiNullSsid1) + templateWifiSsidAllImsi1.assertMatches(identWifiImsi1Ssid1) + templateWifiSsidAllImsi1.assertDoesNotMatch(identWifiImsi2Ssid1) + templateWifiSsidAllImsi1.assertMatches(identWifiImsi1Ssid2) + } + + @Test + fun testCarrierMeteredMatches() { + val templateCarrierImsi1Metered = buildTemplateCarrierMetered(TEST_IMSI1) + + val mobileImsi1 = buildMobileNetworkState(TEST_IMSI1) + val mobileImsi1Unmetered = buildNetworkState(TYPE_MOBILE, TEST_IMSI1, null /* ssid */, + OEM_NONE, false /* metered */) + val mobileImsi2 = buildMobileNetworkState(TEST_IMSI2) + val wifiSsid1 = buildWifiNetworkState(null /* subscriberId */, TEST_SSID1) + val wifiImsi1Ssid1 = buildWifiNetworkState(TEST_IMSI1, TEST_SSID1) + val wifiImsi1Ssid1Unmetered = buildNetworkState(TYPE_WIFI, TEST_IMSI1, TEST_SSID1, + OEM_NONE, false /* metered */) + + val identMobileImsi1Metered = buildNetworkIdentity(mockContext, + mobileImsi1, false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS) + val identMobileImsi1Unmetered = buildNetworkIdentity(mockContext, + mobileImsi1Unmetered, false /* defaultNetwork */, + TelephonyManager.NETWORK_TYPE_UMTS) + val identMobileImsi2Metered = buildNetworkIdentity(mockContext, + mobileImsi2, false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifiSsid1Metered = buildNetworkIdentity( + mockContext, wifiSsid1, true /* defaultNetwork */, 0 /* subType */) + val identCarrierWifiImsi1Metered = buildNetworkIdentity( + mockContext, wifiImsi1Ssid1, true /* defaultNetwork */, 0 /* subType */) + val identCarrierWifiImsi1NonMetered = buildNetworkIdentity(mockContext, + wifiImsi1Ssid1Unmetered, true /* defaultNetwork */, 0 /* subType */) + + templateCarrierImsi1Metered.assertMatches(identMobileImsi1Metered) + templateCarrierImsi1Metered.assertDoesNotMatch(identMobileImsi1Unmetered) + templateCarrierImsi1Metered.assertDoesNotMatch(identMobileImsi2Metered) + templateCarrierImsi1Metered.assertDoesNotMatch(identWifiSsid1Metered) + templateCarrierImsi1Metered.assertMatches(identCarrierWifiImsi1Metered) + templateCarrierImsi1Metered.assertDoesNotMatch(identCarrierWifiImsi1NonMetered) + } + + @Test + fun testRatTypeGroupMatches() { + val stateMobile = buildMobileNetworkState(TEST_IMSI1) + // Build UMTS template that matches mobile identities with RAT in the same + // group with any IMSI. See {@link NetworkTemplate#getCollapsedRatType}. + val templateUmts = buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS) + // Build normal template that matches mobile identities with any RAT and IMSI. + val templateAll = buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL) + // Build template with UNKNOWN RAT that matches mobile identities with RAT that + // cannot be determined. + val templateUnknown = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN) + + val identUmts = buildNetworkIdentity( + mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_UMTS) + val identHsdpa = buildNetworkIdentity( + mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_HSDPA) + val identLte = buildNetworkIdentity( + mockContext, stateMobile, false, TelephonyManager.NETWORK_TYPE_LTE) + val identCombined = buildNetworkIdentity( + mockContext, stateMobile, false, SUBTYPE_COMBINED) + val identImsi2 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2), + false, TelephonyManager.NETWORK_TYPE_UMTS) + val identWifi = buildNetworkIdentity( + mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0) + + // Assert that identity with the same RAT matches. + templateUmts.assertMatches(identUmts) + templateAll.assertMatches(identUmts) + templateUnknown.assertDoesNotMatch(identUmts) + // Assert that identity with the RAT within the same group matches. + templateUmts.assertMatches(identHsdpa) + templateAll.assertMatches(identHsdpa) + templateUnknown.assertDoesNotMatch(identHsdpa) + // Assert that identity with the RAT out of the same group only matches template with + // NETWORK_TYPE_ALL. + templateUmts.assertDoesNotMatch(identLte) + templateAll.assertMatches(identLte) + templateUnknown.assertDoesNotMatch(identLte) + // Assert that identity with combined RAT only matches with template with NETWORK_TYPE_ALL + // and NETWORK_TYPE_UNKNOWN. + templateUmts.assertDoesNotMatch(identCombined) + templateAll.assertMatches(identCombined) + templateUnknown.assertMatches(identCombined) + // Assert that identity with different IMSI matches. + templateUmts.assertMatches(identImsi2) + templateAll.assertMatches(identImsi2) + templateUnknown.assertDoesNotMatch(identImsi2) + // Assert that wifi identity does not match. + templateUmts.assertDoesNotMatch(identWifi) + templateAll.assertDoesNotMatch(identWifi) + templateUnknown.assertDoesNotMatch(identWifi) + } + + @Test + fun testParcelUnparcel() { + val templateMobile = NetworkTemplate(MATCH_MOBILE, TEST_IMSI1, null, null, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, TelephonyManager.NETWORK_TYPE_LTE, + OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT) + val templateWifi = NetworkTemplate(MATCH_WIFI, null, null, TEST_SSID1, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 0, OEM_MANAGED_ALL, + SUBSCRIBER_ID_MATCH_RULE_EXACT) + val templateOem = NetworkTemplate(MATCH_MOBILE, null, null, null, METERED_ALL, + ROAMING_ALL, DEFAULT_NETWORK_ALL, 0, OEM_MANAGED_YES, + SUBSCRIBER_ID_MATCH_RULE_EXACT) + assertParcelSane(templateMobile, 10) + assertParcelSane(templateWifi, 10) + assertParcelSane(templateOem, 10) + } + + // Verify NETWORK_TYPE_* constants in NetworkTemplate do not conflict with + // TelephonyManager#NETWORK_TYPE_* constants. + @Test + fun testNetworkTypeConstants() { + for (ratType in TelephonyManager.getAllNetworkTypes()) { + assertNotEquals(NETWORK_TYPE_ALL, ratType) + assertNotEquals(NETWORK_TYPE_5G_NSA, ratType) + } + } + + @Test + fun testOemNetworkConstants() { + val constantValues = arrayOf(OEM_MANAGED_YES, OEM_MANAGED_ALL, OEM_MANAGED_NO, + OEM_PAID, OEM_PRIVATE, OEM_PAID or OEM_PRIVATE) + + // Verify that "not OEM managed network" constants are equal. + assertEquals(OEM_MANAGED_NO, OEM_NONE) + + // Verify the constants don't conflict. + assertEquals(constantValues.size, constantValues.distinct().count()) + } + + /** + * Helper to enumerate and assert OEM managed wifi and mobile {@code NetworkTemplate}s match + * their the appropriate OEM managed {@code NetworkIdentity}s. + * + * @param networkType {@code TYPE_MOBILE} or {@code TYPE_WIFI} + * @param matchType A match rule from {@code NetworkTemplate.MATCH_*} corresponding to the + * networkType. + * @param subscriberId To be populated with {@code TEST_IMSI*} only if networkType is + * {@code TYPE_MOBILE}. May be left as null when matchType is + * {@link NetworkTemplate.MATCH_MOBILE_WILDCARD}. + * @param templateSsid Top be populated with {@code TEST_SSID*} only if networkType is + * {@code TYPE_WIFI}. May be left as null when matchType is + * {@link NetworkTemplate.MATCH_WIFI_WILDCARD}. + * @param identSsid If networkType is {@code TYPE_WIFI}, this value must *NOT* be null. Provide + * one of {@code TEST_SSID*}. + */ + private fun matchOemManagedIdent( + networkType: Int, + matchType: Int, + subscriberId: String? = null, + templateSsid: String? = null, + identSsid: String? = null + ) { + val oemManagedStates = arrayOf(OEM_NONE, OEM_PAID, OEM_PRIVATE, OEM_PAID or OEM_PRIVATE) + val matchSubscriberIds = arrayOf(subscriberId) + + val templateOemYes = NetworkTemplate(matchType, subscriberId, matchSubscriberIds, + templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_MANAGED_YES, SUBSCRIBER_ID_MATCH_RULE_EXACT) + val templateOemAll = NetworkTemplate(matchType, subscriberId, matchSubscriberIds, + templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_MANAGED_ALL, SUBSCRIBER_ID_MATCH_RULE_EXACT) + + for (identityOemManagedState in oemManagedStates) { + val ident = buildNetworkIdentity(mockContext, buildNetworkState(networkType, + subscriberId, identSsid, identityOemManagedState), /*defaultNetwork=*/false, + /*subType=*/0) + + // Create a template with each OEM managed type and match it against the NetworkIdentity + for (templateOemManagedState in oemManagedStates) { + val template = NetworkTemplate(matchType, subscriberId, matchSubscriberIds, + templateSsid, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, + NETWORK_TYPE_ALL, templateOemManagedState, SUBSCRIBER_ID_MATCH_RULE_EXACT) + if (identityOemManagedState == templateOemManagedState) { + template.assertMatches(ident) + } else { + template.assertDoesNotMatch(ident) + } + } + // OEM_MANAGED_ALL ignores OEM state. + templateOemAll.assertMatches(ident) + if (identityOemManagedState == OEM_NONE) { + // OEM_MANAGED_YES matches everything except OEM_NONE. + templateOemYes.assertDoesNotMatch(ident) + } else { + templateOemYes.assertMatches(ident) + } + } + } + + @Test + fun testOemManagedMatchesIdent() { + matchOemManagedIdent(TYPE_MOBILE, MATCH_MOBILE, subscriberId = TEST_IMSI1) + matchOemManagedIdent(TYPE_MOBILE, MATCH_MOBILE_WILDCARD) + matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI, templateSsid = TEST_SSID1, + identSsid = TEST_SSID1) + matchOemManagedIdent(TYPE_WIFI, MATCH_WIFI_WILDCARD, identSsid = TEST_SSID1) + } +} diff --git a/tests/unit/java/android/net/NetworkUtilsTest.java b/tests/unit/java/android/net/NetworkUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7748288aeb050d0f6715227922b1848a086dc768 --- /dev/null +++ b/tests/unit/java/android/net/NetworkUtilsTest.java @@ -0,0 +1,128 @@ +/* + * 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; + +import static junit.framework.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.math.BigInteger; +import java.util.TreeSet; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class NetworkUtilsTest { + @Test + public void testRoutedIPv4AddressCount() { + final TreeSet<IpPrefix> set = new TreeSet<>(IpPrefix.lengthComparator()); + // No routes routes to no addresses. + assertEquals(0, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("0.0.0.0/0")); + assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("20.18.0.0/16")); + set.add(new IpPrefix("20.18.0.0/24")); + set.add(new IpPrefix("20.18.0.0/8")); + // There is a default route, still covers everything + assertEquals(1l << 32, NetworkUtils.routedIPv4AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("20.18.0.0/24")); + set.add(new IpPrefix("20.18.0.0/8")); + // The 8-length includes the 24-length prefix + assertEquals(1l << 24, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("10.10.10.126/25")); + // The 8-length does not include this 25-length prefix + assertEquals((1l << 24) + (1 << 7), NetworkUtils.routedIPv4AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + set.add(new IpPrefix("1.2.3.4/32")); + assertEquals(1l, NetworkUtils.routedIPv4AddressCount(set)); + + set.add(new IpPrefix("1.2.3.5/32")); + set.add(new IpPrefix("1.2.3.6/32")); + + set.add(new IpPrefix("1.2.3.7/32")); + set.add(new IpPrefix("1.2.3.8/32")); + set.add(new IpPrefix("1.2.3.9/32")); + set.add(new IpPrefix("1.2.3.0/32")); + assertEquals(7l, NetworkUtils.routedIPv4AddressCount(set)); + + // 1.2.3.4/30 eats 1.2.3.{4-7}/32 + set.add(new IpPrefix("1.2.3.4/30")); + set.add(new IpPrefix("6.2.3.4/28")); + set.add(new IpPrefix("120.2.3.4/16")); + assertEquals(7l - 4 + 4 + 16 + 65536, NetworkUtils.routedIPv4AddressCount(set)); + } + + @Test + public void testRoutedIPv6AddressCount() { + final TreeSet<IpPrefix> set = new TreeSet<>(IpPrefix.lengthComparator()); + // No routes routes to no addresses. + assertEquals(BigInteger.ZERO, NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("::/0")); + assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("1234:622a::18/64")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8")); + // There is a default route, still covers everything + assertEquals(BigInteger.ONE.shiftLeft(128), NetworkUtils.routedIPv6AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/96")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6adb/8")); + // The 8-length includes the 96-length prefix + assertEquals(BigInteger.ONE.shiftLeft(120), NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("10::26/64")); + // The 8-length does not include this 64-length prefix + assertEquals(BigInteger.ONE.shiftLeft(120).add(BigInteger.ONE.shiftLeft(64)), + NetworkUtils.routedIPv6AddressCount(set)); + + set.clear(); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/128")); + assertEquals(BigInteger.ONE, NetworkUtils.routedIPv6AddressCount(set)); + + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad5/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad6/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad7/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad8/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad9/128")); + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad0/128")); + assertEquals(BigInteger.valueOf(7), NetworkUtils.routedIPv6AddressCount(set)); + + // add4:f00:80:f7:1111::6ad4/126 eats add4:f00:8[:f7:1111::6ad{4-7}/128 + set.add(new IpPrefix("add4:f00:80:f7:1111::6ad4/126")); + set.add(new IpPrefix("d00d:f00:80:f7:1111::6ade/124")); + set.add(new IpPrefix("f00b:a33::/112")); + assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536), + NetworkUtils.routedIPv6AddressCount(set)); + } +} diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..40f8f1b8d09a93295df4954e1ed1e001545e5c42 --- /dev/null +++ b/tests/unit/java/android/net/QosSocketFilterTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class QosSocketFilterTest { + + @Test + public void testPortExactMatch() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertTrue(QosSocketFilter.matchesAddress( + new InetSocketAddress(addressA, 10), addressB, 10, 10)); + + } + + @Test + public void testPortLessThanStart() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertFalse(QosSocketFilter.matchesAddress( + new InetSocketAddress(addressA, 8), addressB, 10, 10)); + } + + @Test + public void testPortGreaterThanEnd() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertFalse(QosSocketFilter.matchesAddress( + new InetSocketAddress(addressA, 18), addressB, 10, 10)); + } + + @Test + public void testPortBetweenStartAndEnd() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4"); + assertTrue(QosSocketFilter.matchesAddress( + new InetSocketAddress(addressA, 10), addressB, 8, 18)); + } + + @Test + public void testAddressesDontMatch() { + final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4"); + final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.5"); + assertFalse(QosSocketFilter.matchesAddress( + new InetSocketAddress(addressA, 10), addressB, 10, 10)); + } +} + diff --git a/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6714bb1abbe62e6479853dfd39e9204c7fb8e5b2 --- /dev/null +++ b/tests/unit/java/android/net/TelephonyNetworkSpecifierTest.java @@ -0,0 +1,113 @@ +/* + * 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; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.net.wifi.WifiNetworkSpecifier; +import android.telephony.SubscriptionManager; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; + +/** + * Unit test for {@link android.net.TelephonyNetworkSpecifier}. + */ +@SmallTest +public class TelephonyNetworkSpecifierTest { + private static final int TEST_SUBID = 5; + private static final String TEST_SSID = "Test123"; + + /** + * Validate that IllegalArgumentException will be thrown if build TelephonyNetworkSpecifier + * without calling {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}. + */ + @Test + public void testBuilderBuildWithDefault() { + try { + new TelephonyNetworkSpecifier.Builder().build(); + } catch (IllegalArgumentException iae) { + // expected, test pass + } + } + + /** + * Validate that no exception will be thrown even if pass invalid subscription id to + * {@link TelephonyNetworkSpecifier.Builder#setSubscriptionId(int)}. + */ + @Test + public void testBuilderBuildWithInvalidSubId() { + TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + .build(); + assertEquals(specifier.getSubscriptionId(), SubscriptionManager.INVALID_SUBSCRIPTION_ID); + } + + /** + * Validate the correctness of TelephonyNetworkSpecifier when provide valid subId. + */ + @Test + public void testBuilderBuildWithValidSubId() { + final TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + assertEquals(TEST_SUBID, specifier.getSubscriptionId()); + } + + /** + * Validate that parcel marshalling/unmarshalling works. + */ + @Test + public void testParcel() { + TelephonyNetworkSpecifier specifier = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + assertParcelSane(specifier, 1 /* fieldCount */); + } + + /** + * Validate the behavior of method canBeSatisfiedBy(). + */ + @Test + public void testCanBeSatisfiedBy() { + final TelephonyNetworkSpecifier tns1 = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + final TelephonyNetworkSpecifier tns2 = new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(TEST_SUBID) + .build(); + final WifiNetworkSpecifier wns = new WifiNetworkSpecifier.Builder() + .setSsid(TEST_SSID) + .build(); + final MatchAllNetworkSpecifier mans = new MatchAllNetworkSpecifier(); + + // Test equality + assertEquals(tns1, tns2); + assertTrue(tns1.canBeSatisfiedBy(tns1)); + assertTrue(tns1.canBeSatisfiedBy(tns2)); + + // Test other edge cases. + assertFalse(tns1.canBeSatisfiedBy(null)); + assertFalse(tns1.canBeSatisfiedBy(wns)); + assertTrue(tns1.canBeSatisfiedBy(mans)); + } +} diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3135062138ac580c08c18f1672487d04ac2db36b --- /dev/null +++ b/tests/unit/java/android/net/VpnManagerTest.java @@ -0,0 +1,138 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Intent; +import android.test.mock.MockContext; +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.net.VpnProfile; +import com.android.internal.util.MessageUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link VpnManager}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VpnManagerTest { + private static final String PKG_NAME = "fooPackage"; + + private static final String SESSION_NAME_STRING = "testSession"; + private static final String SERVER_ADDR_STRING = "1.2.3.4"; + private static final String IDENTITY_STRING = "Identity"; + private static final byte[] PSK_BYTES = "preSharedKey".getBytes(); + + private IVpnManager mMockService; + private VpnManager mVpnManager; + private final MockContext mMockContext = + new MockContext() { + @Override + public String getOpPackageName() { + return PKG_NAME; + } + }; + + @Before + public void setUp() throws Exception { + mMockService = mock(IVpnManager.class); + mVpnManager = new VpnManager(mMockContext, mMockService); + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final PlatformVpnProfile profile = getPlatformVpnProfile(); + when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))) + .thenReturn(true); + + // Expect there to be no intent returned, as consent has already been granted. + assertNull(mVpnManager.provisionVpnProfile(profile)); + verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); + } + + @Test + public void testProvisionVpnProfileNeedsConsent() throws Exception { + final PlatformVpnProfile profile = getPlatformVpnProfile(); + when(mMockService.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))) + .thenReturn(false); + + // Expect intent to be returned, as consent has not already been granted. + final Intent intent = mVpnManager.provisionVpnProfile(profile); + assertNotNull(intent); + + final ComponentName expectedComponentName = + ComponentName.unflattenFromString( + "com.android.vpndialogs/com.android.vpndialogs.PlatformVpnConfirmDialog"); + assertEquals(expectedComponentName, intent.getComponent()); + verify(mMockService).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME)); + } + + @Test + public void testDeleteProvisionedVpnProfile() throws Exception { + mVpnManager.deleteProvisionedVpnProfile(); + verify(mMockService).deleteVpnProfile(eq(PKG_NAME)); + } + + @Test + public void testStartProvisionedVpnProfile() throws Exception { + mVpnManager.startProvisionedVpnProfile(); + verify(mMockService).startVpnProfile(eq(PKG_NAME)); + } + + @Test + public void testStopProvisionedVpnProfile() throws Exception { + mVpnManager.stopProvisionedVpnProfile(); + verify(mMockService).stopVpnProfile(eq(PKG_NAME)); + } + + private Ikev2VpnProfile getPlatformVpnProfile() throws Exception { + return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING) + .setBypassable(true) + .setMaxMtu(1300) + .setMetered(true) + .setAuthPsk(PSK_BYTES) + .build(); + } + + @Test + public void testVpnTypesEqual() throws Exception { + SparseArray<String> vmVpnTypes = MessageUtils.findMessageNames( + new Class[] { VpnManager.class }, new String[]{ "TYPE_VPN_" }); + SparseArray<String> nativeVpnType = MessageUtils.findMessageNames( + new Class[] { NativeVpnType.class }, new String[]{ "" }); + + // TYPE_VPN_NONE = -1 is only defined in VpnManager. + assertEquals(vmVpnTypes.size() - 1, nativeVpnType.size()); + for (int i = VpnManager.TYPE_VPN_SERVICE; i < vmVpnTypes.size(); i++) { + assertEquals(vmVpnTypes.get(i), "TYPE_VPN_" + nativeVpnType.get(i)); + } + } +} diff --git a/tests/unit/java/android/net/VpnTransportInfoTest.java b/tests/unit/java/android/net/VpnTransportInfoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ccaa5cf7e9f7cc172188b17f37e0ab9d8a520439 --- /dev/null +++ b/tests/unit/java/android/net/VpnTransportInfoTest.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 android.net; + +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.REDACT_NONE; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +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 VpnTransportInfoTest { + + @Test + public void testParceling() { + VpnTransportInfo v = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, "12345"); + assertParcelSane(v, 2 /* fieldCount */); + } + + @Test + public void testEqualsAndHashCode() { + String session1 = "12345"; + String session2 = "6789"; + VpnTransportInfo v11 = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, session1); + VpnTransportInfo v12 = new VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, session1); + VpnTransportInfo v13 = new VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, session1); + VpnTransportInfo v14 = new VpnTransportInfo(VpnManager.TYPE_VPN_LEGACY, session1); + VpnTransportInfo v15 = new VpnTransportInfo(VpnManager.TYPE_VPN_OEM, session1); + VpnTransportInfo v21 = new VpnTransportInfo(VpnManager.TYPE_VPN_LEGACY, session2); + + VpnTransportInfo v31 = v11.makeCopy(REDACT_FOR_NETWORK_SETTINGS); + VpnTransportInfo v32 = v13.makeCopy(REDACT_FOR_NETWORK_SETTINGS); + + assertNotEquals(v11, v12); + assertNotEquals(v13, v14); + assertNotEquals(v14, v15); + assertNotEquals(v14, v21); + + assertEquals(v11, v13); + assertEquals(v31, v32); + assertEquals(v11.hashCode(), v13.hashCode()); + assertEquals(REDACT_FOR_NETWORK_SETTINGS, v32.getApplicableRedactions()); + assertEquals(session1, v15.makeCopy(REDACT_NONE).getSessionId()); + } +} diff --git a/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java new file mode 100644 index 0000000000000000000000000000000000000000..603c875195323496bdcee7da4441b2c731e9c731 --- /dev/null +++ b/tests/unit/java/android/net/ipmemorystore/ParcelableTests.java @@ -0,0 +1,142 @@ +/* + * 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.ipmemorystore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk; +import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirkParcelable; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Modifier; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ParcelableTests { + @Test + public void testNetworkAttributesParceling() throws Exception { + final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); + NetworkAttributes in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + builder.setAssignedV4Address((Inet4Address) Inet4Address.getByName("1.2.3.4")); + // lease will expire in two hours + builder.setAssignedV4AddressExpiry(System.currentTimeMillis() + 7_200_000); + // cluster stays null this time around + builder.setDnsAddresses(Collections.emptyList()); + builder.setMtu(18); + in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + builder.setAssignedV4Address((Inet4Address) Inet4Address.getByName("6.7.8.9")); + builder.setAssignedV4AddressExpiry(System.currentTimeMillis() + 3_600_000); + builder.setCluster("groupHint"); + builder.setDnsAddresses(Arrays.asList( + InetAddress.getByName("ACA1:652B:0911:DE8F:1200:115E:913B:AA2A"), + InetAddress.getByName("6.7.8.9"))); + builder.setMtu(1_000_000); + in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + builder.setMtu(null); + in = builder.build(); + assertEquals(in, new NetworkAttributes(parcelingRoundTrip(in.toParcelable()))); + + // Verify that this test does not miss any new field added later. + // If any field is added to NetworkAttributes it must be tested here for parceling + // roundtrip. + assertEquals(6, Arrays.stream(NetworkAttributes.class.getDeclaredFields()) + .filter(f -> !Modifier.isStatic(f.getModifiers())).count()); + } + + @Test + public void testPrivateDataParceling() throws Exception { + final Blob in = new Blob(); + in.data = new byte[] {89, 111, 108, 111}; + final Blob out = parcelingRoundTrip(in); + // Object.equals on byte[] tests the references + assertEquals(in.data.length, out.data.length); + assertTrue(Arrays.equals(in.data, out.data)); + } + + @Test + public void testSameL3NetworkResponseParceling() throws Exception { + final SameL3NetworkResponseParcelable parcelable = new SameL3NetworkResponseParcelable(); + parcelable.l2Key1 = "key 1"; + parcelable.l2Key2 = "key 2"; + parcelable.confidence = 0.43f; + + final SameL3NetworkResponse in = new SameL3NetworkResponse(parcelable); + assertEquals("key 1", in.l2Key1); + assertEquals("key 2", in.l2Key2); + assertEquals(0.43f, in.confidence, 0.01f /* delta */); + + final SameL3NetworkResponse out = + new SameL3NetworkResponse(parcelingRoundTrip(in.toParcelable())); + + assertEquals(in, out); + assertEquals(in.l2Key1, out.l2Key1); + assertEquals(in.l2Key2, out.l2Key2); + assertEquals(in.confidence, out.confidence, 0.01f /* delta */); + } + + @Test + public void testIPv6ProvisioningLossQuirkParceling() throws Exception { + final NetworkAttributes.Builder builder = new NetworkAttributes.Builder(); + final IPv6ProvisioningLossQuirkParcelable parcelable = + new IPv6ProvisioningLossQuirkParcelable(); + final long expiry = System.currentTimeMillis() + 7_200_000; + + parcelable.detectionCount = 3; + parcelable.quirkExpiry = expiry; // quirk info will expire in two hours + builder.setIpv6ProvLossQuirk(IPv6ProvisioningLossQuirk.fromStableParcelable(parcelable)); + final NetworkAttributes in = builder.build(); + + final NetworkAttributes out = new NetworkAttributes(parcelingRoundTrip(in.toParcelable())); + assertEquals(out.ipv6ProvisioningLossQuirk, in.ipv6ProvisioningLossQuirk); + } + + private <T extends Parcelable> T parcelingRoundTrip(final T in) throws Exception { + final Parcel p = Parcel.obtain(); + in.writeToParcel(p, /* flags */ 0); + p.setDataPosition(0); + final byte[] marshalledData = p.marshall(); + p.recycle(); + + final Parcel q = Parcel.obtain(); + q.unmarshall(marshalledData, 0, marshalledData.length); + q.setDataPosition(0); + + final Parcelable.Creator<T> creator = (Parcelable.Creator<T>) + in.getClass().getField("CREATOR").get(null); // static object, so null receiver + final T unmarshalled = (T) creator.createFromParcel(q); + q.recycle(); + return unmarshalled; + } +} diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b0a9b8a553223c77ebedac2c0729fce7ffd7b3f6 --- /dev/null +++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java @@ -0,0 +1,388 @@ +/* + * 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.nsd; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.AsyncChannel; +import com.android.testutils.HandlerUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NsdManagerTest { + + static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD; + + @Mock Context mContext; + @Mock INsdManager mService; + MockServiceHandler mServiceHandler; + + NsdManager mManager; + + long mTimeoutMs = 200; // non-final so that tests can adjust the value. + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mServiceHandler = spy(MockServiceHandler.create(mContext)); + when(mService.getMessenger()).thenReturn(new Messenger(mServiceHandler)); + + mManager = makeManager(); + } + + @After + public void tearDown() throws Exception { + HandlerUtils.waitForIdle(mServiceHandler, mTimeoutMs); + mServiceHandler.chan.disconnect(); + mServiceHandler.stop(); + if (mManager != null) { + mManager.disconnect(); + } + } + + @Test + public void testResolveService() { + NsdManager manager = mManager; + + NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo reply = new NsdServiceInfo("resolved_name", "resolved_type"); + NsdManager.ResolveListener listener = mock(NsdManager.ResolveListener.class); + + manager.resolveService(request, listener); + int key1 = verifyRequest(NsdManager.RESOLVE_SERVICE); + int err = 33; + sendResponse(NsdManager.RESOLVE_SERVICE_FAILED, err, key1, null); + verify(listener, timeout(mTimeoutMs).times(1)).onResolveFailed(request, err); + + manager.resolveService(request, listener); + int key2 = verifyRequest(NsdManager.RESOLVE_SERVICE); + sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key2, reply); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceResolved(reply); + } + + @Test + public void testParallelResolveService() { + NsdManager manager = mManager; + + NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo reply = new NsdServiceInfo("resolved_name", "resolved_type"); + + NsdManager.ResolveListener listener1 = mock(NsdManager.ResolveListener.class); + NsdManager.ResolveListener listener2 = mock(NsdManager.ResolveListener.class); + + manager.resolveService(request, listener1); + int key1 = verifyRequest(NsdManager.RESOLVE_SERVICE); + + manager.resolveService(request, listener2); + int key2 = verifyRequest(NsdManager.RESOLVE_SERVICE); + + sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key2, reply); + sendResponse(NsdManager.RESOLVE_SERVICE_SUCCEEDED, 0, key1, reply); + + verify(listener1, timeout(mTimeoutMs).times(1)).onServiceResolved(reply); + verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply); + } + + @Test + public void testRegisterService() { + NsdManager manager = mManager; + + NsdServiceInfo request1 = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo request2 = new NsdServiceInfo("another_name", "another_type"); + request1.setPort(2201); + request2.setPort(2202); + NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); + NsdManager.RegistrationListener listener2 = mock(NsdManager.RegistrationListener.class); + + // Register two services + manager.registerService(request1, PROTOCOL, listener1); + int key1 = verifyRequest(NsdManager.REGISTER_SERVICE); + + manager.registerService(request2, PROTOCOL, listener2); + int key2 = verifyRequest(NsdManager.REGISTER_SERVICE); + + // First reques fails, second request succeeds + sendResponse(NsdManager.REGISTER_SERVICE_SUCCEEDED, 0, key2, request2); + verify(listener2, timeout(mTimeoutMs).times(1)).onServiceRegistered(request2); + + int err = 1; + sendResponse(NsdManager.REGISTER_SERVICE_FAILED, err, key1, request1); + verify(listener1, timeout(mTimeoutMs).times(1)).onRegistrationFailed(request1, err); + + // Client retries first request, it succeeds + manager.registerService(request1, PROTOCOL, listener1); + int key3 = verifyRequest(NsdManager.REGISTER_SERVICE); + + sendResponse(NsdManager.REGISTER_SERVICE_SUCCEEDED, 0, key3, request1); + verify(listener1, timeout(mTimeoutMs).times(1)).onServiceRegistered(request1); + + // First request is unregistered, it succeeds + manager.unregisterService(listener1); + int key3again = verifyRequest(NsdManager.UNREGISTER_SERVICE); + assertEquals(key3, key3again); + + sendResponse(NsdManager.UNREGISTER_SERVICE_SUCCEEDED, 0, key3again, null); + verify(listener1, timeout(mTimeoutMs).times(1)).onServiceUnregistered(request1); + + // Second request is unregistered, it fails + manager.unregisterService(listener2); + int key2again = verifyRequest(NsdManager.UNREGISTER_SERVICE); + assertEquals(key2, key2again); + + sendResponse(NsdManager.UNREGISTER_SERVICE_FAILED, err, key2again, null); + verify(listener2, timeout(mTimeoutMs).times(1)).onUnregistrationFailed(request2, err); + + // TODO: do not unregister listener until service is unregistered + // Client retries unregistration of second request, it succeeds + //manager.unregisterService(listener2); + //int key2yetAgain = verifyRequest(NsdManager.UNREGISTER_SERVICE); + //assertEquals(key2, key2yetAgain); + + //sendResponse(NsdManager.UNREGISTER_SERVICE_SUCCEEDED, 0, key2yetAgain, null); + //verify(listener2, timeout(mTimeoutMs).times(1)).onServiceUnregistered(request2); + } + + @Test + public void testDiscoverService() { + NsdManager manager = mManager; + + NsdServiceInfo reply1 = new NsdServiceInfo("a_name", "a_type"); + NsdServiceInfo reply2 = new NsdServiceInfo("another_name", "a_type"); + NsdServiceInfo reply3 = new NsdServiceInfo("a_third_name", "a_type"); + + NsdManager.DiscoveryListener listener = mock(NsdManager.DiscoveryListener.class); + + // Client registers for discovery, request fails + manager.discoverServices("a_type", PROTOCOL, listener); + int key1 = verifyRequest(NsdManager.DISCOVER_SERVICES); + + int err = 1; + sendResponse(NsdManager.DISCOVER_SERVICES_FAILED, err, key1, null); + verify(listener, timeout(mTimeoutMs).times(1)).onStartDiscoveryFailed("a_type", err); + + // Client retries, request succeeds + manager.discoverServices("a_type", PROTOCOL, listener); + int key2 = verifyRequest(NsdManager.DISCOVER_SERVICES); + + sendResponse(NsdManager.DISCOVER_SERVICES_STARTED, 0, key2, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type"); + + + // mdns notifies about services + sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceFound(reply1); + + sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply2); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceFound(reply2); + + sendResponse(NsdManager.SERVICE_LOST, 0, key2, reply2); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceLost(reply2); + + + // Client unregisters its listener + manager.stopServiceDiscovery(listener); + int key2again = verifyRequest(NsdManager.STOP_DISCOVERY); + assertEquals(key2, key2again); + + // TODO: unregister listener immediately and stop notifying it about services + // Notifications are still passed to the client's listener + sendResponse(NsdManager.SERVICE_LOST, 0, key2, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onServiceLost(reply1); + + // Client is notified of complete unregistration + sendResponse(NsdManager.STOP_DISCOVERY_SUCCEEDED, 0, key2again, "a_type"); + verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStopped("a_type"); + + // Notifications are not passed to the client anymore + sendResponse(NsdManager.SERVICE_FOUND, 0, key2, reply3); + verify(listener, timeout(mTimeoutMs).times(0)).onServiceLost(reply3); + + + // Client registers for service discovery + reset(listener); + manager.discoverServices("a_type", PROTOCOL, listener); + int key3 = verifyRequest(NsdManager.DISCOVER_SERVICES); + + sendResponse(NsdManager.DISCOVER_SERVICES_STARTED, 0, key3, reply1); + verify(listener, timeout(mTimeoutMs).times(1)).onDiscoveryStarted("a_type"); + + // Client unregisters immediately, it fails + manager.stopServiceDiscovery(listener); + int key3again = verifyRequest(NsdManager.STOP_DISCOVERY); + assertEquals(key3, key3again); + + err = 2; + sendResponse(NsdManager.STOP_DISCOVERY_FAILED, err, key3again, "a_type"); + verify(listener, timeout(mTimeoutMs).times(1)).onStopDiscoveryFailed("a_type", err); + + // New notifications are not passed to the client anymore + sendResponse(NsdManager.SERVICE_FOUND, 0, key3, reply1); + verify(listener, timeout(mTimeoutMs).times(0)).onServiceFound(reply1); + } + + @Test + public void testInvalidCalls() { + NsdManager manager = mManager; + + NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); + NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); + NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); + + NsdServiceInfo invalidService = new NsdServiceInfo(null, null); + NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type"); + validService.setPort(2222); + + // Service registration + // - invalid arguments + mustFail(() -> { manager.unregisterService(null); }); + mustFail(() -> { manager.registerService(null, -1, null); }); + mustFail(() -> { manager.registerService(null, PROTOCOL, listener1); }); + mustFail(() -> { manager.registerService(invalidService, PROTOCOL, listener1); }); + mustFail(() -> { manager.registerService(validService, -1, listener1); }); + mustFail(() -> { manager.registerService(validService, PROTOCOL, null); }); + manager.registerService(validService, PROTOCOL, listener1); + // - listener already registered + mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); }); + manager.unregisterService(listener1); + // TODO: make listener immediately reusable + //mustFail(() -> { manager.unregisterService(listener1); }); + //manager.registerService(validService, PROTOCOL, listener1); + + // Discover service + // - invalid arguments + mustFail(() -> { manager.stopServiceDiscovery(null); }); + mustFail(() -> { manager.discoverServices(null, -1, null); }); + mustFail(() -> { manager.discoverServices(null, PROTOCOL, listener2); }); + mustFail(() -> { manager.discoverServices("a_service", -1, listener2); }); + mustFail(() -> { manager.discoverServices("a_service", PROTOCOL, null); }); + manager.discoverServices("a_service", PROTOCOL, listener2); + // - listener already registered + mustFail(() -> { manager.discoverServices("another_service", PROTOCOL, listener2); }); + manager.stopServiceDiscovery(listener2); + // TODO: make listener immediately reusable + //mustFail(() -> { manager.stopServiceDiscovery(listener2); }); + //manager.discoverServices("another_service", PROTOCOL, listener2); + + // Resolver service + // - invalid arguments + mustFail(() -> { manager.resolveService(null, null); }); + mustFail(() -> { manager.resolveService(null, listener3); }); + mustFail(() -> { manager.resolveService(invalidService, listener3); }); + mustFail(() -> { manager.resolveService(validService, null); }); + manager.resolveService(validService, listener3); + // - listener already registered:w + mustFail(() -> { manager.resolveService(validService, listener3); }); + } + + public void mustFail(Runnable fn) { + try { + fn.run(); + fail(); + } catch (Exception expected) { + } + } + + NsdManager makeManager() { + NsdManager manager = new NsdManager(mContext, mService); + // Acknowledge first two messages connecting the AsyncChannel. + verify(mServiceHandler, timeout(mTimeoutMs).times(2)).handleMessage(any()); + reset(mServiceHandler); + assertNotNull(mServiceHandler.chan); + return manager; + } + + int verifyRequest(int expectedMessageType) { + HandlerUtils.waitForIdle(mServiceHandler, mTimeoutMs); + verify(mServiceHandler, timeout(mTimeoutMs)).handleMessage(any()); + reset(mServiceHandler); + Message received = mServiceHandler.getLastMessage(); + assertEquals(NsdManager.nameOf(expectedMessageType), NsdManager.nameOf(received.what)); + return received.arg2; + } + + void sendResponse(int replyType, int arg, int key, Object obj) { + mServiceHandler.chan.sendMessage(replyType, arg, key, obj); + } + + // Implements the server side of AsyncChannel connection protocol + public static class MockServiceHandler extends Handler { + public final Context context; + public AsyncChannel chan; + public Message lastMessage; + + MockServiceHandler(Looper l, Context c) { + super(l); + context = c; + } + + synchronized Message getLastMessage() { + return lastMessage; + } + + synchronized void setLastMessage(Message msg) { + lastMessage = obtainMessage(); + lastMessage.copyFrom(msg); + } + + @Override + public void handleMessage(Message msg) { + setLastMessage(msg); + if (msg.what == AsyncChannel.CMD_CHANNEL_FULL_CONNECTION) { + chan = new AsyncChannel(); + chan.connect(context, this, msg.replyTo); + chan.sendMessage(AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED); + } + } + + void stop() { + getLooper().quitSafely(); + } + + static MockServiceHandler create(Context context) { + HandlerThread t = new HandlerThread("mock-service-handler"); + t.start(); + return new MockServiceHandler(t.getLooper(), context); + } + } +} diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..94dfc7515c67f73210ab39c8bc921abaf3aa17a9 --- /dev/null +++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java @@ -0,0 +1,188 @@ +/* + * 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 android.net.nsd; + +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 android.os.Bundle; +import android.os.Parcel; +import android.os.StrictMode; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NsdServiceInfoTest { + + public final static InetAddress LOCALHOST; + static { + // Because test. + StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); + StrictMode.setThreadPolicy(policy); + + InetAddress _host = null; + try { + _host = InetAddress.getLocalHost(); + } catch (UnknownHostException e) { } + LOCALHOST = _host; + } + + @Test + public void testLimits() throws Exception { + NsdServiceInfo info = new NsdServiceInfo(); + + // Non-ASCII keys. + boolean exceptionThrown = false; + try { + info.setAttribute("猫", "meow"); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertEmptyServiceInfo(info); + + // ASCII keys with '=' character. + exceptionThrown = false; + try { + info.setAttribute("kitten=", "meow"); + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertEmptyServiceInfo(info); + + // Single key + value length too long. + exceptionThrown = false; + try { + String longValue = "loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + "oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + + "ooooooooooooooooooooooooooooong"; // 248 characters. + info.setAttribute("longcat", longValue); // Key + value == 255 characters. + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertEmptyServiceInfo(info); + + // Total TXT record length too long. + exceptionThrown = false; + int recordsAdded = 0; + try { + for (int i = 100; i < 300; ++i) { + // 6 char key + 5 char value + 2 bytes overhead = 13 byte record length. + String key = String.format("key%d", i); + info.setAttribute(key, "12345"); + recordsAdded++; + } + } catch (IllegalArgumentException e) { + exceptionThrown = true; + } + assertTrue(exceptionThrown); + assertTrue(100 == recordsAdded); + assertTrue(info.getTxtRecord().length == 1300); + } + + @Test + public void testParcel() throws Exception { + NsdServiceInfo emptyInfo = new NsdServiceInfo(); + checkParcelable(emptyInfo); + + NsdServiceInfo fullInfo = new NsdServiceInfo(); + fullInfo.setServiceName("kitten"); + fullInfo.setServiceType("_kitten._tcp"); + fullInfo.setPort(4242); + fullInfo.setHost(LOCALHOST); + checkParcelable(fullInfo); + + NsdServiceInfo noHostInfo = new NsdServiceInfo(); + noHostInfo.setServiceName("kitten"); + noHostInfo.setServiceType("_kitten._tcp"); + noHostInfo.setPort(4242); + checkParcelable(noHostInfo); + + NsdServiceInfo attributedInfo = new NsdServiceInfo(); + attributedInfo.setServiceName("kitten"); + attributedInfo.setServiceType("_kitten._tcp"); + attributedInfo.setPort(4242); + attributedInfo.setHost(LOCALHOST); + attributedInfo.setAttribute("color", "pink"); + attributedInfo.setAttribute("sound", (new String("ã«ã‚ƒã‚")).getBytes("UTF-8")); + attributedInfo.setAttribute("adorable", (String) null); + attributedInfo.setAttribute("sticky", "yes"); + attributedInfo.setAttribute("siblings", new byte[] {}); + attributedInfo.setAttribute("edge cases", new byte[] {0, -1, 127, -128}); + attributedInfo.removeAttribute("sticky"); + checkParcelable(attributedInfo); + + // Sanity check that we actually wrote attributes to attributedInfo. + assertTrue(attributedInfo.getAttributes().keySet().contains("adorable")); + String sound = new String(attributedInfo.getAttributes().get("sound"), "UTF-8"); + assertTrue(sound.equals("ã«ã‚ƒã‚")); + byte[] edgeCases = attributedInfo.getAttributes().get("edge cases"); + assertTrue(Arrays.equals(edgeCases, new byte[] {0, -1, 127, -128})); + assertFalse(attributedInfo.getAttributes().keySet().contains("sticky")); + } + + public void checkParcelable(NsdServiceInfo original) { + // Write to parcel. + Parcel p = Parcel.obtain(); + Bundle writer = new Bundle(); + writer.putParcelable("test_info", original); + writer.writeToParcel(p, 0); + + // Extract from parcel. + p.setDataPosition(0); + Bundle reader = p.readBundle(); + reader.setClassLoader(NsdServiceInfo.class.getClassLoader()); + NsdServiceInfo result = reader.getParcelable("test_info"); + + // Assert equality of base fields. + assertEquals(original.getServiceName(), result.getServiceName()); + assertEquals(original.getServiceType(), result.getServiceType()); + assertEquals(original.getHost(), result.getHost()); + assertTrue(original.getPort() == result.getPort()); + + // Assert equality of attribute map. + Map<String, byte[]> originalMap = original.getAttributes(); + Map<String, byte[]> resultMap = result.getAttributes(); + assertEquals(originalMap.keySet(), resultMap.keySet()); + for (String key : originalMap.keySet()) { + assertTrue(Arrays.equals(originalMap.get(key), resultMap.get(key))); + } + } + + public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) { + byte[] txtRecord = shouldBeEmpty.getTxtRecord(); + if (txtRecord == null || txtRecord.length == 0) { + return; + } + fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord)); + } +} diff --git a/tests/unit/java/android/net/util/DnsUtilsTest.java b/tests/unit/java/android/net/util/DnsUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b626db8d89e42adc3130b42b03d993e7490ef0ed --- /dev/null +++ b/tests/unit/java/android/net/util/DnsUtilsTest.java @@ -0,0 +1,216 @@ +/* + * 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.util.DnsUtils.IPV6_ADDR_SCOPE_GLOBAL; +import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_LINKLOCAL; +import static android.net.util.DnsUtils.IPV6_ADDR_SCOPE_SITELOCAL; + +import static org.junit.Assert.assertEquals; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.InetAddresses; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DnsUtilsTest { + private InetAddress stringToAddress(@NonNull String addr) { + return InetAddresses.parseNumericAddress(addr); + } + + private DnsUtils.SortableAddress makeSortableAddress(@NonNull String addr) { + return makeSortableAddress(addr, null); + } + + private DnsUtils.SortableAddress makeSortableAddress(@NonNull String addr, + @Nullable String srcAddr) { + return new DnsUtils.SortableAddress(stringToAddress(addr), + srcAddr != null ? stringToAddress(srcAddr) : null); + } + + @Test + public void testRfc6724Comparator() { + final List<DnsUtils.SortableAddress> test = Arrays.asList( + // Ipv4 + makeSortableAddress("216.58.200.36", "192.168.1.1"), + // global with different scope src + makeSortableAddress("2404:6800:4008:801::2004", "fe80::1111:2222"), + // global without src addr + makeSortableAddress("2404:6800:cafe:801::1"), + // loop back + makeSortableAddress("::1", "::1"), + // link local + makeSortableAddress("fe80::c46f:1cff:fe04:39b4", "fe80::1"), + // teredo tunneling + makeSortableAddress("2001::47c1", "2001::2"), + // 6bone without src addr + makeSortableAddress("3ffe::1234:5678"), + // IPv4-compatible + makeSortableAddress("::216.58.200.36", "::216.58.200.9"), + // 6bone + makeSortableAddress("3ffe::1234:5678", "3ffe::1234:1"), + // IPv4-mapped IPv6 + makeSortableAddress("::ffff:192.168.95.7", "::ffff:192.168.95.1")); + + final List<InetAddress> expected = Arrays.asList( + stringToAddress("::1"), // loop back + stringToAddress("fe80::c46f:1cff:fe04:39b4"), // link local + stringToAddress("216.58.200.36"), // Ipv4 + stringToAddress("::ffff:192.168.95.7"), // IPv4-mapped IPv6 + stringToAddress("2001::47c1"), // teredo tunneling + stringToAddress("::216.58.200.36"), // IPv4-compatible + stringToAddress("3ffe::1234:5678"), // 6bone + stringToAddress("2404:6800:4008:801::2004"), // global with different scope src + stringToAddress("2404:6800:cafe:801::1"), // global without src addr + stringToAddress("3ffe::1234:5678")); // 6bone without src addr + + Collections.sort(test, new DnsUtils.Rfc6724Comparator()); + + for (int i = 0; i < test.size(); ++i) { + assertEquals(test.get(i).address, expected.get(i)); + } + + // TODO: add more combinations + } + + @Test + public void testV4SortableAddress() { + // Test V4 address + DnsUtils.SortableAddress test = makeSortableAddress("216.58.200.36"); + assertEquals(test.hasSrcAddr, 0); + assertEquals(test.prefixMatchLen, 0); + assertEquals(test.address, stringToAddress("216.58.200.36")); + assertEquals(test.labelMatch, 0); + assertEquals(test.scopeMatch, 0); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 4); + assertEquals(test.precedence, 35); + + // Test V4 loopback address with the same source address + test = makeSortableAddress("127.1.2.3", "127.1.2.3"); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.prefixMatchLen, 0); + assertEquals(test.address, stringToAddress("127.1.2.3")); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 4); + assertEquals(test.precedence, 35); + } + + @Test + public void testV6SortableAddress() { + // Test global address + DnsUtils.SortableAddress test = makeSortableAddress("2404:6800:4008:801::2004"); + assertEquals(test.address, stringToAddress("2404:6800:4008:801::2004")); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + + // Test global address with global source address + test = makeSortableAddress("2404:6800:4008:801::2004", + "2401:fa00:fc:fd00:6d6c:7199:b8e7:41d6"); + assertEquals(test.address, stringToAddress("2404:6800:4008:801::2004")); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 1); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + assertEquals(test.prefixMatchLen, 13); + + // Test global address with linklocal source address + test = makeSortableAddress("2404:6800:4008:801::2004", "fe80::c46f:1cff:fe04:39b4"); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 0); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + assertEquals(test.prefixMatchLen, 0); + + // Test loopback address with the same source address + test = makeSortableAddress("::1", "::1"); + assertEquals(test.hasSrcAddr, 1); + assertEquals(test.prefixMatchLen, 16 * 8); + assertEquals(test.labelMatch, 1); + assertEquals(test.scopeMatch, 1); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 0); + assertEquals(test.precedence, 50); + + // Test linklocal address + test = makeSortableAddress("fe80::c46f:1cff:fe04:39b4"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + + // Test linklocal address + test = makeSortableAddress("fe80::"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_LINKLOCAL); + assertEquals(test.label, 1); + assertEquals(test.precedence, 40); + + // Test 6to4 address + test = makeSortableAddress("2002:c000:0204::"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 2); + assertEquals(test.precedence, 30); + + // Test unique local address + test = makeSortableAddress("fc00::c000:13ab"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 13); + assertEquals(test.precedence, 3); + + // Test teredo tunneling address + test = makeSortableAddress("2001::47c1"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 5); + assertEquals(test.precedence, 5); + + // Test IPv4-compatible addresses + test = makeSortableAddress("::216.58.200.36"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 3); + assertEquals(test.precedence, 1); + + // Test site-local address + test = makeSortableAddress("fec0::cafe:3ab2"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_SITELOCAL); + assertEquals(test.label, 11); + assertEquals(test.precedence, 1); + + // Test 6bone address + test = makeSortableAddress("3ffe::1234:5678"); + assertEquals(test.scope, IPV6_ADDR_SCOPE_GLOBAL); + assertEquals(test.label, 12); + assertEquals(test.precedence, 1); + } +} diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5006d5345f5fcae2d2dd818a914a9260226848e1 --- /dev/null +++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt @@ -0,0 +1,145 @@ +/* + * 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.content.Context +import android.content.res.Resources +import android.net.ConnectivityResources +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.MAX_TRANSPORT +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_VPN +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import androidx.test.filters.SmallTest +import com.android.internal.R +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock + +/** + * Tests for [KeepaliveUtils]. + * + * Build, install and run with: + * atest android.net.util.KeepaliveUtilsTest + */ +@RunWith(JUnit4::class) +@SmallTest +class KeepaliveUtilsTest { + + // Prepare mocked context with given resource strings. + private fun getMockedContextWithStringArrayRes( + id: Int, + name: String, + res: Array<out String?>? + ): Context { + val mockRes = mock(Resources::class.java) + doReturn(res).`when`(mockRes).getStringArray(eq(id)) + doReturn(id).`when`(mockRes).getIdentifier(eq(name), any(), any()) + + return mock(Context::class.java).apply { + doReturn(mockRes).`when`(this).getResources() + ConnectivityResources.setResourcesContextForTest(this) + } + } + + @After + fun tearDown() { + ConnectivityResources.setResourcesContextForTest(null) + } + + @Test + fun testGetSupportedKeepalives() { + fun assertRunWithException(res: Array<out String?>?) { + try { + val mockContext = getMockedContextWithStringArrayRes( + R.array.config_networkSupportedKeepaliveCount, + "config_networkSupportedKeepaliveCount", res) + KeepaliveUtils.getSupportedKeepalives(mockContext) + fail("Expected KeepaliveDeviceConfigurationException") + } catch (expected: KeepaliveUtils.KeepaliveDeviceConfigurationException) { + } + } + + // Check resource with various invalid format. + assertRunWithException(null) + assertRunWithException(arrayOf<String?>(null)) + assertRunWithException(arrayOfNulls<String?>(10)) + assertRunWithException(arrayOf("")) + assertRunWithException(arrayOf("3,ABC")) + assertRunWithException(arrayOf("6,3,3")) + assertRunWithException(arrayOf("5")) + + // Check resource with invalid slots value. + assertRunWithException(arrayOf("3,-1")) + + // Check resource with invalid transport type. + assertRunWithException(arrayOf("-1,3")) + assertRunWithException(arrayOf("10,3")) + + // Check valid customization generates expected array. + val validRes = arrayOf("0,3", "1,0", "4,4") + val expectedValidRes = intArrayOf(3, 0, 0, 0, 4, 0, 0, 0, 0) + + val mockContext = getMockedContextWithStringArrayRes( + R.array.config_networkSupportedKeepaliveCount, + "config_networkSupportedKeepaliveCount", validRes) + val actual = KeepaliveUtils.getSupportedKeepalives(mockContext) + assertArrayEquals(expectedValidRes, actual) + } + + @Test + fun testGetSupportedKeepalivesForNetworkCapabilities() { + // Mock customized supported keepalives for each transport type, and assuming: + // 3 for cellular, + // 6 for wifi, + // 0 for others. + val cust = IntArray(MAX_TRANSPORT + 1).apply { + this[TRANSPORT_CELLULAR] = 3 + this[TRANSPORT_WIFI] = 6 + } + + val nc = NetworkCapabilities() + // Check supported keepalives with single transport type. + nc.transportTypes = intArrayOf(TRANSPORT_CELLULAR) + assertEquals(3, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)) + + // Check supported keepalives with multiple transport types. + nc.transportTypes = intArrayOf(TRANSPORT_WIFI, TRANSPORT_VPN) + assertEquals(0, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)) + + // Check supported keepalives with non-customized transport type. + nc.transportTypes = intArrayOf(TRANSPORT_ETHERNET) + assertEquals(0, KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc)) + + // Check supported keepalives with undefined transport type. + nc.transportTypes = intArrayOf(MAX_TRANSPORT + 1) + try { + KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(cust, nc) + fail("Expected ArrayIndexOutOfBoundsException") + } catch (expected: ArrayIndexOutOfBoundsException) { + } + } +} diff --git a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..25aa6266577e8241112c3a520a56944aeceecf45 --- /dev/null +++ b/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt @@ -0,0 +1,148 @@ +/* + * 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.util + +import android.content.Context +import android.content.res.Resources +import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER +import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE +import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY +import android.net.ConnectivityResources +import android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI +import android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE +import android.net.util.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener +import android.provider.Settings +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.test.mock.MockContentResolver +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.connectivity.resources.R +import com.android.internal.util.test.FakeSettingsProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.any +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +/** + * Tests for [MultinetworkPolicyTracker]. + * + * Build, install and run with: + * atest android.net.util.MultinetworkPolicyTrackerTest + */ +@RunWith(AndroidJUnit4::class) +@SmallTest +class MultinetworkPolicyTrackerTest { + private val resources = mock(Resources::class.java).also { + doReturn(R.integer.config_networkAvoidBadWifi).`when`(it).getIdentifier( + eq("config_networkAvoidBadWifi"), eq("integer"), any()) + doReturn(0).`when`(it).getInteger(R.integer.config_networkAvoidBadWifi) + } + private val telephonyManager = mock(TelephonyManager::class.java) + private val subscriptionManager = mock(SubscriptionManager::class.java).also { + doReturn(null).`when`(it).getActiveSubscriptionInfo(anyInt()) + } + private val resolver = MockContentResolver().apply { + addProvider(Settings.AUTHORITY, FakeSettingsProvider()) } + private val context = mock(Context::class.java).also { + doReturn(Context.TELEPHONY_SERVICE).`when`(it) + .getSystemServiceName(TelephonyManager::class.java) + doReturn(telephonyManager).`when`(it).getSystemService(Context.TELEPHONY_SERVICE) + doReturn(subscriptionManager).`when`(it) + .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) + doReturn(resolver).`when`(it).contentResolver + doReturn(resources).`when`(it).resources + doReturn(it).`when`(it).createConfigurationContext(any()) + Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "1") + ConnectivityResources.setResourcesContextForTest(it) + } + private val tracker = MultinetworkPolicyTracker(context, null /* handler */) + + private fun assertMultipathPreference(preference: Int) { + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + preference.toString()) + tracker.updateMeteredMultipathPreference() + assertEquals(preference, tracker.meteredMultipathPreference) + } + + @After + fun tearDown() { + ConnectivityResources.setResourcesContextForTest(null) + } + + @Test + fun testUpdateMeteredMultipathPreference() { + assertMultipathPreference(MULTIPATH_PREFERENCE_HANDOVER) + assertMultipathPreference(MULTIPATH_PREFERENCE_RELIABILITY) + assertMultipathPreference(MULTIPATH_PREFERENCE_PERFORMANCE) + } + + @Test + fun testUpdateAvoidBadWifi() { + Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "0") + assertTrue(tracker.updateAvoidBadWifi()) + assertFalse(tracker.avoidBadWifi) + + doReturn(1).`when`(resources).getInteger(R.integer.config_networkAvoidBadWifi) + assertTrue(tracker.updateAvoidBadWifi()) + assertTrue(tracker.avoidBadWifi) + } + + @Test + fun testOnActiveDataSubscriptionIdChanged() { + val testSubId = 1000 + val subscriptionInfo = SubscriptionInfo(testSubId, ""/* iccId */, 1/* iccId */, + "TMO"/* displayName */, "TMO"/* carrierName */, 1/* nameSource */, 1/* iconTint */, + "123"/* number */, 1/* roaming */, null/* icon */, "310"/* mcc */, "210"/* mnc */, + ""/* countryIso */, false/* isEmbedded */, null/* nativeAccessRules */, + "1"/* cardString */) + doReturn(subscriptionInfo).`when`(subscriptionManager).getActiveSubscriptionInfo(testSubId) + + // Modify avoidBadWifi and meteredMultipathPreference settings value and local variables in + // MultinetworkPolicyTracker should be also updated after subId changed. + Settings.Global.putString(resolver, NETWORK_AVOID_BAD_WIFI, "0") + Settings.Global.putString(resolver, NETWORK_METERED_MULTIPATH_PREFERENCE, + MULTIPATH_PREFERENCE_PERFORMANCE.toString()) + + val listenerCaptor = ArgumentCaptor.forClass( + ActiveDataSubscriptionIdListener::class.java) + verify(telephonyManager, times(1)) + .registerTelephonyCallback(any(), listenerCaptor.capture()) + val listener = listenerCaptor.value + listener.onActiveDataSubscriptionIdChanged(testSubId) + + // Check it get resource value with test sub id. + verify(subscriptionManager, times(1)).getActiveSubscriptionInfo(testSubId) + verify(context).createConfigurationContext(argThat { it.mcc == 310 && it.mnc == 210 }) + + // Check if avoidBadWifi and meteredMultipathPreference values have been updated. + assertFalse(tracker.avoidBadWifi) + assertEquals(MULTIPATH_PREFERENCE_PERFORMANCE, tracker.meteredMultipathPreference) + } +} diff --git a/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3cfecd552967271b8d6e6c9fe1e490e726360cdc --- /dev/null +++ b/tests/unit/java/com/android/internal/net/NetworkUtilsInternalTest.java @@ -0,0 +1,89 @@ +/* + * 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 com.android.internal.net; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.EPERM; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_STREAM; + +import static junit.framework.Assert.assertEquals; + +import static org.junit.Assert.fail; + +import android.system.ErrnoException; +import android.system.Os; + +import androidx.test.runner.AndroidJUnit4; + +import libcore.io.IoUtils; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@androidx.test.filters.SmallTest +public class NetworkUtilsInternalTest { + + private static void expectSocketSuccess(String msg, int domain, int type) { + try { + IoUtils.closeQuietly(Os.socket(domain, type, 0)); + } catch (ErrnoException e) { + fail(msg + e.getMessage()); + } + } + + private static void expectSocketPemissionError(String msg, int domain, int type) { + try { + IoUtils.closeQuietly(Os.socket(domain, type, 0)); + fail(msg); + } catch (ErrnoException e) { + assertEquals(msg, e.errno, EPERM); + } + } + + private static void expectHasNetworking() { + expectSocketSuccess("Creating a UNIX socket should not have thrown ErrnoException", + AF_UNIX, SOCK_STREAM); + expectSocketSuccess("Creating a AF_INET socket shouldn't have thrown ErrnoException", + AF_INET, SOCK_DGRAM); + expectSocketSuccess("Creating a AF_INET6 socket shouldn't have thrown ErrnoException", + AF_INET6, SOCK_DGRAM); + } + + private static void expectNoNetworking() { + expectSocketSuccess("Creating a UNIX socket should not have thrown ErrnoException", + AF_UNIX, SOCK_STREAM); + expectSocketPemissionError( + "Creating a AF_INET socket should have thrown ErrnoException(EPERM)", + AF_INET, SOCK_DGRAM); + expectSocketPemissionError( + "Creating a AF_INET6 socket should have thrown ErrnoException(EPERM)", + AF_INET6, SOCK_DGRAM); + } + + @Test + public void testSetAllowNetworkingForProcess() { + expectHasNetworking(); + NetworkUtilsInternal.setAllowNetworkingForProcess(false); + expectNoNetworking(); + NetworkUtilsInternal.setAllowNetworkingForProcess(true); + expectHasNetworking(); + } +} diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cb0f0710d6e7fc2fa592b2814bcdc55f4404dfd9 --- /dev/null +++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java @@ -0,0 +1,194 @@ +/* + * 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.internal.net; + +import static com.android.testutils.ParcelUtils.assertParcelSane; + +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 android.net.IpSecAlgorithm; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Unit tests for {@link VpnProfile}. */ +@SmallTest +@RunWith(JUnit4.class) +public class VpnProfileTest { + private static final String DUMMY_PROFILE_KEY = "Test"; + + private static final int ENCODED_INDEX_AUTH_PARAMS_INLINE = 23; + private static final int ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS = 24; + + @Test + public void testDefaults() throws Exception { + final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY); + + assertEquals(DUMMY_PROFILE_KEY, p.key); + assertEquals("", p.name); + assertEquals(VpnProfile.TYPE_PPTP, p.type); + assertEquals("", p.server); + assertEquals("", p.username); + assertEquals("", p.password); + assertEquals("", p.dnsServers); + assertEquals("", p.searchDomains); + assertEquals("", p.routes); + assertTrue(p.mppe); + assertEquals("", p.l2tpSecret); + assertEquals("", p.ipsecIdentifier); + assertEquals("", p.ipsecSecret); + assertEquals("", p.ipsecUserCert); + assertEquals("", p.ipsecCaCert); + assertEquals("", p.ipsecServerCert); + assertEquals(null, p.proxy); + assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty()); + assertFalse(p.isBypassable); + assertFalse(p.isMetered); + assertEquals(1360, p.maxMtu); + assertFalse(p.areAuthParamsInline); + assertFalse(p.isRestrictedToTestNetworks); + } + + private VpnProfile getSampleIkev2Profile(String key) { + final VpnProfile p = new VpnProfile(key, true /* isRestrictedToTestNetworks */); + + p.name = "foo"; + p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + p.server = "bar"; + p.username = "baz"; + p.password = "qux"; + p.dnsServers = "8.8.8.8"; + p.searchDomains = ""; + p.routes = "0.0.0.0/0"; + p.mppe = false; + p.l2tpSecret = ""; + p.ipsecIdentifier = "quux"; + p.ipsecSecret = "quuz"; + p.ipsecUserCert = "corge"; + p.ipsecCaCert = "grault"; + p.ipsecServerCert = "garply"; + p.proxy = null; + p.setAllowedAlgorithms( + Arrays.asList( + IpSecAlgorithm.AUTH_CRYPT_AES_GCM, + IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305, + IpSecAlgorithm.AUTH_HMAC_SHA512, + IpSecAlgorithm.CRYPT_AES_CBC)); + p.isBypassable = true; + p.isMetered = true; + p.maxMtu = 1350; + p.areAuthParamsInline = true; + + // Not saved, but also not compared. + p.saveLogin = true; + + return p; + } + + @Test + public void testEquals() { + assertEquals( + getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY)); + + final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + modified.maxMtu--; + assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified); + } + + @Test + public void testParcelUnparcel() { + assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 23); + } + + @Test + public void testEncodeDecode() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertEquals(profile, decoded); + } + + @Test + public void testEncodeDecodeTooManyValues() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + final byte[] tooManyValues = + (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes(); + + assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues)); + } + + private String getEncodedDecodedIkev2ProfileMissingValues(int... missingIndices) { + // Sort to ensure when we remove, we can do it from greatest first. + Arrays.sort(missingIndices); + + final String encoded = new String(getSampleIkev2Profile(DUMMY_PROFILE_KEY).encode()); + final List<String> parts = + new ArrayList<>(Arrays.asList(encoded.split(VpnProfile.VALUE_DELIMITER))); + + // Remove from back first to ensure indexing is consistent. + for (int i = missingIndices.length - 1; i >= 0; i--) { + parts.remove(missingIndices[i]); + } + + return String.join(VpnProfile.VALUE_DELIMITER, parts.toArray(new String[0])); + } + + @Test + public void testEncodeDecodeInvalidNumberOfValues() { + final String tooFewValues = + getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_AUTH_PARAMS_INLINE, + ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */); + + assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes())); + } + + @Test + public void testEncodeDecodeMissingIsRestrictedToTestNetworks() { + final String tooFewValues = + getEncodedDecodedIkev2ProfileMissingValues( + ENCODED_INDEX_RESTRICTED_TO_TEST_NETWORKS /* missingIndices */); + + // Verify decoding without isRestrictedToTestNetworks defaults to false + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues.getBytes()); + assertFalse(decoded.isRestrictedToTestNetworks); + } + + @Test + public void testEncodeDecodeLoginsNotSaved() { + final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY); + profile.saveLogin = false; + + final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode()); + assertNotEquals(profile, decoded); + + // Add the username/password back, everything else must be equal. + decoded.username = profile.username; + decoded.password = profile.password; + assertEquals(profile, decoded); + } +} diff --git a/tests/unit/java/com/android/internal/util/BitUtilsTest.java b/tests/unit/java/com/android/internal/util/BitUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d2fbdce9771afb2a48b146d0b005c5ecdd10a415 --- /dev/null +++ b/tests/unit/java/com/android/internal/util/BitUtilsTest.java @@ -0,0 +1,201 @@ +/* + * 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.internal.util; + +import static com.android.internal.util.BitUtils.bytesToBEInt; +import static com.android.internal.util.BitUtils.bytesToLEInt; +import static com.android.internal.util.BitUtils.getUint16; +import static com.android.internal.util.BitUtils.getUint32; +import static com.android.internal.util.BitUtils.getUint8; +import static com.android.internal.util.BitUtils.packBits; +import static com.android.internal.util.BitUtils.uint16; +import static com.android.internal.util.BitUtils.uint32; +import static com.android.internal.util.BitUtils.uint8; +import static com.android.internal.util.BitUtils.unpackBits; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Random; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BitUtilsTest { + + @Test + public void testUnsignedByteWideningConversions() { + byte b0 = 0; + byte b1 = 1; + byte bm1 = -1; + assertEquals(0, uint8(b0)); + assertEquals(1, uint8(b1)); + assertEquals(127, uint8(Byte.MAX_VALUE)); + assertEquals(128, uint8(Byte.MIN_VALUE)); + assertEquals(255, uint8(bm1)); + assertEquals(255, uint8((byte)255)); + } + + @Test + public void testUnsignedShortWideningConversions() { + short s0 = 0; + short s1 = 1; + short sm1 = -1; + assertEquals(0, uint16(s0)); + assertEquals(1, uint16(s1)); + assertEquals(32767, uint16(Short.MAX_VALUE)); + assertEquals(32768, uint16(Short.MIN_VALUE)); + assertEquals(65535, uint16(sm1)); + assertEquals(65535, uint16((short)65535)); + } + + @Test + public void testUnsignedShortComposition() { + byte b0 = 0; + byte b1 = 1; + byte b2 = 2; + byte b10 = 10; + byte b16 = 16; + byte b128 = -128; + byte b224 = -32; + byte b255 = -1; + assertEquals(0x0000, uint16(b0, b0)); + assertEquals(0xffff, uint16(b255, b255)); + assertEquals(0x0a01, uint16(b10, b1)); + assertEquals(0x8002, uint16(b128, b2)); + assertEquals(0x01ff, uint16(b1, b255)); + assertEquals(0x80ff, uint16(b128, b255)); + assertEquals(0xe010, uint16(b224, b16)); + } + + @Test + public void testUnsignedIntWideningConversions() { + assertEquals(0, uint32(0)); + assertEquals(1, uint32(1)); + assertEquals(2147483647L, uint32(Integer.MAX_VALUE)); + assertEquals(2147483648L, uint32(Integer.MIN_VALUE)); + assertEquals(4294967295L, uint32(-1)); + assertEquals(4294967295L, uint32((int)4294967295L)); + } + + @Test + public void testBytesToInt() { + assertEquals(0x00000000, bytesToBEInt(bytes(0, 0, 0, 0))); + assertEquals(0xffffffff, bytesToBEInt(bytes(255, 255, 255, 255))); + assertEquals(0x0a000001, bytesToBEInt(bytes(10, 0, 0, 1))); + assertEquals(0x0a000002, bytesToBEInt(bytes(10, 0, 0, 2))); + assertEquals(0x0a001fff, bytesToBEInt(bytes(10, 0, 31, 255))); + assertEquals(0xe0000001, bytesToBEInt(bytes(224, 0, 0, 1))); + + assertEquals(0x00000000, bytesToLEInt(bytes(0, 0, 0, 0))); + assertEquals(0x01020304, bytesToLEInt(bytes(4, 3, 2, 1))); + assertEquals(0xffff0000, bytesToLEInt(bytes(0, 0, 255, 255))); + } + + @Test + public void testUnsignedGetters() { + ByteBuffer b = ByteBuffer.allocate(4); + b.putInt(0xffff); + + assertEquals(0x0, getUint8(b, 0)); + assertEquals(0x0, getUint8(b, 1)); + assertEquals(0xff, getUint8(b, 2)); + assertEquals(0xff, getUint8(b, 3)); + + assertEquals(0x0, getUint16(b, 0)); + assertEquals(0xffff, getUint16(b, 2)); + + b.rewind(); + b.putInt(0xffffffff); + assertEquals(0xffffffffL, getUint32(b, 0)); + } + + @Test + public void testBitsPacking() { + BitPackingTestCase[] testCases = { + new BitPackingTestCase(0, ints()), + new BitPackingTestCase(1, ints(0)), + new BitPackingTestCase(2, ints(1)), + new BitPackingTestCase(3, ints(0, 1)), + new BitPackingTestCase(4, ints(2)), + new BitPackingTestCase(6, ints(1, 2)), + new BitPackingTestCase(9, ints(0, 3)), + new BitPackingTestCase(~Long.MAX_VALUE, ints(63)), + new BitPackingTestCase(~Long.MAX_VALUE + 1, ints(0, 63)), + new BitPackingTestCase(~Long.MAX_VALUE + 2, ints(1, 63)), + }; + for (BitPackingTestCase tc : testCases) { + int[] got = unpackBits(tc.packedBits); + assertTrue( + "unpackBits(" + + tc.packedBits + + "): expected " + + Arrays.toString(tc.bits) + + " but got " + + Arrays.toString(got), + Arrays.equals(tc.bits, got)); + } + for (BitPackingTestCase tc : testCases) { + long got = packBits(tc.bits); + assertEquals( + "packBits(" + + Arrays.toString(tc.bits) + + "): expected " + + tc.packedBits + + " but got " + + got, + tc.packedBits, + got); + } + + long[] moreTestCases = { + 0, 1, -1, 23895, -908235, Long.MAX_VALUE, Long.MIN_VALUE, new Random().nextLong(), + }; + for (long l : moreTestCases) { + assertEquals(l, packBits(unpackBits(l))); + } + } + + static byte[] bytes(int b1, int b2, int b3, int b4) { + return new byte[] {b(b1), b(b2), b(b3), b(b4)}; + } + + static byte b(int i) { + return (byte) i; + } + + static int[] ints(int... array) { + return array; + } + + static class BitPackingTestCase { + final int[] bits; + final long packedBits; + + BitPackingTestCase(long packedBits, int[] bits) { + this.bits = bits; + this.packedBits = packedBits; + } + } +} diff --git a/tests/unit/java/com/android/internal/util/RingBufferTest.java b/tests/unit/java/com/android/internal/util/RingBufferTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d06095a690cf2a7ed0f29180e658d458f33ac431 --- /dev/null +++ b/tests/unit/java/com/android/internal/util/RingBufferTest.java @@ -0,0 +1,178 @@ +/* + * 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.internal.util; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class RingBufferTest { + + @Test + public void testEmptyRingBuffer() { + RingBuffer<String> buffer = new RingBuffer<>(String.class, 100); + + assertArrayEquals(new String[0], buffer.toArray()); + } + + @Test + public void testIncorrectConstructorArguments() { + try { + RingBuffer<String> buffer = new RingBuffer<>(String.class, -10); + fail("Should not be able to create a negative capacity RingBuffer"); + } catch (IllegalArgumentException expected) { + } + + try { + RingBuffer<String> buffer = new RingBuffer<>(String.class, 0); + fail("Should not be able to create a 0 capacity RingBuffer"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testRingBufferWithNoWrapping() { + RingBuffer<String> buffer = new RingBuffer<>(String.class, 100); + + buffer.append("a"); + buffer.append("b"); + buffer.append("c"); + buffer.append("d"); + buffer.append("e"); + + String[] expected = {"a", "b", "c", "d", "e"}; + assertArrayEquals(expected, buffer.toArray()); + } + + @Test + public void testRingBufferWithCapacity1() { + RingBuffer<String> buffer = new RingBuffer<>(String.class, 1); + + buffer.append("a"); + assertArrayEquals(new String[]{"a"}, buffer.toArray()); + + buffer.append("b"); + assertArrayEquals(new String[]{"b"}, buffer.toArray()); + + buffer.append("c"); + assertArrayEquals(new String[]{"c"}, buffer.toArray()); + + buffer.append("d"); + assertArrayEquals(new String[]{"d"}, buffer.toArray()); + + buffer.append("e"); + assertArrayEquals(new String[]{"e"}, buffer.toArray()); + } + + @Test + public void testRingBufferWithWrapping() { + int capacity = 100; + RingBuffer<String> buffer = new RingBuffer<>(String.class, capacity); + + buffer.append("a"); + buffer.append("b"); + buffer.append("c"); + buffer.append("d"); + buffer.append("e"); + + String[] expected1 = {"a", "b", "c", "d", "e"}; + assertArrayEquals(expected1, buffer.toArray()); + + String[] expected2 = new String[capacity]; + int firstIndex = 0; + int lastIndex = capacity - 1; + + expected2[firstIndex] = "e"; + for (int i = 1; i < capacity; i++) { + buffer.append("x"); + expected2[i] = "x"; + } + assertArrayEquals(expected2, buffer.toArray()); + + buffer.append("x"); + expected2[firstIndex] = "x"; + assertArrayEquals(expected2, buffer.toArray()); + + for (int i = 0; i < 10; i++) { + for (String s : expected2) { + buffer.append(s); + } + } + assertArrayEquals(expected2, buffer.toArray()); + + buffer.append("a"); + expected2[lastIndex] = "a"; + assertArrayEquals(expected2, buffer.toArray()); + } + + @Test + public void testGetNextSlot() { + int capacity = 100; + RingBuffer<DummyClass1> buffer = new RingBuffer<>(DummyClass1.class, capacity); + + final DummyClass1[] actual = new DummyClass1[capacity]; + final DummyClass1[] expected = new DummyClass1[capacity]; + for (int i = 0; i < capacity; ++i) { + final DummyClass1 obj = buffer.getNextSlot(); + obj.x = capacity * i; + actual[i] = obj; + expected[i] = new DummyClass1(); + expected[i].x = capacity * i; + } + assertArrayEquals(expected, buffer.toArray()); + + for (int i = 0; i < capacity; ++i) { + if (actual[i] != buffer.getNextSlot()) { + fail("getNextSlot() should re-use objects if available"); + } + } + + RingBuffer<DummyClass2> buffer2 = new RingBuffer<>(DummyClass2.class, capacity); + assertNull("getNextSlot() should return null if the object can't be initiated " + + "(No nullary constructor)", buffer2.getNextSlot()); + + RingBuffer<DummyClass3> buffer3 = new RingBuffer<>(DummyClass3.class, capacity); + assertNull("getNextSlot() should return null if the object can't be initiated " + + "(Inaccessible class)", buffer3.getNextSlot()); + } + + public static final class DummyClass1 { + int x; + + public boolean equals(Object o) { + if (o instanceof DummyClass1) { + final DummyClass1 other = (DummyClass1) o; + return other.x == this.x; + } + return false; + } + } + + public static final class DummyClass2 { + public DummyClass2(int x) {} + } + + private static final class DummyClass3 {} +} diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..466138550aae76b44d860043eac86dc8c1ad237f --- /dev/null +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -0,0 +1,12901 @@ +/* + * 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 com.android.server; + +import static android.Manifest.permission.CHANGE_NETWORK_STATE; +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.DUMP; +import static android.Manifest.permission.LOCAL_MAC_ADDRESS; +import static android.Manifest.permission.NETWORK_FACTORY; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.ACTION_PACKAGE_REPLACED; +import static android.content.Intent.ACTION_USER_ADDED; +import static android.content.Intent.ACTION_USER_REMOVED; +import static android.content.Intent.ACTION_USER_UNLOCKED; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +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.MATCH_ANY_USER; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN; +import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER; +import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK; +import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED; +import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER; +import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; +import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO; +import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE; +import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT; +import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA; +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; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_FALLBACK; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL; +import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_BIP; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_EIMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID; +import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE; +import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY; +import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_VSIM; +import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P; +import static android.net.NetworkCapabilities.NET_CAPABILITY_XCAP; +import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; +import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.REDACT_NONE; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; +import static android.net.NetworkScore.KEEP_CONNECTED_FOR_HANDOVER; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; +import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED; +import static android.net.RouteInfo.RTN_UNREACHABLE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_REMOVED; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS; +import static android.os.Process.INVALID_UID; +import static android.system.OsConstants.IPPROTO_TCP; + +import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType; +import static com.android.testutils.ConcurrentUtils.await; +import static com.android.testutils.ConcurrentUtils.durationOf; +import static com.android.testutils.ExceptionUtils.ignoreExceptions; +import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor; +import static com.android.testutils.MiscAsserts.assertContainsAll; +import static com.android.testutils.MiscAsserts.assertContainsExactly; +import static com.android.testutils.MiscAsserts.assertEmpty; +import static com.android.testutils.MiscAsserts.assertLength; +import static com.android.testutils.MiscAsserts.assertRunsInAtMost; +import static com.android.testutils.MiscAsserts.assertSameElements; +import static com.android.testutils.MiscAsserts.assertThrows; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +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.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.app.AppOpsManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.usage.NetworkStatsManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentProvider; +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.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.location.LocationManager; +import android.net.CaptivePortalData; +import android.net.ConnectionInfo; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.ConnectivityManager.PacketKeepalive; +import android.net.ConnectivityManager.PacketKeepaliveCallback; +import android.net.ConnectivityManager.TooManyRequestsException; +import android.net.ConnectivityResources; +import android.net.ConnectivitySettingsManager; +import android.net.ConnectivityThread; +import android.net.DataStallReportParcelable; +import android.net.EthernetManager; +import android.net.IConnectivityDiagnosticsCallback; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.INetworkMonitor; +import android.net.INetworkMonitorCallbacks; +import android.net.IOnCompleteListener; +import android.net.IQosCallback; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MatchAllNetworkSpecifier; +import android.net.NativeNetworkConfig; +import android.net.NativeNetworkType; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkFactory; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkPolicyManager; +import android.net.NetworkPolicyManager.NetworkPolicyCallback; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.NetworkSpecifier; +import android.net.NetworkStack; +import android.net.NetworkStateSnapshot; +import android.net.NetworkTestResultParcelable; +import android.net.OemNetworkPreferences; +import android.net.ProxyInfo; +import android.net.QosCallbackException; +import android.net.QosFilter; +import android.net.QosSession; +import android.net.ResolverParamsParcel; +import android.net.RouteInfo; +import android.net.RouteInfoParcel; +import android.net.SocketKeepalive; +import android.net.TransportInfo; +import android.net.UidRange; +import android.net.UidRangeParcel; +import android.net.UnderlyingNetworkInfo; +import android.net.Uri; +import android.net.VpnManager; +import android.net.VpnTransportInfo; +import android.net.metrics.IpConnectivityLog; +import android.net.networkstack.NetworkStackClientBase; +import android.net.resolv.aidl.Nat64PrefixEventParcel; +import android.net.resolv.aidl.PrivateDnsValidationEventParcel; +import android.net.shared.NetworkMonitorUtils; +import android.net.shared.PrivateDnsConfig; +import android.net.util.MultinetworkPolicyTracker; +import android.os.BadParcelableException; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.SystemClock; +import android.os.SystemConfigManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.security.Credentials; +import android.system.Os; +import android.telephony.TelephonyManager; +import android.telephony.data.EpsBearerQosSessionAttributes; +import android.telephony.data.NrQosSessionAttributes; +import android.test.mock.MockContentResolver; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; +import android.util.Range; +import android.util.SparseArray; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.WakeupMessage; +import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.net.module.util.ArrayTrackRecord; +import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo; +import com.android.server.connectivity.MockableSystemProperties; +import com.android.server.connectivity.Nat464Xlat; +import com.android.server.connectivity.NetworkAgentInfo; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; +import com.android.server.connectivity.ProxyTracker; +import com.android.server.connectivity.QosCallbackTracker; +import com.android.server.connectivity.Vpn; +import com.android.server.connectivity.VpnProfileStore; +import com.android.server.net.NetworkPinner; +import com.android.testutils.ExceptionUtils; +import com.android.testutils.HandlerUtils; +import com.android.testutils.RecorderCallback.CallbackEntry; +import com.android.testutils.TestableNetworkCallback; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.stubbing.Answer; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import kotlin.reflect.KClass; + +/** + * Tests for {@link ConnectivityService}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.ConnectivityServiceTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityServiceTest { + private static final String TAG = "ConnectivityServiceTest"; + + private static final int TIMEOUT_MS = 500; + // Broadcasts can take a long time to be delivered. The test will not wait for that long unless + // there is a failure, so use a long timeout. + private static final int BROADCAST_TIMEOUT_MS = 30_000; + private static final int TEST_LINGER_DELAY_MS = 400; + private static final int TEST_NASCENT_DELAY_MS = 300; + // Chosen to be less than the linger and nascent timeout. This ensures that we can distinguish + // between a LOST callback that arrives immediately and a LOST callback that arrives after + // the linger/nascent timeout. For this, our assertions should run fast enough to leave + // less than (mService.mLingerDelayMs - TEST_CALLBACK_TIMEOUT_MS) between the time callbacks are + // supposedly fired, and the time we call expectCallback. + private static final int TEST_CALLBACK_TIMEOUT_MS = 250; + // Chosen to be less than TEST_CALLBACK_TIMEOUT_MS. This ensures that requests have time to + // complete before callbacks are verified. + private static final int TEST_REQUEST_TIMEOUT_MS = 150; + + private static final int UNREASONABLY_LONG_ALARM_WAIT_MS = 1000; + + private static final long TIMESTAMP = 1234L; + + private static final int NET_ID = 110; + private static final int OEM_PREF_ANY_NET_ID = -1; + // Set a non-zero value to verify the flow to set tcp init rwnd value. + private static final int TEST_TCP_INIT_RWND = 60; + + // Used for testing the per-work-profile default network. + private static final int TEST_APP_ID = 103; + private static final int TEST_WORK_PROFILE_USER_ID = 2; + private static final int TEST_WORK_PROFILE_APP_UID = + UserHandle.getUid(TEST_WORK_PROFILE_USER_ID, TEST_APP_ID); + private static final String CLAT_PREFIX = "v4-"; + private static final String MOBILE_IFNAME = "test_rmnet_data0"; + private static final String WIFI_IFNAME = "test_wlan0"; + private static final String WIFI_WOL_IFNAME = "test_wlan_wol"; + private static final String VPN_IFNAME = "tun10042"; + private static final String TEST_PACKAGE_NAME = "com.android.test.package"; + private static final int TEST_PACKAGE_UID = 123; + private static final String ALWAYS_ON_PACKAGE = "com.android.test.alwaysonvpn"; + + private static final String INTERFACE_NAME = "interface"; + + private static final String TEST_VENUE_URL_NA_PASSPOINT = "https://android.com/"; + private static final String TEST_VENUE_URL_NA_OTHER = "https://example.com/"; + private static final String TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT = + "https://android.com/terms/"; + private static final String TEST_TERMS_AND_CONDITIONS_URL_NA_OTHER = + "https://example.com/terms/"; + private static final String TEST_VENUE_URL_CAPPORT = "https://android.com/capport/"; + private static final String TEST_USER_PORTAL_API_URL_CAPPORT = + "https://android.com/user/api/capport/"; + private static final String TEST_FRIENDLY_NAME = "Network friendly name"; + private static final String TEST_REDIRECT_URL = "http://example.com/firstPath"; + + private MockContext mServiceContext; + private HandlerThread mCsHandlerThread; + private HandlerThread mVMSHandlerThread; + private ConnectivityService.Dependencies mDeps; + private ConnectivityService mService; + private WrappedConnectivityManager mCm; + private TestNetworkAgentWrapper mWiFiNetworkAgent; + private TestNetworkAgentWrapper mCellNetworkAgent; + private TestNetworkAgentWrapper mEthernetNetworkAgent; + private MockVpn mMockVpn; + private Context mContext; + private NetworkPolicyCallback mPolicyCallback; + private WrappedMultinetworkPolicyTracker mPolicyTracker; + private HandlerThread mAlarmManagerThread; + private TestNetIdManager mNetIdManager; + private QosCallbackMockHelper mQosCallbackMockHelper; + private QosCallbackTracker mQosCallbackTracker; + private VpnManagerService mVpnManagerService; + private TestNetworkCallback mDefaultNetworkCallback; + private TestNetworkCallback mSystemDefaultNetworkCallback; + private TestNetworkCallback mProfileDefaultNetworkCallback; + + // State variables required to emulate NetworkPolicyManagerService behaviour. + private int mBlockedReasons = BLOCKED_REASON_NONE; + + @Mock DeviceIdleInternal mDeviceIdleInternal; + @Mock INetworkManagementService mNetworkManagementService; + @Mock NetworkStatsManager mStatsManager; + @Mock IDnsResolver mMockDnsResolver; + @Mock INetd mMockNetd; + @Mock NetworkStackClientBase mNetworkStack; + @Mock PackageManager mPackageManager; + @Mock UserManager mUserManager; + @Mock NotificationManager mNotificationManager; + @Mock AlarmManager mAlarmManager; + @Mock IConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback; + @Mock IBinder mIBinder; + @Mock LocationManager mLocationManager; + @Mock AppOpsManager mAppOpsManager; + @Mock TelephonyManager mTelephonyManager; + @Mock MockableSystemProperties mSystemProperties; + @Mock EthernetManager mEthernetManager; + @Mock NetworkPolicyManager mNetworkPolicyManager; + @Mock VpnProfileStore mVpnProfileStore; + @Mock SystemConfigManager mSystemConfigManager; + @Mock Resources mResources; + + private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor = + ArgumentCaptor.forClass(ResolverParamsParcel.class); + + // This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods + // do not go through ConnectivityService but talk to netd directly, so they don't automatically + // reflect the state of our test ConnectivityService. + private class WrappedConnectivityManager extends ConnectivityManager { + private Network mFakeBoundNetwork; + + public synchronized boolean bindProcessToNetwork(Network network) { + mFakeBoundNetwork = network; + return true; + } + + public synchronized Network getBoundNetworkForProcess() { + return mFakeBoundNetwork; + } + + public WrappedConnectivityManager(Context context, ConnectivityService service) { + super(context, service); + } + } + + private class MockContext extends BroadcastInterceptingContext { + private final MockContentResolver mContentResolver; + + @Spy private Resources mInternalResources; + private final LinkedBlockingQueue<Intent> mStartedActivities = new LinkedBlockingQueue<>(); + + // Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant + private final HashMap<String, Integer> mMockedPermissions = new HashMap<>(); + + MockContext(Context base, ContentProvider settingsProvider) { + super(base); + + mInternalResources = spy(base.getResources()); + when(mInternalResources.getStringArray(com.android.internal.R.array.networkAttributes)) + .thenReturn(new String[] { + "wifi,1,1,1,-1,true", + "mobile,0,0,0,-1,true", + "mobile_mms,2,0,2,60000,true", + "mobile_supl,3,0,2,60000,true", + }); + + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, settingsProvider); + } + + @Override + public void startActivityAsUser(Intent intent, UserHandle handle) { + mStartedActivities.offer(intent); + } + + public Intent expectStartActivityIntent(int timeoutMs) { + Intent intent = null; + try { + intent = mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) {} + assertNotNull("Did not receive sign-in intent after " + timeoutMs + "ms", intent); + return intent; + } + + public void expectNoStartActivityIntent(int timeoutMs) { + try { + assertNull("Received unexpected Intent to start activity", + mStartedActivities.poll(timeoutMs, TimeUnit.MILLISECONDS)); + } catch (InterruptedException e) {} + } + + @Override + public ComponentName startService(Intent service) { + final String action = service.getAction(); + if (!VpnConfig.SERVICE_INTERFACE.equals(action)) { + fail("Attempt to start unknown service, action=" + action); + } + return new ComponentName(service.getPackage(), "com.android.test.Service"); + } + + @Override + public Object getSystemService(String name) { + if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm; + if (Context.NOTIFICATION_SERVICE.equals(name)) return mNotificationManager; + if (Context.USER_SERVICE.equals(name)) return mUserManager; + if (Context.ALARM_SERVICE.equals(name)) return mAlarmManager; + if (Context.LOCATION_SERVICE.equals(name)) return mLocationManager; + if (Context.APP_OPS_SERVICE.equals(name)) return mAppOpsManager; + if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager; + if (Context.ETHERNET_SERVICE.equals(name)) return mEthernetManager; + if (Context.NETWORK_POLICY_SERVICE.equals(name)) return mNetworkPolicyManager; + if (Context.SYSTEM_CONFIG_SERVICE.equals(name)) return mSystemConfigManager; + if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager; + return super.getSystemService(name); + } + + final HashMap<UserHandle, UserManager> mUserManagers = new HashMap<>(); + @Override + public Context createContextAsUser(UserHandle user, int flags) { + final Context asUser = mock(Context.class, AdditionalAnswers.delegatesTo(this)); + doReturn(user).when(asUser).getUser(); + doAnswer((inv) -> { + final UserManager um = mUserManagers.computeIfAbsent(user, + u -> mock(UserManager.class, AdditionalAnswers.delegatesTo(mUserManager))); + return um; + }).when(asUser).getSystemService(Context.USER_SERVICE); + return asUser; + } + + public void setWorkProfile(@NonNull final UserHandle userHandle, boolean value) { + // This relies on all contexts for a given user returning the same UM mock + final UserManager umMock = createContextAsUser(userHandle, 0 /* flags */) + .getSystemService(UserManager.class); + doReturn(value).when(umMock).isManagedProfile(); + doReturn(value).when(mUserManager).isManagedProfile(eq(userHandle.getIdentifier())); + } + + @Override + public ContentResolver getContentResolver() { + return mContentResolver; + } + + @Override + public Resources getResources() { + return mInternalResources; + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + private int checkMockedPermission(String permission, Supplier<Integer> ifAbsent) { + final Integer granted = mMockedPermissions.get(permission); + return granted != null ? granted : ifAbsent.get(); + } + + @Override + public int checkPermission(String permission, int pid, int uid) { + return checkMockedPermission( + permission, () -> super.checkPermission(permission, pid, uid)); + } + + @Override + public int checkCallingOrSelfPermission(String permission) { + return checkMockedPermission( + permission, () -> super.checkCallingOrSelfPermission(permission)); + } + + @Override + public void enforceCallingOrSelfPermission(String permission, String message) { + final Integer granted = mMockedPermissions.get(permission); + if (granted == null) { + super.enforceCallingOrSelfPermission(permission, message); + return; + } + + if (!granted.equals(PERMISSION_GRANTED)) { + throw new SecurityException("[Test] permission denied: " + permission); + } + } + + /** + * Mock checks for the specified permission, and have them behave as per {@code granted}. + * + * <p>Passing null reverts to default behavior, which does a real permission check on the + * test package. + * @param granted One of {@link PackageManager#PERMISSION_GRANTED} or + * {@link PackageManager#PERMISSION_DENIED}. + */ + public void setPermission(String permission, Integer granted) { + mMockedPermissions.put(permission, granted); + } + } + + private void waitForIdle() { + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + waitForIdle(mCellNetworkAgent, TIMEOUT_MS); + waitForIdle(mWiFiNetworkAgent, TIMEOUT_MS); + waitForIdle(mEthernetNetworkAgent, TIMEOUT_MS); + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + HandlerUtils.waitForIdle(ConnectivityThread.get(), TIMEOUT_MS); + } + + private void waitForIdle(TestNetworkAgentWrapper agent, long timeoutMs) { + if (agent == null) { + return; + } + agent.waitForIdle(timeoutMs); + } + + @Test + public void testWaitForIdle() throws Exception { + final int attempts = 50; // Causes the test to take about 200ms on bullhead-eng. + + // Tests that waitForIdle returns immediately if the service is already idle. + for (int i = 0; i < attempts; i++) { + waitForIdle(); + } + + // Bring up a network that we can use to send messages to ConnectivityService. + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + Network n = mWiFiNetworkAgent.getNetwork(); + assertNotNull(n); + + // Tests that calling waitForIdle waits for messages to be processed. + for (int i = 0; i < attempts; i++) { + mWiFiNetworkAgent.setSignalStrength(i); + waitForIdle(); + assertEquals(i, mCm.getNetworkCapabilities(n).getSignalStrength()); + } + } + + // This test has an inherent race condition in it, and cannot be enabled for continuous testing + // or presubmit tests. It is kept for manual runs and documentation purposes. + @Ignore + public void verifyThatNotWaitingForIdleCausesRaceConditions() throws Exception { + // Bring up a network that we can use to send messages to ConnectivityService. + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + Network n = mWiFiNetworkAgent.getNetwork(); + assertNotNull(n); + + // Ensure that not calling waitForIdle causes a race condition. + final int attempts = 50; // Causes the test to take about 200ms on bullhead-eng. + for (int i = 0; i < attempts; i++) { + mWiFiNetworkAgent.setSignalStrength(i); + if (i != mCm.getNetworkCapabilities(n).getSignalStrength()) { + // We hit a race condition, as expected. Pass the test. + return; + } + } + + // No race? There is a bug in this test. + fail("expected race condition at least once in " + attempts + " attempts"); + } + + private class TestNetworkAgentWrapper extends NetworkAgentWrapper { + private static final int VALIDATION_RESULT_INVALID = 0; + + private static final long DATA_STALL_TIMESTAMP = 10L; + private static final int DATA_STALL_DETECTION_METHOD = 1; + + private INetworkMonitor mNetworkMonitor; + private INetworkMonitorCallbacks mNmCallbacks; + private int mNmValidationResult = VALIDATION_RESULT_INVALID; + private int mProbesCompleted; + private int mProbesSucceeded; + private String mNmValidationRedirectUrl = null; + private boolean mNmProvNotificationRequested = false; + private Runnable mCreatedCallback; + private Runnable mUnwantedCallback; + private Runnable mDisconnectedCallback; + + private final ConditionVariable mNetworkStatusReceived = new ConditionVariable(); + // Contains the redirectUrl from networkStatus(). Before reading, wait for + // mNetworkStatusReceived. + private String mRedirectUrl; + + TestNetworkAgentWrapper(int transport) throws Exception { + this(transport, new LinkProperties(), null); + } + + TestNetworkAgentWrapper(int transport, LinkProperties linkProperties) + throws Exception { + this(transport, linkProperties, null); + } + + private TestNetworkAgentWrapper(int transport, LinkProperties linkProperties, + NetworkCapabilities ncTemplate) throws Exception { + super(transport, linkProperties, ncTemplate, mServiceContext); + + // Waits for the NetworkAgent to be registered, which includes the creation of the + // NetworkMonitor. + waitForIdle(TIMEOUT_MS); + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + HandlerUtils.waitForIdle(ConnectivityThread.get(), TIMEOUT_MS); + } + + @Override + protected InstrumentedNetworkAgent makeNetworkAgent(LinkProperties linkProperties, + NetworkAgentConfig nac) throws Exception { + mNetworkMonitor = mock(INetworkMonitor.class); + + final Answer validateAnswer = inv -> { + new Thread(ignoreExceptions(this::onValidationRequested)).start(); + return null; + }; + + doAnswer(validateAnswer).when(mNetworkMonitor).notifyNetworkConnected(any(), any()); + doAnswer(validateAnswer).when(mNetworkMonitor).forceReevaluation(anyInt()); + + final ArgumentCaptor<Network> nmNetworkCaptor = ArgumentCaptor.forClass(Network.class); + final ArgumentCaptor<INetworkMonitorCallbacks> nmCbCaptor = + ArgumentCaptor.forClass(INetworkMonitorCallbacks.class); + doNothing().when(mNetworkStack).makeNetworkMonitor( + nmNetworkCaptor.capture(), + any() /* name */, + nmCbCaptor.capture()); + + final InstrumentedNetworkAgent na = + new InstrumentedNetworkAgent(this, linkProperties, nac) { + @Override + public void networkStatus(int status, String redirectUrl) { + mRedirectUrl = redirectUrl; + mNetworkStatusReceived.open(); + } + + @Override + public void onNetworkCreated() { + super.onNetworkCreated(); + if (mCreatedCallback != null) mCreatedCallback.run(); + } + + @Override + public void onNetworkUnwanted() { + super.onNetworkUnwanted(); + if (mUnwantedCallback != null) mUnwantedCallback.run(); + } + + @Override + public void onNetworkDestroyed() { + super.onNetworkDestroyed(); + if (mDisconnectedCallback != null) mDisconnectedCallback.run(); + } + }; + + assertEquals(na.getNetwork().netId, nmNetworkCaptor.getValue().netId); + mNmCallbacks = nmCbCaptor.getValue(); + + mNmCallbacks.onNetworkMonitorCreated(mNetworkMonitor); + + return na; + } + + private void onValidationRequested() throws Exception { + if (mNmProvNotificationRequested + && ((mNmValidationResult & NETWORK_VALIDATION_RESULT_VALID) != 0)) { + mNmCallbacks.hideProvisioningNotification(); + mNmProvNotificationRequested = false; + } + + mNmCallbacks.notifyProbeStatusChanged(mProbesCompleted, mProbesSucceeded); + final NetworkTestResultParcelable p = new NetworkTestResultParcelable(); + p.result = mNmValidationResult; + p.probesAttempted = mProbesCompleted; + p.probesSucceeded = mProbesSucceeded; + p.redirectUrl = mNmValidationRedirectUrl; + p.timestampMillis = TIMESTAMP; + mNmCallbacks.notifyNetworkTestedWithExtras(p); + + if (mNmValidationRedirectUrl != null) { + mNmCallbacks.showProvisioningNotification( + "test_provisioning_notif_action", TEST_PACKAGE_NAME); + mNmProvNotificationRequested = true; + } + } + + /** + * Connect without adding any internet capability. + */ + public void connectWithoutInternet() { + super.connect(); + } + + /** + * Transition this NetworkAgent to CONNECTED state with NET_CAPABILITY_INTERNET. + * @param validated Indicate if network should pretend to be validated. + */ + public void connect(boolean validated) { + connect(validated, true, false /* isStrictMode */); + } + + /** + * Transition this NetworkAgent to CONNECTED state. + * @param validated Indicate if network should pretend to be validated. + * @param hasInternet Indicate if network should pretend to have NET_CAPABILITY_INTERNET. + */ + public void connect(boolean validated, boolean hasInternet, boolean isStrictMode) { + assertFalse(getNetworkCapabilities().hasCapability(NET_CAPABILITY_INTERNET)); + + ConnectivityManager.NetworkCallback callback = null; + final ConditionVariable validatedCv = new ConditionVariable(); + if (validated) { + setNetworkValid(isStrictMode); + NetworkRequest request = new NetworkRequest.Builder() + .addTransportType(getNetworkCapabilities().getTransportTypes()[0]) + .clearCapabilities() + .build(); + callback = new ConnectivityManager.NetworkCallback() { + public void onCapabilitiesChanged(Network network, + NetworkCapabilities networkCapabilities) { + if (network.equals(getNetwork()) && + networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) { + validatedCv.open(); + } + } + }; + mCm.registerNetworkCallback(request, callback); + } + if (hasInternet) { + addCapability(NET_CAPABILITY_INTERNET); + } + + connectWithoutInternet(); + + if (validated) { + // Wait for network to validate. + waitFor(validatedCv); + setNetworkInvalid(isStrictMode); + } + + if (callback != null) mCm.unregisterNetworkCallback(callback); + } + + public void connectWithCaptivePortal(String redirectUrl, boolean isStrictMode) { + setNetworkPortal(redirectUrl, isStrictMode); + connect(false, true /* hasInternet */, isStrictMode); + } + + public void connectWithPartialConnectivity() { + setNetworkPartial(); + connect(false); + } + + public void connectWithPartialValidConnectivity(boolean isStrictMode) { + setNetworkPartialValid(isStrictMode); + connect(false, true /* hasInternet */, isStrictMode); + } + + void setNetworkValid(boolean isStrictMode) { + mNmValidationResult = NETWORK_VALIDATION_RESULT_VALID; + mNmValidationRedirectUrl = null; + int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS; + if (isStrictMode) { + probesSucceeded |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + // The probesCompleted equals to probesSucceeded for the case of valid network, so put + // the same value into two different parameter of the method. + setProbesStatus(probesSucceeded, probesSucceeded); + } + + void setNetworkInvalid(boolean isStrictMode) { + mNmValidationResult = VALIDATION_RESULT_INVALID; + mNmValidationRedirectUrl = null; + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS + | NETWORK_VALIDATION_PROBE_HTTP; + int probesSucceeded = 0; + // If the isStrictMode is true, it means the network is invalid when NetworkMonitor + // tried to validate the private DNS but failed. + if (isStrictMode) { + probesCompleted &= ~NETWORK_VALIDATION_PROBE_HTTP; + probesSucceeded = probesCompleted; + probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setNetworkPortal(String redirectUrl, boolean isStrictMode) { + setNetworkInvalid(isStrictMode); + mNmValidationRedirectUrl = redirectUrl; + // Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP + // in the beginning, so the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet. + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP; + int probesSucceeded = VALIDATION_RESULT_INVALID; + if (isStrictMode) { + probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setNetworkPartial() { + mNmValidationResult = NETWORK_VALIDATION_RESULT_PARTIAL; + mNmValidationRedirectUrl = null; + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS + | NETWORK_VALIDATION_PROBE_FALLBACK; + int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK; + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setNetworkPartialValid(boolean isStrictMode) { + setNetworkPartial(); + mNmValidationResult |= NETWORK_VALIDATION_RESULT_VALID; + int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS + | NETWORK_VALIDATION_PROBE_HTTP; + int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP; + // Suppose the partial network cannot pass the private DNS validation as well, so only + // add NETWORK_VALIDATION_PROBE_DNS in probesCompleted but not probesSucceeded. + if (isStrictMode) { + probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS; + } + setProbesStatus(probesCompleted, probesSucceeded); + } + + void setProbesStatus(int probesCompleted, int probesSucceeded) { + mProbesCompleted = probesCompleted; + mProbesSucceeded = probesSucceeded; + } + + void notifyCapportApiDataChanged(CaptivePortalData data) { + try { + mNmCallbacks.notifyCaptivePortalDataChanged(data); + } catch (RemoteException e) { + throw new AssertionError("This cannot happen", e); + } + } + + public String waitForRedirectUrl() { + assertTrue(mNetworkStatusReceived.block(TIMEOUT_MS)); + return mRedirectUrl; + } + + public void expectDisconnected() { + expectDisconnected(TIMEOUT_MS); + } + + public void expectPreventReconnectReceived() { + expectPreventReconnectReceived(TIMEOUT_MS); + } + + void notifyDataStallSuspected() throws Exception { + final DataStallReportParcelable p = new DataStallReportParcelable(); + p.detectionMethod = DATA_STALL_DETECTION_METHOD; + p.timestampMillis = DATA_STALL_TIMESTAMP; + mNmCallbacks.notifyDataStallSuspected(p); + } + + public void setCreatedCallback(Runnable r) { + mCreatedCallback = r; + } + + public void setUnwantedCallback(Runnable r) { + mUnwantedCallback = r; + } + + public void setDisconnectedCallback(Runnable r) { + mDisconnectedCallback = r; + } + } + + /** + * A NetworkFactory that allows to wait until any in-flight NetworkRequest add or remove + * operations have been processed and test for them. + */ + private static class MockNetworkFactory extends NetworkFactory { + private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false); + + static class RequestEntry { + @NonNull + public final NetworkRequest request; + + RequestEntry(@NonNull final NetworkRequest request) { + this.request = request; + } + + static final class Add extends RequestEntry { + Add(@NonNull final NetworkRequest request) { + super(request); + } + } + + static final class Remove extends RequestEntry { + Remove(@NonNull final NetworkRequest request) { + super(request); + } + } + + @Override + public String toString() { + return "RequestEntry [ " + getClass().getName() + " : " + request + " ]"; + } + } + + // History of received requests adds and removes. + private final ArrayTrackRecord<RequestEntry>.ReadHead mRequestHistory = + new ArrayTrackRecord<RequestEntry>().newReadHead(); + + private static <T> T failIfNull(@Nullable final T obj, @Nullable final String message) { + if (null == obj) fail(null != message ? message : "Must not be null"); + return obj; + } + + public RequestEntry.Add expectRequestAdd() { + return failIfNull((RequestEntry.Add) mRequestHistory.poll(TIMEOUT_MS, + it -> it instanceof RequestEntry.Add), "Expected request add"); + } + + public void expectRequestAdds(final int count) { + for (int i = count; i > 0; --i) { + expectRequestAdd(); + } + } + + public RequestEntry.Remove expectRequestRemove() { + return failIfNull((RequestEntry.Remove) mRequestHistory.poll(TIMEOUT_MS, + it -> it instanceof RequestEntry.Remove), "Expected request remove"); + } + + public void expectRequestRemoves(final int count) { + for (int i = count; i > 0; --i) { + expectRequestRemove(); + } + } + + // Used to collect the networks requests managed by this factory. This is a duplicate of + // the internal information stored in the NetworkFactory (which is private). + private SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>(); + private final HandlerThread mHandlerSendingRequests; + + public MockNetworkFactory(Looper looper, Context context, String logTag, + NetworkCapabilities filter, HandlerThread threadSendingRequests) { + super(looper, context, logTag, filter); + mHandlerSendingRequests = threadSendingRequests; + } + + public int getMyRequestCount() { + return getRequestCount(); + } + + protected void startNetwork() { + mNetworkStarted.set(true); + } + + protected void stopNetwork() { + mNetworkStarted.set(false); + } + + public boolean getMyStartRequested() { + return mNetworkStarted.get(); + } + + + @Override + protected void needNetworkFor(NetworkRequest request) { + mNetworkRequests.put(request.requestId, request); + super.needNetworkFor(request); + mRequestHistory.add(new RequestEntry.Add(request)); + } + + @Override + protected void releaseNetworkFor(NetworkRequest request) { + mNetworkRequests.remove(request.requestId); + super.releaseNetworkFor(request); + mRequestHistory.add(new RequestEntry.Remove(request)); + } + + public void assertRequestCountEquals(final int count) { + assertEquals(count, getMyRequestCount()); + } + + @Override + public void terminate() { + super.terminate(); + // Make sure there are no remaining requests unaccounted for. + HandlerUtils.waitForIdle(mHandlerSendingRequests, TIMEOUT_MS); + assertNull(mRequestHistory.poll(0, r -> true)); + } + + // Trigger releasing the request as unfulfillable + public void triggerUnfulfillable(NetworkRequest r) { + super.releaseRequestAsUnfulfillableByAnyFactory(r); + } + + public void assertNoRequestChanged() { + assertNull(mRequestHistory.poll(0, r -> true)); + } + } + + private Set<UidRange> uidRangesForUids(int... uids) { + final ArraySet<UidRange> ranges = new ArraySet<>(); + for (final int uid : uids) { + ranges.add(new UidRange(uid, uid)); + } + return ranges; + } + + private static Looper startHandlerThreadAndReturnLooper() { + final HandlerThread handlerThread = new HandlerThread("MockVpnThread"); + handlerThread.start(); + return handlerThread.getLooper(); + } + + private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork { + // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does + // not inherit from NetworkAgent. + private TestNetworkAgentWrapper mMockNetworkAgent; + private boolean mAgentRegistered = false; + + private int mVpnType = VpnManager.TYPE_VPN_SERVICE; + private UnderlyingNetworkInfo mUnderlyingNetworkInfo; + + // These ConditionVariables allow tests to wait for LegacyVpnRunner to be stopped/started. + // TODO: this scheme is ad-hoc and error-prone because it does not fail if, for example, the + // test expects two starts in a row, or even if the production code calls start twice in a + // row. find a better solution. Simply putting a method to create a LegacyVpnRunner into + // Vpn.Dependencies doesn't work because LegacyVpnRunner is not a static class and has + // extensive access into the internals of Vpn. + private ConditionVariable mStartLegacyVpnCv = new ConditionVariable(); + private ConditionVariable mStopVpnRunnerCv = new ConditionVariable(); + + public MockVpn(int userId) { + super(startHandlerThreadAndReturnLooper(), mServiceContext, + new Dependencies() { + @Override + public boolean isCallerSystem() { + return true; + } + + @Override + public DeviceIdleInternal getDeviceIdleInternal() { + return mDeviceIdleInternal; + } + }, + mNetworkManagementService, mMockNetd, userId, mVpnProfileStore); + } + + public void setUids(Set<UidRange> uids) { + mNetworkCapabilities.setUids(UidRange.toIntRanges(uids)); + if (mAgentRegistered) { + mMockNetworkAgent.setNetworkCapabilities(mNetworkCapabilities, true); + } + } + + public void setVpnType(int vpnType) { + mVpnType = vpnType; + } + + @Override + public Network getNetwork() { + return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork(); + } + + @Override + public int getActiveVpnType() { + return mVpnType; + } + + private LinkProperties makeLinkProperties() { + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(VPN_IFNAME); + return lp; + } + + private void registerAgent(boolean isAlwaysMetered, Set<UidRange> uids, LinkProperties lp) + throws Exception { + if (mAgentRegistered) throw new IllegalStateException("already registered"); + updateState(NetworkInfo.DetailedState.CONNECTING, "registerAgent"); + mConfig = new VpnConfig(); + mConfig.session = "MySession12345"; + setUids(uids); + if (!isAlwaysMetered) mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED); + mInterface = VPN_IFNAME; + mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(getActiveVpnType(), + mConfig.session)); + mMockNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp, + mNetworkCapabilities); + mMockNetworkAgent.waitForIdle(TIMEOUT_MS); + + verify(mMockNetd, times(1)).networkAddUidRanges(eq(mMockVpn.getNetwork().getNetId()), + eq(toUidRangeStableParcels(uids))); + verify(mMockNetd, never()) + .networkRemoveUidRanges(eq(mMockVpn.getNetwork().getNetId()), any()); + mAgentRegistered = true; + verify(mMockNetd).networkCreate(nativeNetworkConfigVpn(getNetwork().netId, + !mMockNetworkAgent.isBypassableVpn(), mVpnType)); + updateState(NetworkInfo.DetailedState.CONNECTED, "registerAgent"); + mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities()); + mNetworkAgent = mMockNetworkAgent.getNetworkAgent(); + } + + private void registerAgent(Set<UidRange> uids) throws Exception { + registerAgent(false /* isAlwaysMetered */, uids, makeLinkProperties()); + } + + private void connect(boolean validated, boolean hasInternet, boolean isStrictMode) { + mMockNetworkAgent.connect(validated, hasInternet, isStrictMode); + } + + private void connect(boolean validated) { + mMockNetworkAgent.connect(validated); + } + + private TestNetworkAgentWrapper getAgent() { + return mMockNetworkAgent; + } + + public void establish(LinkProperties lp, int uid, Set<UidRange> ranges, boolean validated, + boolean hasInternet, boolean isStrictMode) throws Exception { + mNetworkCapabilities.setOwnerUid(uid); + mNetworkCapabilities.setAdministratorUids(new int[]{uid}); + registerAgent(false, ranges, lp); + connect(validated, hasInternet, isStrictMode); + waitForIdle(); + } + + public void establish(LinkProperties lp, int uid, Set<UidRange> ranges) throws Exception { + establish(lp, uid, ranges, true, true, false); + } + + public void establishForMyUid(LinkProperties lp) throws Exception { + final int uid = Process.myUid(); + establish(lp, uid, uidRangesForUids(uid), true, true, false); + } + + public void establishForMyUid(boolean validated, boolean hasInternet, boolean isStrictMode) + throws Exception { + final int uid = Process.myUid(); + establish(makeLinkProperties(), uid, uidRangesForUids(uid), validated, hasInternet, + isStrictMode); + } + + public void establishForMyUid() throws Exception { + establishForMyUid(makeLinkProperties()); + } + + public void sendLinkProperties(LinkProperties lp) { + mMockNetworkAgent.sendLinkProperties(lp); + } + + public void disconnect() { + if (mMockNetworkAgent != null) { + mMockNetworkAgent.disconnect(); + updateState(NetworkInfo.DetailedState.DISCONNECTED, "disconnect"); + } + mAgentRegistered = false; + setUids(null); + // Remove NET_CAPABILITY_INTERNET or MockNetworkAgent will refuse to connect later on. + mNetworkCapabilities.removeCapability(NET_CAPABILITY_INTERNET); + mInterface = null; + } + + @Override + public void startLegacyVpnRunner() { + mStartLegacyVpnCv.open(); + } + + public void expectStartLegacyVpnRunner() { + assertTrue("startLegacyVpnRunner not called after " + TIMEOUT_MS + " ms", + mStartLegacyVpnCv.block(TIMEOUT_MS)); + + // startLegacyVpn calls stopVpnRunnerPrivileged, which will open mStopVpnRunnerCv, just + // before calling startLegacyVpnRunner. Restore mStopVpnRunnerCv, so the test can expect + // that the VpnRunner is stopped and immediately restarted by calling + // expectStartLegacyVpnRunner() and expectStopVpnRunnerPrivileged() back-to-back. + mStopVpnRunnerCv = new ConditionVariable(); + } + + @Override + public void stopVpnRunnerPrivileged() { + if (mVpnRunner != null) { + super.stopVpnRunnerPrivileged(); + disconnect(); + mStartLegacyVpnCv = new ConditionVariable(); + } + mVpnRunner = null; + mStopVpnRunnerCv.open(); + } + + public void expectStopVpnRunnerPrivileged() { + assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms", + mStopVpnRunnerCv.block(TIMEOUT_MS)); + } + + @Override + public synchronized UnderlyingNetworkInfo getUnderlyingNetworkInfo() { + if (mUnderlyingNetworkInfo != null) return mUnderlyingNetworkInfo; + + return super.getUnderlyingNetworkInfo(); + } + + private synchronized void setUnderlyingNetworkInfo( + UnderlyingNetworkInfo underlyingNetworkInfo) { + mUnderlyingNetworkInfo = underlyingNetworkInfo; + } + } + + private UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) { + return ranges.stream().map( + r -> new UidRangeParcel(r.start, r.stop)).toArray(UidRangeParcel[]::new); + } + + private VpnManagerService makeVpnManagerService() { + final VpnManagerService.Dependencies deps = new VpnManagerService.Dependencies() { + public int getCallingUid() { + return mDeps.getCallingUid(); + } + + public HandlerThread makeHandlerThread() { + return mVMSHandlerThread; + } + + @Override + public VpnProfileStore getVpnProfileStore() { + return mVpnProfileStore; + } + + public INetd getNetd() { + return mMockNetd; + } + + public INetworkManagementService getINetworkManagementService() { + return mNetworkManagementService; + } + }; + return new VpnManagerService(mServiceContext, deps); + } + + private void assertVpnTransportInfo(NetworkCapabilities nc, int type) { + assertNotNull(nc); + final TransportInfo ti = nc.getTransportInfo(); + assertTrue("VPN TransportInfo is not a VpnTransportInfo: " + ti, + ti instanceof VpnTransportInfo); + assertEquals(type, ((VpnTransportInfo) ti).getType()); + + } + + private void processBroadcast(Intent intent) { + mServiceContext.sendBroadcast(intent); + HandlerUtils.waitForIdle(mVMSHandlerThread, TIMEOUT_MS); + waitForIdle(); + } + + private void mockVpn(int uid) { + synchronized (mVpnManagerService.mVpns) { + int userId = UserHandle.getUserId(uid); + mMockVpn = new MockVpn(userId); + // Every running user always has a Vpn in the mVpns array, even if no VPN is running. + mVpnManagerService.mVpns.put(userId, mMockVpn); + } + } + + private void mockUidNetworkingBlocked() { + doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1)) + ).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean()); + } + + private boolean isUidBlocked(int blockedReasons, boolean meteredNetwork) { + final int blockedOnAllNetworksReason = (blockedReasons & ~BLOCKED_METERED_REASON_MASK); + if (blockedOnAllNetworksReason != BLOCKED_REASON_NONE) { + return true; + } + if (meteredNetwork) { + return blockedReasons != BLOCKED_REASON_NONE; + } + return false; + } + + private void setBlockedReasonChanged(int blockedReasons) { + mBlockedReasons = blockedReasons; + mPolicyCallback.onUidBlockedReasonChanged(Process.myUid(), blockedReasons); + } + + private Nat464Xlat getNat464Xlat(NetworkAgentWrapper mna) { + return mService.getNetworkAgentInfoForNetwork(mna.getNetwork()).clatd; + } + + private static class WrappedMultinetworkPolicyTracker extends MultinetworkPolicyTracker { + volatile boolean mConfigRestrictsAvoidBadWifi; + volatile int mConfigMeteredMultipathPreference; + + WrappedMultinetworkPolicyTracker(Context c, Handler h, Runnable r) { + super(c, h, r); + } + + @Override + public boolean configRestrictsAvoidBadWifi() { + return mConfigRestrictsAvoidBadWifi; + } + + @Override + public int configMeteredMultipathPreference() { + return mConfigMeteredMultipathPreference; + } + } + + /** + * Wait up to TIMEOUT_MS for {@code conditionVariable} to open. + * Fails if TIMEOUT_MS goes by before {@code conditionVariable} opens. + */ + static private void waitFor(ConditionVariable conditionVariable) { + if (conditionVariable.block(TIMEOUT_MS)) { + return; + } + fail("ConditionVariable was blocked for more than " + TIMEOUT_MS + "ms"); + } + + private <T> T doAsUid(final int uid, @NonNull final Supplier<T> what) { + when(mDeps.getCallingUid()).thenReturn(uid); + try { + return what.get(); + } finally { + returnRealCallingUid(); + } + } + + private void doAsUid(final int uid, @NonNull final Runnable what) { + doAsUid(uid, () -> { + what.run(); return Void.TYPE; + }); + } + + private void registerNetworkCallbackAsUid(NetworkRequest request, NetworkCallback callback, + int uid) { + doAsUid(uid, () -> { + mCm.registerNetworkCallback(request, callback); + }); + } + + private void registerDefaultNetworkCallbackAsUid(@NonNull final NetworkCallback callback, + final int uid) { + doAsUid(uid, () -> { + mCm.registerDefaultNetworkCallback(callback); + waitForIdle(); + }); + } + + private interface ExceptionalRunnable { + void run() throws Exception; + } + + private void withPermission(String permission, ExceptionalRunnable r) throws Exception { + if (mServiceContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + r.run(); + return; + } + try { + mServiceContext.setPermission(permission, PERMISSION_GRANTED); + r.run(); + } finally { + mServiceContext.setPermission(permission, PERMISSION_DENIED); + } + } + + private static final int PRIMARY_USER = 0; + private static final UidRange PRIMARY_UIDRANGE = + UidRange.createForUser(UserHandle.of(PRIMARY_USER)); + private static final int APP1_UID = UserHandle.getUid(PRIMARY_USER, 10100); + private static final int APP2_UID = UserHandle.getUid(PRIMARY_USER, 10101); + private static final int VPN_UID = UserHandle.getUid(PRIMARY_USER, 10043); + private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER, "", + UserInfo.FLAG_PRIMARY); + private static final UserHandle PRIMARY_USER_HANDLE = new UserHandle(PRIMARY_USER); + + private static final int RESTRICTED_USER = 1; + private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "", + UserInfo.FLAG_RESTRICTED); + static { + RESTRICTED_USER_INFO.restrictedProfileParentId = PRIMARY_USER; + } + + @Before + public void setUp() throws Exception { + mNetIdManager = new TestNetIdManager(); + + mContext = InstrumentationRegistry.getContext(); + + MockitoAnnotations.initMocks(this); + + when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO)); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE)); + when(mUserManager.getUserInfo(PRIMARY_USER)).thenReturn(PRIMARY_USER_INFO); + // canHaveRestrictedProfile does not take a userId. It applies to the userId of the context + // it was started from, i.e., PRIMARY_USER. + when(mUserManager.canHaveRestrictedProfile()).thenReturn(true); + when(mUserManager.getUserInfo(RESTRICTED_USER)).thenReturn(RESTRICTED_USER_INFO); + + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = Build.VERSION_CODES.Q; + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any())) + .thenReturn(applicationInfo); + when(mPackageManager.getTargetSdkVersion(anyString())) + .thenReturn(applicationInfo.targetSdkVersion); + when(mSystemConfigManager.getSystemPermissionUids(anyString())).thenReturn(new int[0]); + + // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not. + // http://b/25897652 . + if (Looper.myLooper() == null) { + Looper.prepare(); + } + mockDefaultPackages(); + mockHasSystemFeature(FEATURE_WIFI, true); + mockHasSystemFeature(FEATURE_WIFI_DIRECT, true); + doReturn(true).when(mTelephonyManager).isDataCapable(); + + FakeSettingsProvider.clearSettingsProvider(); + mServiceContext = new MockContext(InstrumentationRegistry.getContext(), + new FakeSettingsProvider()); + mServiceContext.setUseRegisteredHandlers(true); + + mAlarmManagerThread = new HandlerThread("TestAlarmManager"); + mAlarmManagerThread.start(); + initAlarmManager(mAlarmManager, mAlarmManagerThread.getThreadHandler()); + + mCsHandlerThread = new HandlerThread("TestConnectivityService"); + mVMSHandlerThread = new HandlerThread("TestVpnManagerService"); + mDeps = makeDependencies(); + returnRealCallingUid(); + mService = new ConnectivityService(mServiceContext, + mMockDnsResolver, + mock(IpConnectivityLog.class), + mMockNetd, + mDeps); + mService.mLingerDelayMs = TEST_LINGER_DELAY_MS; + mService.mNascentDelayMs = TEST_NASCENT_DELAY_MS; + verify(mDeps).makeMultinetworkPolicyTracker(any(), any(), any()); + + final ArgumentCaptor<NetworkPolicyCallback> policyCallbackCaptor = + ArgumentCaptor.forClass(NetworkPolicyCallback.class); + verify(mNetworkPolicyManager).registerNetworkPolicyCallback(any(), + policyCallbackCaptor.capture()); + mPolicyCallback = policyCallbackCaptor.getValue(); + + // Create local CM before sending system ready so that we can answer + // getSystemService() correctly. + mCm = new WrappedConnectivityManager(InstrumentationRegistry.getContext(), mService); + mService.systemReadyInternal(); + mVpnManagerService = makeVpnManagerService(); + mVpnManagerService.systemReady(); + mockVpn(Process.myUid()); + mCm.bindProcessToNetwork(null); + mQosCallbackTracker = mock(QosCallbackTracker.class); + + // Ensure that the default setting for Captive Portals is used for most tests + setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT); + setAlwaysOnNetworks(false); + setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com"); + } + + private void returnRealCallingUid() { + doAnswer((invocationOnMock) -> Binder.getCallingUid()).when(mDeps).getCallingUid(); + } + + private ConnectivityService.Dependencies makeDependencies() { + doReturn(false).when(mSystemProperties).getBoolean("ro.radio.noril", false); + final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class); + doReturn(mCsHandlerThread).when(deps).makeHandlerThread(); + doReturn(mNetIdManager).when(deps).makeNetIdManager(); + doReturn(mNetworkStack).when(deps).getNetworkStack(); + doReturn(mSystemProperties).when(deps).getSystemProperties(); + doReturn(mock(ProxyTracker.class)).when(deps).makeProxyTracker(any(), any()); + doReturn(true).when(deps).queryUserAccess(anyInt(), any(), any()); + doAnswer(inv -> { + mPolicyTracker = new WrappedMultinetworkPolicyTracker( + inv.getArgument(0), inv.getArgument(1), inv.getArgument(2)); + return mPolicyTracker; + }).when(deps).makeMultinetworkPolicyTracker(any(), any(), any()); + doReturn(true).when(deps).getCellular464XlatEnabled(); + + doReturn(60000).when(mResources).getInteger(R.integer.config_networkTransitionTimeout); + doReturn("").when(mResources).getString(R.string.config_networkCaptivePortalServerUrl); + doReturn(new String[]{ WIFI_WOL_IFNAME }).when(mResources).getStringArray( + R.array.config_wakeonlan_supported_interfaces); + doReturn(new String[] { "0,1", "1,3" }).when(mResources).getStringArray( + R.array.config_networkSupportedKeepaliveCount); + doReturn(new String[0]).when(mResources).getStringArray( + R.array.config_networkNotifySwitches); + doReturn(new int[]{10, 11, 12, 14, 15}).when(mResources).getIntArray( + R.array.config_protectedNetworks); + // We don't test the actual notification value strings, so just return an empty array. + // It doesn't matter what the values are as long as it's not null. + doReturn(new String[0]).when(mResources).getStringArray(R.array.network_switch_type_name); + + doReturn(R.array.config_networkSupportedKeepaliveCount).when(mResources) + .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any()); + doReturn(R.array.network_switch_type_name).when(mResources) + .getIdentifier(eq("network_switch_type_name"), eq("array"), any()); + + + final ConnectivityResources connRes = mock(ConnectivityResources.class); + doReturn(mResources).when(connRes).get(); + doReturn(connRes).when(deps).getResources(any()); + + final Context mockResContext = mock(Context.class); + doReturn(mResources).when(mockResContext).getResources(); + ConnectivityResources.setResourcesContextForTest(mockResContext); + + return deps; + } + + private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) { + doAnswer(inv -> { + final long when = inv.getArgument(1); + final WakeupMessage wakeupMsg = inv.getArgument(3); + final Handler handler = inv.getArgument(4); + + long delayMs = when - SystemClock.elapsedRealtime(); + if (delayMs < 0) delayMs = 0; + if (delayMs > UNREASONABLY_LONG_ALARM_WAIT_MS) { + fail("Attempting to send msg more than " + UNREASONABLY_LONG_ALARM_WAIT_MS + + "ms into the future: " + delayMs); + } + alarmHandler.postDelayed(() -> handler.post(wakeupMsg::onAlarm), wakeupMsg /* token */, + delayMs); + + return null; + }).when(am).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(), anyString(), + any(WakeupMessage.class), any()); + + doAnswer(inv -> { + final WakeupMessage wakeupMsg = inv.getArgument(0); + alarmHandler.removeCallbacksAndMessages(wakeupMsg /* token */); + return null; + }).when(am).cancel(any(WakeupMessage.class)); + } + + @After + public void tearDown() throws Exception { + unregisterDefaultNetworkCallbacks(); + maybeTearDownEnterpriseNetwork(); + setAlwaysOnNetworks(false); + if (mCellNetworkAgent != null) { + mCellNetworkAgent.disconnect(); + mCellNetworkAgent = null; + } + if (mWiFiNetworkAgent != null) { + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent = null; + } + if (mEthernetNetworkAgent != null) { + mEthernetNetworkAgent.disconnect(); + mEthernetNetworkAgent = null; + } + + if (mQosCallbackMockHelper != null) { + mQosCallbackMockHelper.tearDown(); + mQosCallbackMockHelper = null; + } + mMockVpn.disconnect(); + waitForIdle(); + + FakeSettingsProvider.clearSettingsProvider(); + ConnectivityResources.setResourcesContextForTest(null); + + mCsHandlerThread.quitSafely(); + mAlarmManagerThread.quitSafely(); + } + + private void mockDefaultPackages() throws Exception { + final String myPackageName = mContext.getPackageName(); + final PackageInfo myPackageInfo = mContext.getPackageManager().getPackageInfo( + myPackageName, PackageManager.GET_PERMISSIONS); + when(mPackageManager.getPackagesForUid(Binder.getCallingUid())).thenReturn( + new String[] {myPackageName}); + when(mPackageManager.getPackageInfoAsUser(eq(myPackageName), anyInt(), + eq(UserHandle.getCallingUserId()))).thenReturn(myPackageInfo); + + when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + Arrays.asList(new PackageInfo[] { + buildPackageInfo(/* SYSTEM */ false, APP1_UID), + buildPackageInfo(/* SYSTEM */ false, APP2_UID), + buildPackageInfo(/* SYSTEM */ false, VPN_UID) + })); + + // Create a fake always-on VPN package. + final int userId = UserHandle.getCallingUserId(); + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = Build.VERSION_CODES.R; // Always-on supported in N+. + when(mPackageManager.getApplicationInfoAsUser(eq(ALWAYS_ON_PACKAGE), anyInt(), + eq(userId))).thenReturn(applicationInfo); + + // Minimal mocking to keep Vpn#isAlwaysOnPackageSupported happy. + ResolveInfo rInfo = new ResolveInfo(); + rInfo.serviceInfo = new ServiceInfo(); + rInfo.serviceInfo.metaData = new Bundle(); + final List<ResolveInfo> services = Arrays.asList(new ResolveInfo[]{rInfo}); + when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA), + eq(userId))).thenReturn(services); + when(mPackageManager.getPackageUidAsUser(TEST_PACKAGE_NAME, userId)) + .thenReturn(Process.myUid()); + when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, userId)) + .thenReturn(VPN_UID); + } + + private void verifyActiveNetwork(int transport) { + // Test getActiveNetworkInfo() + assertNotNull(mCm.getActiveNetworkInfo()); + assertEquals(transportToLegacyType(transport), mCm.getActiveNetworkInfo().getType()); + // Test getActiveNetwork() + assertNotNull(mCm.getActiveNetwork()); + assertEquals(mCm.getActiveNetwork(), mCm.getActiveNetworkForUid(Process.myUid())); + if (!NetworkCapabilities.isValidTransport(transport)) { + throw new IllegalStateException("Unknown transport " + transport); + } + switch (transport) { + case TRANSPORT_WIFI: + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + break; + case TRANSPORT_CELLULAR: + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + break; + case TRANSPORT_ETHERNET: + assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + break; + default: + break; + } + // Test getNetworkInfo(Network) + assertNotNull(mCm.getNetworkInfo(mCm.getActiveNetwork())); + assertEquals(transportToLegacyType(transport), + mCm.getNetworkInfo(mCm.getActiveNetwork()).getType()); + assertNotNull(mCm.getActiveNetworkInfoForUid(Process.myUid())); + // Test getNetworkCapabilities(Network) + assertNotNull(mCm.getNetworkCapabilities(mCm.getActiveNetwork())); + assertTrue(mCm.getNetworkCapabilities(mCm.getActiveNetwork()).hasTransport(transport)); + } + + private void verifyNoNetwork() { + waitForIdle(); + // Test getActiveNetworkInfo() + assertNull(mCm.getActiveNetworkInfo()); + // Test getActiveNetwork() + assertNull(mCm.getActiveNetwork()); + assertNull(mCm.getActiveNetworkForUid(Process.myUid())); + // Test getAllNetworks() + assertEmpty(mCm.getAllNetworks()); + assertEmpty(mCm.getAllNetworkStateSnapshots()); + } + + /** + * Class to simplify expecting broadcasts using BroadcastInterceptingContext. + * Ensures that the receiver is unregistered after the expected broadcast is received. This + * cannot be done in the BroadcastReceiver itself because BroadcastInterceptingContext runs + * the receivers' receive method while iterating over the list of receivers, and unregistering + * the receiver during iteration throws ConcurrentModificationException. + */ + private class ExpectedBroadcast extends CompletableFuture<Intent> { + private final BroadcastReceiver mReceiver; + + ExpectedBroadcast(BroadcastReceiver receiver) { + mReceiver = receiver; + } + + public Intent expectBroadcast(int timeoutMs) throws Exception { + try { + return get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + fail("Expected broadcast not received after " + timeoutMs + " ms"); + return null; + } finally { + mServiceContext.unregisterReceiver(mReceiver); + } + } + + public Intent expectBroadcast() throws Exception { + return expectBroadcast(BROADCAST_TIMEOUT_MS); + } + + public void expectNoBroadcast(int timeoutMs) throws Exception { + waitForIdle(); + try { + final Intent intent = get(timeoutMs, TimeUnit.MILLISECONDS); + fail("Unexpected broadcast: " + intent.getAction() + " " + intent.getExtras()); + } catch (TimeoutException expected) { + } finally { + mServiceContext.unregisterReceiver(mReceiver); + } + } + } + + /** Expects that {@code count} CONNECTIVITY_ACTION broadcasts are received. */ + private ExpectedBroadcast registerConnectivityBroadcast(final int count) { + return registerConnectivityBroadcastThat(count, intent -> true); + } + + private ExpectedBroadcast registerConnectivityBroadcastThat(final int count, + @NonNull final Predicate<Intent> filter) { + final IntentFilter intentFilter = new IntentFilter(CONNECTIVITY_ACTION); + // AtomicReference allows receiver to access expected even though it is constructed later. + final AtomicReference<ExpectedBroadcast> expectedRef = new AtomicReference<>(); + final BroadcastReceiver receiver = new BroadcastReceiver() { + private int mRemaining = count; + public void onReceive(Context context, Intent intent) { + final int type = intent.getIntExtra(EXTRA_NETWORK_TYPE, -1); + final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO); + Log.d(TAG, "Received CONNECTIVITY_ACTION type=" + type + " ni=" + ni); + if (!filter.test(intent)) return; + if (--mRemaining == 0) { + expectedRef.get().complete(intent); + } + } + }; + final ExpectedBroadcast expected = new ExpectedBroadcast(receiver); + expectedRef.set(expected); + mServiceContext.registerReceiver(receiver, intentFilter); + return expected; + } + + private boolean extraInfoInBroadcastHasExpectedNullness(NetworkInfo ni) { + final DetailedState state = ni.getDetailedState(); + if (state == DetailedState.CONNECTED && ni.getExtraInfo() == null) return false; + // Expect a null extraInfo if the network is CONNECTING, because a CONNECTIVITY_ACTION + // broadcast with a state of CONNECTING only happens due to legacy VPN lockdown, which also + // nulls out extraInfo. + if (state == DetailedState.CONNECTING && ni.getExtraInfo() != null) return false; + // Can't make any assertions about DISCONNECTED broadcasts. When a network actually + // disconnects, disconnectAndDestroyNetwork sets its state to DISCONNECTED and its extraInfo + // to null. But if the DISCONNECTED broadcast is just simulated by LegacyTypeTracker due to + // a network switch, extraInfo will likely be populated. + // This is likely a bug in CS, but likely not one we can fix without impacting apps. + return true; + } + + private ExpectedBroadcast expectConnectivityAction(int type, NetworkInfo.DetailedState state) { + return registerConnectivityBroadcastThat(1, intent -> { + final int actualType = intent.getIntExtra(EXTRA_NETWORK_TYPE, -1); + final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO); + return type == actualType + && state == ni.getDetailedState() + && extraInfoInBroadcastHasExpectedNullness(ni); + }); + } + + @Test + public void testNetworkTypes() { + // Ensure that our mocks for the networkAttributes config variable work as expected. If they + // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types + // will fail. Failing here is much easier to debug. + assertTrue(mCm.isNetworkSupported(TYPE_WIFI)); + assertTrue(mCm.isNetworkSupported(TYPE_MOBILE)); + assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_MMS)); + assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_FOTA)); + assertFalse(mCm.isNetworkSupported(TYPE_PROXY)); + + // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our + // mocks, this assert exercises the ConnectivityService code path that ensures that + // TYPE_ETHERNET is supported if the ethernet service is running. + assertTrue(mCm.isNetworkSupported(TYPE_ETHERNET)); + } + + @Test + public void testNetworkFeature() throws Exception { + // Connect the cell agent and wait for the connected broadcast. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_SUPL); + ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + + // Build legacy request for SUPL. + final NetworkCapabilities legacyCaps = new NetworkCapabilities(); + legacyCaps.addTransportType(TRANSPORT_CELLULAR); + legacyCaps.addCapability(NET_CAPABILITY_SUPL); + final NetworkRequest legacyRequest = new NetworkRequest(legacyCaps, TYPE_MOBILE_SUPL, + ConnectivityManager.REQUEST_ID_UNSET, NetworkRequest.Type.REQUEST); + + // File request, withdraw it and make sure no broadcast is sent + b = registerConnectivityBroadcast(1); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.requestNetwork(legacyRequest, callback); + callback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent); + mCm.unregisterNetworkCallback(callback); + b.expectNoBroadcast(800); // 800ms long enough to at least flake if this is sent + + // Disconnect the network and expect mobile disconnected broadcast. + b = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + } + + @Test + public void testLingering() throws Exception { + verifyNoNetwork(); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + assertNull(mCm.getActiveNetworkInfo()); + assertNull(mCm.getActiveNetwork()); + // Test bringing up validated cellular. + ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + assertLength(2, mCm.getAllNetworks()); + assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) || + mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork())); + assertTrue(mCm.getAllNetworks()[0].equals(mWiFiNetworkAgent.getNetwork()) || + mCm.getAllNetworks()[1].equals(mWiFiNetworkAgent.getNetwork())); + // Test bringing up validated WiFi. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertLength(2, mCm.getAllNetworks()); + assertTrue(mCm.getAllNetworks()[0].equals(mCm.getActiveNetwork()) || + mCm.getAllNetworks()[1].equals(mCm.getActiveNetwork())); + assertTrue(mCm.getAllNetworks()[0].equals(mCellNetworkAgent.getNetwork()) || + mCm.getAllNetworks()[1].equals(mCellNetworkAgent.getNetwork())); + // Test cellular linger timeout. + mCellNetworkAgent.expectDisconnected(); + waitForIdle(); + assertLength(1, mCm.getAllNetworks()); + verifyActiveNetwork(TRANSPORT_WIFI); + assertLength(1, mCm.getAllNetworks()); + assertEquals(mCm.getAllNetworks()[0], mCm.getActiveNetwork()); + // Test WiFi disconnect. + b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyNoNetwork(); + } + + /** + * Verify a newly created network will be inactive instead of torn down even if no one is + * requesting. + */ + @Test + public void testNewNetworkInactive() throws Exception { + // Create a callback that monitoring the testing network. + final TestNetworkCallback listenCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(new NetworkRequest.Builder().build(), listenCallback); + + // 1. Create a network that is not requested by anyone, and does not satisfy any of the + // default requests. Verify that the network will be inactive instead of torn down. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + listenCallback.assertNoCallback(); + + // Verify that the network will be torn down after nascent expiry. A small period of time + // is added in case of flakiness. + final int nascentTimeoutMs = + mService.mNascentDelayMs + mService.mNascentDelayMs / 4; + listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent, nascentTimeoutMs); + + // 2. Create a network that is satisfied by a request comes later. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final TestNetworkCallback wifiCallback = new TestNetworkCallback(); + mCm.requestNetwork(wifiRequest, wifiCallback); + wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Verify that the network will be kept since the request is still satisfied. And is able + // to get disconnected as usual if the request is released after the nascent timer expires. + listenCallback.assertNoCallback(nascentTimeoutMs); + mCm.unregisterNetworkCallback(wifiCallback); + listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // 3. Create a network that is satisfied by a request comes later. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + listenCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + mCm.requestNetwork(wifiRequest, wifiCallback); + wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Verify that the network will still be torn down after the request gets removed. + mCm.unregisterNetworkCallback(wifiCallback); + listenCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // There is no need to ensure that LOSING is never sent in the common case that the + // network immediately satisfies a request that was already present, because it is already + // verified anywhere whenever {@code TestNetworkCallback#expectAvailable*} is called. + + mCm.unregisterNetworkCallback(listenCallback); + } + + /** + * Verify a newly created network will be inactive and switch to background if only background + * request is satisfied. + */ + @Test + public void testNewNetworkInactive_bgNetwork() throws Exception { + // Create a callback that monitoring the wifi network. + final TestNetworkCallback wifiListenCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(), wifiListenCallback); + + // Create callbacks that can monitor background and foreground mobile networks. + // This is done by granting using background networks permission before registration. Thus, + // the service will not add {@code NET_CAPABILITY_FOREGROUND} by default. + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + final TestNetworkCallback bgMobileListenCallback = new TestNetworkCallback(); + final TestNetworkCallback fgMobileListenCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(), bgMobileListenCallback); + mCm.registerNetworkCallback(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_FOREGROUND).build(), fgMobileListenCallback); + + // Connect wifi, which satisfies default request. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + wifiListenCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + + // Connect a cellular network, verify that satisfies only the background callback. + setAlwaysOnNetworks(true); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + bgMobileListenCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + fgMobileListenCallback.assertNoCallback(); + assertFalse(isForegroundNetwork(mCellNetworkAgent)); + + mCellNetworkAgent.disconnect(); + bgMobileListenCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + fgMobileListenCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(wifiListenCallback); + mCm.unregisterNetworkCallback(bgMobileListenCallback); + mCm.unregisterNetworkCallback(fgMobileListenCallback); + } + + @Test + public void testValidatedCellularOutscoresUnvalidatedWiFi() throws Exception { + // Test bringing up unvalidated WiFi + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up unvalidated cellular + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test cellular disconnect. + mCellNetworkAgent.disconnect(); + waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up validated cellular + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test cellular disconnect. + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test WiFi disconnect. + b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyNoNetwork(); + } + + @Test + public void testUnvalidatedWifiOutscoresUnvalidatedCellular() throws Exception { + // Test bringing up unvalidated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test bringing up unvalidated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test WiFi disconnect. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test cellular disconnect. + b = registerConnectivityBroadcast(1); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyNoNetwork(); + } + + @Test + public void testUnlingeringDoesNotValidate() throws Exception { + // Test bringing up unvalidated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + // Test cellular disconnect. + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.disconnect(); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Unlingering a network should not cause it to be marked as validated. + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + } + + @Test + public void testCellularOutscoresWeakWifi() throws Exception { + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test WiFi getting really weak. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.adjustScore(-11); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test WiFi restoring signal strength. + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.adjustScore(11); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testReapingNetwork() throws Exception { + // Test bringing up WiFi without NET_CAPABILITY_INTERNET. + // Expect it to be torn down immediately because it satisfies no requests. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithoutInternet(); + mWiFiNetworkAgent.expectDisconnected(); + // Test bringing up cellular without NET_CAPABILITY_INTERNET. + // Expect it to be torn down immediately because it satisfies no requests. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mCellNetworkAgent.connectWithoutInternet(); + mCellNetworkAgent.expectDisconnected(); + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up unvalidated cellular. + // Expect it to be torn down because it could never be the highest scoring network + // satisfying the default request even if it validated. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + mCellNetworkAgent.expectDisconnected(); + verifyActiveNetwork(TRANSPORT_WIFI); + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + } + + @Test + public void testCellularFallback() throws Exception { + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Reevaluate WiFi (it'll instantly fail DNS). + b = registerConnectivityBroadcast(2); + assertTrue(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + mCm.reportBadNetwork(mWiFiNetworkAgent.getNetwork()); + // Should quickly fall back to Cellular. + b.expectBroadcast(); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Reevaluate cellular (it'll instantly fail DNS). + b = registerConnectivityBroadcast(2); + assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + mCm.reportBadNetwork(mCellNetworkAgent.getNetwork()); + // Should quickly fall back to WiFi. + b.expectBroadcast(); + assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testWiFiFallback() throws Exception { + // Test bringing up unvalidated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mWiFiNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + // Test bringing up validated cellular. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + b = registerConnectivityBroadcast(2); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + // Reevaluate cellular (it'll instantly fail DNS). + b = registerConnectivityBroadcast(2); + assertTrue(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + mCm.reportBadNetwork(mCellNetworkAgent.getNetwork()); + // Should quickly fall back to WiFi. + b.expectBroadcast(); + assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability( + NET_CAPABILITY_VALIDATED)); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testRequiresValidation() { + assertTrue(NetworkMonitorUtils.isValidationRequired( + mCm.getDefaultRequest().networkCapabilities)); + } + + /** + * Utility NetworkCallback for testing. The caller must explicitly test for all the callbacks + * this class receives, by calling expectCallback() exactly once each time a callback is + * received. assertNoCallback may be called at any time. + */ + private class TestNetworkCallback extends TestableNetworkCallback { + TestNetworkCallback() { + super(TEST_CALLBACK_TIMEOUT_MS); + } + + @Override + public void assertNoCallback() { + // TODO: better support this use case in TestableNetworkCallback + waitForIdle(); + assertNoCallback(0 /* timeout */); + } + + @Override + public <T extends CallbackEntry> T expectCallback(final KClass<T> type, final HasNetwork n, + final long timeoutMs) { + final T callback = super.expectCallback(type, n, timeoutMs); + if (callback instanceof CallbackEntry.Losing) { + // TODO : move this to the specific test(s) needing this rather than here. + final CallbackEntry.Losing losing = (CallbackEntry.Losing) callback; + final int maxMsToLive = losing.getMaxMsToLive(); + String msg = String.format( + "Invalid linger time value %d, must be between %d and %d", + maxMsToLive, 0, mService.mLingerDelayMs); + assertTrue(msg, 0 <= maxMsToLive && maxMsToLive <= mService.mLingerDelayMs); + } + return callback; + } + } + + // Can't be part of TestNetworkCallback because "cannot be declared static; static methods can + // only be declared in a static or top level type". + static void assertNoCallbacks(TestNetworkCallback ... callbacks) { + for (TestNetworkCallback c : callbacks) { + c.assertNoCallback(); + } + } + + static void expectOnLost(TestNetworkAgentWrapper network, TestNetworkCallback ... callbacks) { + for (TestNetworkCallback c : callbacks) { + c.expectCallback(CallbackEntry.LOST, network); + } + } + + static void expectAvailableCallbacksUnvalidatedWithSpecifier(TestNetworkAgentWrapper network, + NetworkSpecifier specifier, TestNetworkCallback ... callbacks) { + for (TestNetworkCallback c : callbacks) { + c.expectCallback(CallbackEntry.AVAILABLE, network); + c.expectCapabilitiesThat(network, (nc) -> + !nc.hasCapability(NET_CAPABILITY_VALIDATED) + && Objects.equals(specifier, nc.getNetworkSpecifier())); + c.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, network); + c.expectCallback(CallbackEntry.BLOCKED_STATUS, network); + } + } + + @Test + public void testStateChangeNetworkCallbacks() throws Exception { + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + + // Test unvalidated networks + ExpectedBroadcast b = registerConnectivityBroadcast(1); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + cellNetworkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + // This should not trigger spurious onAvailable() callbacks, b/21762680. + mCellNetworkAgent.adjustScore(-1); + waitForIdle(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + b = registerConnectivityBroadcast(2); + mWiFiNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + cellNetworkCallback.assertNoCallback(); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + b = registerConnectivityBroadcast(1); + mCellNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + b.expectBroadcast(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + // Test validated networks + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + genericNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + // This should not trigger spurious onAvailable() callbacks, b/21762680. + mCellNetworkAgent.adjustScore(-1); + waitForIdle(); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + genericNetworkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + genericNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + wifiNetworkCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + mWiFiNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + + mCellNetworkAgent.disconnect(); + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback); + } + + private void doNetworkCallbacksSanitizationTest(boolean sanitized) throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(wifiRequest, callback); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + final LinkProperties newLp = new LinkProperties(); + final Uri capportUrl = Uri.parse("https://capport.example.com/api"); + final CaptivePortalData capportData = new CaptivePortalData.Builder() + .setCaptive(true).build(); + + final Uri expectedCapportUrl = sanitized ? null : capportUrl; + newLp.setCaptivePortalApiUrl(capportUrl); + mWiFiNetworkAgent.sendLinkProperties(newLp); + callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl())); + defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportUrl, lp.getCaptivePortalApiUrl())); + + final CaptivePortalData expectedCapportData = sanitized ? null : capportData; + mWiFiNetworkAgent.notifyCapportApiDataChanged(capportData); + callback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportData, lp.getCaptivePortalData())); + defaultCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, lp -> + Objects.equals(expectedCapportData, lp.getCaptivePortalData())); + + final LinkProperties lp = mCm.getLinkProperties(mWiFiNetworkAgent.getNetwork()); + assertEquals(expectedCapportUrl, lp.getCaptivePortalApiUrl()); + assertEquals(expectedCapportData, lp.getCaptivePortalData()); + } + + @Test + public void networkCallbacksSanitizationTest_Sanitize() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + doNetworkCallbacksSanitizationTest(true /* sanitized */); + } + + @Test + public void networkCallbacksSanitizationTest_NoSanitize_NetworkStack() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_GRANTED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + doNetworkCallbacksSanitizationTest(false /* sanitized */); + } + + @Test + public void networkCallbacksSanitizationTest_NoSanitize_Settings() throws Exception { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + doNetworkCallbacksSanitizationTest(false /* sanitized */); + } + + @Test + public void testOwnerUidCannotChange() throws Exception { + final NetworkCapabilities ncTemplate = new NetworkCapabilities(); + final int originalOwnerUid = Process.myUid(); + ncTemplate.setOwnerUid(originalOwnerUid); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), + ncTemplate); + mWiFiNetworkAgent.connect(false); + waitForIdle(); + + // Send ConnectivityService an update to the mWiFiNetworkAgent's capabilities that changes + // the owner UID and an unrelated capability. + NetworkCapabilities agentCapabilities = mWiFiNetworkAgent.getNetworkCapabilities(); + assertEquals(originalOwnerUid, agentCapabilities.getOwnerUid()); + agentCapabilities.setOwnerUid(42); + assertFalse(agentCapabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)); + agentCapabilities.addCapability(NET_CAPABILITY_NOT_CONGESTED); + mWiFiNetworkAgent.setNetworkCapabilities(agentCapabilities, true); + waitForIdle(); + + // Owner UIDs are not visible without location permission. + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + // Check that the capability change has been applied but the owner UID is not modified. + NetworkCapabilities nc = mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()); + assertEquals(originalOwnerUid, nc.getOwnerUid()); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_CONGESTED)); + } + + @Test + public void testMultipleLingering() throws Exception { + // This test would be flaky with the default 120ms timer: that is short enough that + // lingered networks are torn down before assertions can be run. We don't want to mock the + // lingering timer to keep the WakeupMessage logic realistic: this has already proven useful + // in detecting races. + mService.mLingerDelayMs = 300; + + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_NOT_METERED) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent.connect(true); + // We get AVAILABLE on wifi when wifi connects and satisfies our unmetered request. + // We then get LOSING when wifi validates and cell is outscored. + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mEthernetNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mEthernetNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + defaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + for (int i = 0; i < 4; i++) { + TestNetworkAgentWrapper oldNetwork, newNetwork; + if (i % 2 == 0) { + mWiFiNetworkAgent.adjustScore(-15); + oldNetwork = mWiFiNetworkAgent; + newNetwork = mCellNetworkAgent; + } else { + mWiFiNetworkAgent.adjustScore(15); + oldNetwork = mCellNetworkAgent; + newNetwork = mWiFiNetworkAgent; + + } + callback.expectCallback(CallbackEntry.LOSING, oldNetwork); + // TODO: should we send an AVAILABLE callback to newNetwork, to indicate that it is no + // longer lingering? + defaultCallback.expectAvailableCallbacksValidated(newNetwork); + assertEquals(newNetwork.getNetwork(), mCm.getActiveNetwork()); + } + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Verify that if a network no longer satisfies a request, we send LOST and not LOSING, even + // if the network is still up. + mWiFiNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + // We expect a notification about the capabilities change, and nothing else. + defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, mWiFiNetworkAgent); + defaultCallback.assertNoCallback(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Wifi no longer satisfies our listen, which is for an unmetered network. + // But because its score is 55, it's still up (and the default network). + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Disconnect our test networks. + mWiFiNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + mCellNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + + mCm.unregisterNetworkCallback(callback); + waitForIdle(); + + // Check that a network is only lingered or torn down if it would not satisfy a request even + // if it validated. + request = new NetworkRequest.Builder().clearCapabilities().build(); + callback = new TestNetworkCallback(); + + mCm.registerNetworkCallback(request, callback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); // Score: 10 + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi with a score of 20. + // Cell stays up because it would satisfy the default request if it validated. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); // Score: 20 + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but + // it's arguably correct to linger it, since it was the default network before it validated. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + mCellNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + + // If a network is lingering, and we add and remove a request from it, resume lingering. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // TODO: Investigate sending validated before losing. + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + NetworkCallback noopCallback = new NetworkCallback(); + mCm.requestNetwork(cellRequest, noopCallback); + // TODO: should this cause an AVAILABLE callback, to indicate that the network is no longer + // lingering? + mCm.unregisterNetworkCallback(noopCallback); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + + // Similar to the above: lingering can start even after the lingered request is removed. + // Disconnect wifi and switch to cell. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Cell is now the default network. Pin it with a cell-specific request. + noopCallback = new NetworkCallback(); // Can't reuse NetworkCallbacks. http://b/20701525 + mCm.requestNetwork(cellRequest, noopCallback); + + // Now connect wifi, and expect it to become the default network. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + // The default request is lingering on cell, but nothing happens to cell, and we send no + // callbacks for it, because it's kept up by cellRequest. + callback.assertNoCallback(); + // Now unregister cellRequest and expect cell to start lingering. + mCm.unregisterNetworkCallback(noopCallback); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + + // Let linger run its course. + callback.assertNoCallback(); + final int lingerTimeoutMs = mService.mLingerDelayMs + mService.mLingerDelayMs / 4; + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, lingerTimeoutMs); + + // Register a TRACK_DEFAULT request and check that it does not affect lingering. + TestNetworkCallback trackDefaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(trackDefaultCallback); + trackDefaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent); + trackDefaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Let linger run its course. + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent, lingerTimeoutMs); + + // Clean up. + mEthernetNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + trackDefaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(trackDefaultCallback); + } + + private void grantUsingBackgroundNetworksPermissionForUid(final int uid) throws Exception { + grantUsingBackgroundNetworksPermissionForUid(uid, mContext.getPackageName()); + } + + private void grantUsingBackgroundNetworksPermissionForUid( + final int uid, final String packageName) throws Exception { + when(mPackageManager.getPackageInfo( + eq(packageName), eq(GET_PERMISSIONS | MATCH_ANY_USER))) + .thenReturn(buildPackageInfo(true /* hasSystemPermission */, uid)); + mService.mPermissionMonitor.onPackageAdded(packageName, uid); + } + + @Test + public void testNetworkGoesIntoBackgroundAfterLinger() throws Exception { + setAlwaysOnNetworks(true); + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities() + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + // Wifi comes up and cell lingers. + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + + // File a request for cellular, then release it. + NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + NetworkCallback noopCallback = new NetworkCallback(); + mCm.requestNetwork(cellRequest, noopCallback); + mCm.unregisterNetworkCallback(noopCallback); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + + // Let linger run its course. + callback.assertNoCallback(); + final int lingerTimeoutMs = TEST_LINGER_DELAY_MS + TEST_LINGER_DELAY_MS / 4; + callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent, + lingerTimeoutMs); + + // Clean up. + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(callback); + } + + private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) { + return new NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL, permission, + /*secure=*/ false, VpnManager.TYPE_VPN_NONE); + } + + private NativeNetworkConfig nativeNetworkConfigVpn(int netId, boolean secure, int vpnType) { + return new NativeNetworkConfig(netId, NativeNetworkType.VIRTUAL, INetd.PERMISSION_NONE, + secure, vpnType); + } + + @Test + public void testNetworkAgentCallbacks() throws Exception { + // Keeps track of the order of events that happen in this test. + final LinkedBlockingQueue<String> eventOrder = new LinkedBlockingQueue<>(); + + final NetworkRequest request = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + final AtomicReference<Network> wifiNetwork = new AtomicReference<>(); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + // Expectations for state when various callbacks fire. These expectations run on the handler + // thread and not on the test thread because they need to prevent the handler thread from + // advancing while they examine state. + + // 1. When onCreated fires, netd has been told to create the network. + mWiFiNetworkAgent.setCreatedCallback(() -> { + eventOrder.offer("onNetworkCreated"); + wifiNetwork.set(mWiFiNetworkAgent.getNetwork()); + assertNotNull(wifiNetwork.get()); + try { + verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + wifiNetwork.get().getNetId(), INetd.PERMISSION_NONE)); + } catch (RemoteException impossible) { + fail(); + } + }); + + // 2. onNetworkUnwanted isn't precisely ordered with respect to any particular events. Just + // check that it is fired at some point after disconnect. + mWiFiNetworkAgent.setUnwantedCallback(() -> eventOrder.offer("onNetworkUnwanted")); + + // 3. While the teardown timer is running, connectivity APIs report the network is gone, but + // netd has not yet been told to destroy it. + final Runnable duringTeardown = () -> { + eventOrder.offer("timePasses"); + assertNull(mCm.getLinkProperties(wifiNetwork.get())); + try { + verify(mMockNetd, never()).networkDestroy(wifiNetwork.get().getNetId()); + } catch (RemoteException impossible) { + fail(); + } + }; + + // 4. After onNetworkDisconnected is called, connectivity APIs report the network is gone, + // and netd has been told to destroy it. + mWiFiNetworkAgent.setDisconnectedCallback(() -> { + eventOrder.offer("onNetworkDisconnected"); + assertNull(mCm.getLinkProperties(wifiNetwork.get())); + try { + verify(mMockNetd).networkDestroy(wifiNetwork.get().getNetId()); + } catch (RemoteException impossible) { + fail(); + } + }); + + // Connect a network, and file a request for it after it has come up, to ensure the nascent + // timer is cleared and the test does not have to wait for it. Filing the request after the + // network has come up is necessary because ConnectivityService does not appear to clear the + // nascent timer if the first request satisfied by the network was filed before the network + // connected. + // TODO: fix this bug, file the request before connecting, and remove the waitForIdle. + mWiFiNetworkAgent.connectWithoutInternet(); + waitForIdle(); + mCm.requestNetwork(request, callback); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Set teardown delay and make sure CS has processed it. + mWiFiNetworkAgent.getNetworkAgent().setTeardownDelayMillis(300); + waitForIdle(); + + // Post the duringTeardown lambda to the handler so it fires while teardown is in progress. + // The delay must be long enough it will run after the unregisterNetworkCallback has torn + // down the network and started the teardown timer, and short enough that the lambda is + // scheduled to run before the teardown timer. + final Handler h = new Handler(mCsHandlerThread.getLooper()); + h.postDelayed(duringTeardown, 150); + + // Disconnect the network and check that events happened in the right order. + mCm.unregisterNetworkCallback(callback); + assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("onNetworkUnwanted", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("timePasses", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertEquals("onNetworkDisconnected", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + mCm.unregisterNetworkCallback(callback); + } + + @Test + public void testExplicitlySelected() throws Exception { + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up validated cell. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + // Bring up unvalidated wifi with explicitlySelected=true. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, false); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Cell Remains the default. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Lower wifi's score to below than cell, and check that it doesn't disconnect because + // it's explicitly selected. + mWiFiNetworkAgent.adjustScore(-40); + mWiFiNetworkAgent.adjustScore(40); + callback.assertNoCallback(); + + // If the user chooses yes on the "No Internet access, stay connected?" dialog, we switch to + // wifi even though it's unvalidated. + mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), true, false); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Disconnect wifi, and then reconnect, again with explicitlySelected=true. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, false); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // If the user chooses no on the "No Internet access, stay connected?" dialog, we ask the + // network to disconnect. + mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), false, false); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Reconnect, again with explicitlySelected=true, but this time validate. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, false); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mEthernetNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mEthernetNetworkAgent); + assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + callback.assertNoCallback(); + + // Disconnect wifi, and then reconnect as if the user had selected "yes, don't ask again" + // (i.e., with explicitlySelected=true and acceptUnvalidated=true). Expect to switch to + // wifi immediately. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true, true); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mEthernetNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + mEthernetNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + + // Disconnect and reconnect with explicitlySelected=false and acceptUnvalidated=true. + // Check that the network is not scored specially and that the device prefers cell data. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(false, true); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Clean up. + mWiFiNetworkAgent.disconnect(); + mCellNetworkAgent.disconnect(); + + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + } + + private void tryNetworkFactoryRequests(int capability) throws Exception { + // Verify NOT_RESTRICTED is set appropriately + final NetworkCapabilities nc = new NetworkRequest.Builder().addCapability(capability) + .build().networkCapabilities; + if (capability == NET_CAPABILITY_CBS || capability == NET_CAPABILITY_DUN + || capability == NET_CAPABILITY_EIMS || capability == NET_CAPABILITY_FOTA + || capability == NET_CAPABILITY_IA || capability == NET_CAPABILITY_IMS + || capability == NET_CAPABILITY_RCS || capability == NET_CAPABILITY_XCAP + || capability == NET_CAPABILITY_VSIM || capability == NET_CAPABILITY_BIP + || capability == NET_CAPABILITY_ENTERPRISE) { + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } else { + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + } + + NetworkCapabilities filter = new NetworkCapabilities(); + filter.addTransportType(TRANSPORT_CELLULAR); + filter.addCapability(capability); + // Add NOT_VCN_MANAGED capability into filter unconditionally since some requests will add + // NOT_VCN_MANAGED automatically but not for NetworkCapabilities, + // see {@code NetworkCapabilities#deduceNotVcnManagedCapability} for more details. + filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests"); + handlerThread.start(); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(45); + testFactory.register(); + + final NetworkCallback networkCallback; + if (capability != NET_CAPABILITY_INTERNET) { + // If the capability passed in argument is part of the default request, then the + // factory will see the default request. Otherwise the filter will prevent the + // factory from seeing it. In that case, add a request so it can be tested. + assertFalse(testFactory.getMyStartRequested()); + NetworkRequest request = new NetworkRequest.Builder().addCapability(capability).build(); + networkCallback = new NetworkCallback(); + mCm.requestNetwork(request, networkCallback); + } else { + networkCallback = null; + } + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + assertTrue(testFactory.getMyStartRequested()); + + // Now bring in a higher scored network. + TestNetworkAgentWrapper testAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + // When testAgent connects, because of its score (50 legacy int / cell transport) + // it will beat or equal the testFactory's offer, so the request will be removed. + // Note the agent as validated only if the capability is INTERNET, as it's the only case + // where it makes sense. + testAgent.connect(NET_CAPABILITY_INTERNET == capability /* validated */); + testAgent.addCapability(capability); + testFactory.expectRequestRemove(); + testFactory.assertRequestCountEquals(0); + assertFalse(testFactory.getMyStartRequested()); + + // Add a request and make sure it's not sent to the factory, because the agent + // is satisfying it better. + final NetworkCallback cb = new ConnectivityManager.NetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder().addCapability(capability).build(), cb); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(0); + assertFalse(testFactory.getMyStartRequested()); + + // If using legacy scores, make the test agent weak enough to have the exact same score as + // the factory (50 for cell - 5 adjustment). Make sure the factory doesn't see the request. + // If not using legacy score, this is a no-op and the "same score removes request" behavior + // has already been tested above. + testAgent.adjustScore(-5); + expectNoRequestChanged(testFactory); + assertFalse(testFactory.getMyStartRequested()); + + // Make the test agent weak enough that the factory will see the two requests (the one that + // was just sent, and either the default one or the one sent at the top of this test if + // the default won't be seen). + testAgent.setScore(new NetworkScore.Builder().setLegacyInt(2).setExiting(true).build()); + testFactory.expectRequestAdds(2); + testFactory.assertRequestCountEquals(2); + assertTrue(testFactory.getMyStartRequested()); + + // Now unregister and make sure the request is removed. + mCm.unregisterNetworkCallback(cb); + testFactory.expectRequestRemove(); + + // Bring in a bunch of requests. + assertEquals(1, testFactory.getMyRequestCount()); + ConnectivityManager.NetworkCallback[] networkCallbacks = + new ConnectivityManager.NetworkCallback[10]; + for (int i = 0; i< networkCallbacks.length; i++) { + networkCallbacks[i] = new ConnectivityManager.NetworkCallback(); + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(capability); + mCm.requestNetwork(builder.build(), networkCallbacks[i]); + } + testFactory.expectRequestAdds(10); + testFactory.assertRequestCountEquals(11); // +1 for the default/test specific request + assertTrue(testFactory.getMyStartRequested()); + + // Remove the requests. + for (int i = 0; i < networkCallbacks.length; i++) { + mCm.unregisterNetworkCallback(networkCallbacks[i]); + } + testFactory.expectRequestRemoves(10); + testFactory.assertRequestCountEquals(1); + assertTrue(testFactory.getMyStartRequested()); + + // Adjust the agent score up again. Expect the request to be withdrawn. + testAgent.setScore(new NetworkScore.Builder().setLegacyInt(50).build()); + testFactory.expectRequestRemove(); + testFactory.assertRequestCountEquals(0); + assertFalse(testFactory.getMyStartRequested()); + + // Drop the higher scored network. + testAgent.disconnect(); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + assertEquals(1, testFactory.getMyRequestCount()); + assertTrue(testFactory.getMyStartRequested()); + + testFactory.terminate(); + if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback); + handlerThread.quit(); + } + + @Test + public void testNetworkFactoryRequests() throws Exception { + tryNetworkFactoryRequests(NET_CAPABILITY_MMS); + tryNetworkFactoryRequests(NET_CAPABILITY_SUPL); + tryNetworkFactoryRequests(NET_CAPABILITY_DUN); + tryNetworkFactoryRequests(NET_CAPABILITY_FOTA); + tryNetworkFactoryRequests(NET_CAPABILITY_IMS); + tryNetworkFactoryRequests(NET_CAPABILITY_CBS); + tryNetworkFactoryRequests(NET_CAPABILITY_WIFI_P2P); + tryNetworkFactoryRequests(NET_CAPABILITY_IA); + tryNetworkFactoryRequests(NET_CAPABILITY_RCS); + tryNetworkFactoryRequests(NET_CAPABILITY_XCAP); + tryNetworkFactoryRequests(NET_CAPABILITY_ENTERPRISE); + tryNetworkFactoryRequests(NET_CAPABILITY_EIMS); + tryNetworkFactoryRequests(NET_CAPABILITY_NOT_METERED); + tryNetworkFactoryRequests(NET_CAPABILITY_INTERNET); + tryNetworkFactoryRequests(NET_CAPABILITY_TRUSTED); + tryNetworkFactoryRequests(NET_CAPABILITY_NOT_VPN); + tryNetworkFactoryRequests(NET_CAPABILITY_VSIM); + tryNetworkFactoryRequests(NET_CAPABILITY_BIP); + // Skipping VALIDATED and CAPTIVE_PORTAL as they're disallowed. + } + + @Test + public void testRegisterIgnoringScore() throws Exception { + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(90).build()); + mWiFiNetworkAgent.connect(true /* validated */); + + // Make sure the factory sees the default network + final NetworkCapabilities filter = new NetworkCapabilities(); + filter.addTransportType(TRANSPORT_CELLULAR); + filter.addCapability(NET_CAPABILITY_INTERNET); + filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests"); + handlerThread.start(); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.register(); + + final MockNetworkFactory testFactoryAll = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactoryAll", filter, mCsHandlerThread); + testFactoryAll.registerIgnoringScore(); + + // The regular test factory should not see the request, because WiFi is stronger than cell. + expectNoRequestChanged(testFactory); + // With ignoringScore though the request is seen. + testFactoryAll.expectRequestAdd(); + + // The legacy int will be ignored anyway, set the only other knob to true + mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(110) + .setTransportPrimary(true).build()); + + expectNoRequestChanged(testFactory); // still not seeing the request + expectNoRequestChanged(testFactoryAll); // still seeing the request + + mWiFiNetworkAgent.disconnect(); + } + + @Test + public void testNetworkFactoryUnregister() throws Exception { + // Make sure the factory sees the default network + final NetworkCapabilities filter = new NetworkCapabilities(); + filter.addCapability(NET_CAPABILITY_INTERNET); + filter.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + + final HandlerThread handlerThread = new HandlerThread("testNetworkFactoryRequests"); + handlerThread.start(); + + // Checks that calling setScoreFilter on a NetworkFactory immediately before closing it + // does not crash. + for (int i = 0; i < 100; i++) { + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + // Register the factory and don't be surprised when the default request arrives. + testFactory.register(); + testFactory.expectRequestAdd(); + + testFactory.setScoreFilter(42); + testFactory.terminate(); + + if (i % 2 == 0) { + try { + testFactory.register(); + fail("Re-registering terminated NetworkFactory should throw"); + } catch (IllegalStateException expected) { + } + } + } + handlerThread.quit(); + } + + @Test + public void testNoMutableNetworkRequests() throws Exception { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + NetworkRequest request1 = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED) + .build(); + NetworkRequest request2 = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL) + .build(); + + Class<IllegalArgumentException> expected = IllegalArgumentException.class; + assertThrows(expected, () -> mCm.requestNetwork(request1, new NetworkCallback())); + assertThrows(expected, () -> mCm.requestNetwork(request1, pendingIntent)); + assertThrows(expected, () -> mCm.requestNetwork(request2, new NetworkCallback())); + assertThrows(expected, () -> mCm.requestNetwork(request2, pendingIntent)); + } + + @Test + public void testMMSonWiFi() throws Exception { + // Test bringing up cellular without MMS NetworkRequest gets reaped + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS); + mCellNetworkAgent.connectWithoutInternet(); + mCellNetworkAgent.expectDisconnected(); + waitForIdle(); + assertEmpty(mCm.getAllNetworks()); + verifyNoNetwork(); + + // Test bringing up validated WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + + // Register MMS NetworkRequest + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(builder.build(), networkCallback); + + // Test bringing up unvalidated cellular with MMS + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS); + mCellNetworkAgent.connectWithoutInternet(); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + verifyActiveNetwork(TRANSPORT_WIFI); + + // Test releasing NetworkRequest disconnects cellular with MMS + mCm.unregisterNetworkCallback(networkCallback); + mCellNetworkAgent.expectDisconnected(); + verifyActiveNetwork(TRANSPORT_WIFI); + } + + @Test + public void testMMSonCell() throws Exception { + // Test bringing up cellular without MMS + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + mCellNetworkAgent.connect(false); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + + // Register MMS NetworkRequest + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_MMS); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(builder.build(), networkCallback); + + // Test bringing up MMS cellular network + TestNetworkAgentWrapper + mmsNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS); + mmsNetworkAgent.connectWithoutInternet(); + networkCallback.expectAvailableCallbacksUnvalidated(mmsNetworkAgent); + verifyActiveNetwork(TRANSPORT_CELLULAR); + + // Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent + mCm.unregisterNetworkCallback(networkCallback); + mmsNetworkAgent.expectDisconnected(); + verifyActiveNetwork(TRANSPORT_CELLULAR); + } + + @Test + public void testPartialConnectivity() throws Exception { + // Register network callback. + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up validated mobile data. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + // Bring up wifi with partial connectivity. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithPartialConnectivity(); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); + + // Mobile data should be the default network. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + callback.assertNoCallback(); + + // With HTTPS probe disabled, NetworkMonitor should pass the network validation with http + // probe. + mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */); + // If the user chooses yes to use this partial connectivity wifi, switch the default + // network to wifi and check if wifi becomes valid or not. + mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), true /* accept */, + false /* always */); + // If user accepts partial connectivity network, + // NetworkMonitor#setAcceptPartialConnectivity() should be called too. + waitForIdle(); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + + // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is + // validated. + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + NetworkCapabilities nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, + mWiFiNetworkAgent); + assertTrue(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // Disconnect and reconnect wifi with partial connectivity again. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connectWithPartialConnectivity(); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); + + // Mobile data should be the default network. + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + + // If the user chooses no, disconnect wifi immediately. + mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), false/* accept */, + false /* always */); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // If user accepted partial connectivity before, and device reconnects to that network + // again, but now the network has full connectivity. The network shouldn't contain + // NET_CAPABILITY_PARTIAL_CONNECTIVITY. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + // acceptUnvalidated is also used as setting for accepting partial networks. + mWiFiNetworkAgent.explicitlySelected(true /* explicitlySelected */, + true /* acceptUnvalidated */); + mWiFiNetworkAgent.connect(true); + + // If user accepted partial connectivity network before, + // NetworkMonitor#setAcceptPartialConnectivity() will be called in + // ConnectivityService#updateNetworkInfo(). + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + nc = callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertFalse(nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)); + + // Wifi should be the default network. + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // The user accepted partial connectivity and selected "don't ask again". Now the user + // reconnects to the partial connectivity network. Switch to wifi as soon as partial + // connectivity is detected. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(true /* explicitlySelected */, + true /* acceptUnvalidated */); + mWiFiNetworkAgent.connectWithPartialConnectivity(); + // If user accepted partial connectivity network before, + // NetworkMonitor#setAcceptPartialConnectivity() will be called in + // ConnectivityService#updateNetworkInfo(). + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + callback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, mWiFiNetworkAgent); + mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */); + + // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is + // validated. + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // If the user accepted partial connectivity, and the device auto-reconnects to the partial + // connectivity network, it should contain both PARTIAL_CONNECTIVITY and VALIDATED. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.explicitlySelected(false /* explicitlySelected */, + true /* acceptUnvalidated */); + + // NetworkMonitor will immediately (once the HTTPS probe fails...) report the network as + // valid, because ConnectivityService calls setAcceptPartialConnectivity before it calls + // notifyNetworkConnected. + mWiFiNetworkAgent.connectWithPartialValidConnectivity(false /* isStrictMode */); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mWiFiNetworkAgent.mNetworkMonitor, times(1)).setAcceptPartialConnectivity(); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith( + NET_CAPABILITY_PARTIAL_CONNECTIVITY | NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + } + + @Test + public void testCaptivePortalOnPartialConnectivity() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + // Bring up a network with a captive portal. + // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String redirectUrl = "http://android.com/path"; + mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), redirectUrl); + + // Check that startCaptivePortalApp sends the expected command to NetworkMonitor. + mCm.startCaptivePortalApp(mWiFiNetworkAgent.getNetwork()); + verify(mWiFiNetworkAgent.mNetworkMonitor, timeout(TIMEOUT_MS).times(1)) + .launchCaptivePortalApp(); + + // Report that the captive portal is dismissed with partial connectivity, and check that + // callbacks are fired. + mWiFiNetworkAgent.setNetworkPartial(); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + waitForIdle(); + captivePortalCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, + mWiFiNetworkAgent); + + // Report partial connectivity is accepted. + mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */); + mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), true /* accept */, + false /* always */); + waitForIdle(); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + NetworkCapabilities nc = + validatedCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY, + mWiFiNetworkAgent); + + mCm.unregisterNetworkCallback(captivePortalCallback); + mCm.unregisterNetworkCallback(validatedCallback); + } + + @Test + public void testCaptivePortal() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + // Bring up a network with a captive portal. + // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String firstRedirectUrl = "http://example.com/firstPath"; + mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl); + + // Take down network. + // Expect onLost callback. + mWiFiNetworkAgent.disconnect(); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Bring up a network with a captive portal. + // Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String secondRedirectUrl = "http://example.com/secondPath"; + mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), secondRedirectUrl); + + // Make captive portal disappear then revalidate. + // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL. + mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Expect NET_CAPABILITY_VALIDATED onAvailable callback. + validatedCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Break network connectivity. + // Expect NET_CAPABILITY_VALIDATED onLost callback. + mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false); + validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + } + + @Test + public void testCaptivePortalApp() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + // Bring up wifi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + validatedCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + Network wifiNetwork = mWiFiNetworkAgent.getNetwork(); + + // Check that calling startCaptivePortalApp does nothing. + final int fastTimeoutMs = 100; + mCm.startCaptivePortalApp(wifiNetwork); + waitForIdle(); + verify(mWiFiNetworkAgent.mNetworkMonitor, never()).launchCaptivePortalApp(); + mServiceContext.expectNoStartActivityIntent(fastTimeoutMs); + + // Turn into a captive portal. + mWiFiNetworkAgent.setNetworkPortal("http://example.com", false /* isStrictMode */); + mCm.reportNetworkConnectivity(wifiNetwork, false); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Check that startCaptivePortalApp sends the expected command to NetworkMonitor. + mCm.startCaptivePortalApp(wifiNetwork); + waitForIdle(); + verify(mWiFiNetworkAgent.mNetworkMonitor).launchCaptivePortalApp(); + + // NetworkMonitor uses startCaptivePortal(Network, Bundle) (startCaptivePortalAppInternal) + final Bundle testBundle = new Bundle(); + final String testKey = "testkey"; + final String testValue = "testvalue"; + testBundle.putString(testKey, testValue); + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_GRANTED); + mCm.startCaptivePortalApp(wifiNetwork, testBundle); + final Intent signInIntent = mServiceContext.expectStartActivityIntent(TIMEOUT_MS); + assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, signInIntent.getAction()); + assertEquals(testValue, signInIntent.getStringExtra(testKey)); + + // Report that the captive portal is dismissed, and check that callbacks are fired + mWiFiNetworkAgent.setNetworkValid(false /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + mCm.unregisterNetworkCallback(validatedCallback); + mCm.unregisterNetworkCallback(captivePortalCallback); + } + + @Test + public void testAvoidOrIgnoreCaptivePortals() throws Exception { + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + final TestNetworkCallback validatedCallback = new TestNetworkCallback(); + final NetworkRequest validatedRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_VALIDATED).build(); + mCm.registerNetworkCallback(validatedRequest, validatedCallback); + + setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID); + // Bring up a network with a captive portal. + // Expect it to fail to connect and not result in any callbacks. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + String firstRedirectUrl = "http://example.com/firstPath"; + + mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl, false /* isStrictMode */); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent.expectPreventReconnectReceived(); + + assertNoCallbacks(captivePortalCallback, validatedCallback); + } + + @Test + public void testCaptivePortalApi() throws Exception { + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final String redirectUrl = "http://example.com/firstPath"; + + mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + final CaptivePortalData testData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(redirectUrl)) + .setBytesRemaining(12345L) + .build(); + + mWiFiNetworkAgent.notifyCapportApiDataChanged(testData); + + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> testData.equals(lp.getCaptivePortalData())); + + final LinkProperties newLps = new LinkProperties(); + newLps.setMtu(1234); + mWiFiNetworkAgent.sendLinkProperties(newLps); + // CaptivePortalData is not lost and unchanged when LPs are received from the NetworkAgent + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> testData.equals(lp.getCaptivePortalData()) && lp.getMtu() == 1234); + } + + private TestNetworkCallback setupNetworkCallbackAndConnectToWifi() throws Exception { + // Grant NETWORK_SETTINGS permission to be able to receive LinkProperties change callbacks + // with sensitive (captive portal) data + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback captivePortalCallback = new TestNetworkCallback(); + final NetworkRequest captivePortalRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build(); + mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + mWiFiNetworkAgent.connectWithCaptivePortal(TEST_REDIRECT_URL, false /* isStrictMode */); + captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + return captivePortalCallback; + } + + private class CaptivePortalTestData { + CaptivePortalTestData(CaptivePortalData naPasspointData, CaptivePortalData capportData, + CaptivePortalData naOtherData, CaptivePortalData expectedMergedPasspointData, + CaptivePortalData expectedMergedOtherData) { + mNaPasspointData = naPasspointData; + mCapportData = capportData; + mNaOtherData = naOtherData; + mExpectedMergedPasspointData = expectedMergedPasspointData; + mExpectedMergedOtherData = expectedMergedOtherData; + } + + public final CaptivePortalData mNaPasspointData; + public final CaptivePortalData mCapportData; + public final CaptivePortalData mNaOtherData; + public final CaptivePortalData mExpectedMergedPasspointData; + public final CaptivePortalData mExpectedMergedOtherData; + + } + + private CaptivePortalTestData setupCaptivePortalData() { + final CaptivePortalData capportData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL)) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_CAPPORT)) + .setUserPortalUrl(Uri.parse(TEST_USER_PORTAL_API_URL_CAPPORT)) + .setExpiryTime(1000000L) + .setBytesRemaining(12345L) + .build(); + + final CaptivePortalData naPasspointData = new CaptivePortalData.Builder() + .setBytesRemaining(80802L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + + final CaptivePortalData naOtherData = new CaptivePortalData.Builder() + .setBytesRemaining(80802L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_OTHER), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) + .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_OTHER), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_OTHER) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + + final CaptivePortalData expectedMergedPasspointData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL)) + .setBytesRemaining(12345L) + .setExpiryTime(1000000L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setUserPortalUrl(Uri.parse(TEST_TERMS_AND_CONDITIONS_URL_NA_PASSPOINT), + CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + + final CaptivePortalData expectedMergedOtherData = new CaptivePortalData.Builder() + .setUserPortalUrl(Uri.parse(TEST_REDIRECT_URL)) + .setBytesRemaining(12345L) + .setExpiryTime(1000000L) + .setVenueInfoUrl(Uri.parse(TEST_VENUE_URL_CAPPORT)) + .setUserPortalUrl(Uri.parse(TEST_USER_PORTAL_API_URL_CAPPORT)) + .setVenueFriendlyName(TEST_FRIENDLY_NAME).build(); + return new CaptivePortalTestData(naPasspointData, capportData, naOtherData, + expectedMergedPasspointData, expectedMergedOtherData); + } + + @Test + public void testMergeCaptivePortalApiWithFriendlyNameAndVenueUrl() throws Exception { + final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi(); + final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData(); + + // Baseline capport data + mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData); + + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData())); + + // Venue URL, T&C URL and friendly name from Network agent with Passpoint source, confirm + // that API data gets precedence on the bytes remaining. + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setCaptivePortalData(captivePortalTestData.mNaPasspointData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the capport data is merged + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedPasspointData + .equals(lp.getCaptivePortalData())); + + // Now send this information from non-Passpoint source, confirm that Capport data takes + // precedence + linkProperties.setCaptivePortalData(captivePortalTestData.mNaOtherData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the capport data is merged + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedOtherData + .equals(lp.getCaptivePortalData())); + + // Create a new LP with no Network agent capport data + final LinkProperties newLps = new LinkProperties(); + newLps.setMtu(1234); + mWiFiNetworkAgent.sendLinkProperties(newLps); + // CaptivePortalData is not lost and has the original values when LPs are received from the + // NetworkAgent + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData()) + && lp.getMtu() == 1234); + + // Now send capport data only from the Network agent + mWiFiNetworkAgent.notifyCapportApiDataChanged(null); + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> lp.getCaptivePortalData() == null); + + newLps.setCaptivePortalData(captivePortalTestData.mNaPasspointData); + mWiFiNetworkAgent.sendLinkProperties(newLps); + + // Make sure that only the network agent capport data is available + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mNaPasspointData.equals(lp.getCaptivePortalData())); + } + + @Test + public void testMergeCaptivePortalDataFromNetworkAgentFirstThenCapport() throws Exception { + final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi(); + final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData(); + + // Venue URL and friendly name from Network agent, confirm that API data gets precedence + // on the bytes remaining. + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setCaptivePortalData(captivePortalTestData.mNaPasspointData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the data is saved correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mNaPasspointData.equals(lp.getCaptivePortalData())); + + // Expected merged data: Network agent data is preferred, and values that are not used by + // it are merged from capport data + mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData); + + // Make sure that the Capport data is merged correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedPasspointData.equals( + lp.getCaptivePortalData())); + + // Now set the naData to null + linkProperties.setCaptivePortalData(null); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the Capport data is retained correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mCapportData.equals(lp.getCaptivePortalData())); + } + + @Test + public void testMergeCaptivePortalDataFromNetworkAgentOtherSourceFirstThenCapport() + throws Exception { + final TestNetworkCallback captivePortalCallback = setupNetworkCallbackAndConnectToWifi(); + final CaptivePortalTestData captivePortalTestData = setupCaptivePortalData(); + + // Venue URL and friendly name from Network agent, confirm that API data gets precedence + // on the bytes remaining. + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setCaptivePortalData(captivePortalTestData.mNaOtherData); + mWiFiNetworkAgent.sendLinkProperties(linkProperties); + + // Make sure that the data is saved correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mNaOtherData.equals(lp.getCaptivePortalData())); + + // Expected merged data: Network agent data is preferred, and values that are not used by + // it are merged from capport data + mWiFiNetworkAgent.notifyCapportApiDataChanged(captivePortalTestData.mCapportData); + + // Make sure that the Capport data is merged correctly + captivePortalCallback.expectLinkPropertiesThat(mWiFiNetworkAgent, + lp -> captivePortalTestData.mExpectedMergedOtherData.equals( + lp.getCaptivePortalData())); + } + + private NetworkRequest.Builder newWifiRequestBuilder() { + return new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI); + } + + /** + * Verify request matching behavior with network specifiers. + * + * This test does not check updating the specifier on a live network because the specifier is + * immutable and this triggers a WTF in + * {@link ConnectivityService#mixInCapabilities(NetworkAgentInfo, NetworkCapabilities)}. + */ + @Test + public void testNetworkSpecifier() throws Exception { + // A NetworkSpecifier subclass that matches all networks but must not be visible to apps. + class ConfidentialMatchAllNetworkSpecifier extends NetworkSpecifier implements + Parcelable { + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + return true; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) {} + + @Override + public NetworkSpecifier redact() { + return null; + } + } + + // A network specifier that matches either another LocalNetworkSpecifier with the same + // string or a ConfidentialMatchAllNetworkSpecifier, and can be passed to apps as is. + class LocalStringNetworkSpecifier extends NetworkSpecifier implements Parcelable { + private String mString; + + LocalStringNetworkSpecifier(String string) { + mString = string; + } + + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + if (other instanceof LocalStringNetworkSpecifier) { + return TextUtils.equals(mString, + ((LocalStringNetworkSpecifier) other).mString); + } + if (other instanceof ConfidentialMatchAllNetworkSpecifier) return true; + return false; + } + + @Override + public int describeContents() { + return 0; + } + @Override + public void writeToParcel(Parcel dest, int flags) {} + } + + + NetworkRequest rEmpty1 = newWifiRequestBuilder().build(); + NetworkRequest rEmpty2 = newWifiRequestBuilder().setNetworkSpecifier((String) null).build(); + NetworkRequest rEmpty3 = newWifiRequestBuilder().setNetworkSpecifier("").build(); + NetworkRequest rEmpty4 = newWifiRequestBuilder().setNetworkSpecifier( + (NetworkSpecifier) null).build(); + NetworkRequest rFoo = newWifiRequestBuilder().setNetworkSpecifier( + new LocalStringNetworkSpecifier("foo")).build(); + NetworkRequest rBar = newWifiRequestBuilder().setNetworkSpecifier( + new LocalStringNetworkSpecifier("bar")).build(); + + TestNetworkCallback cEmpty1 = new TestNetworkCallback(); + TestNetworkCallback cEmpty2 = new TestNetworkCallback(); + TestNetworkCallback cEmpty3 = new TestNetworkCallback(); + TestNetworkCallback cEmpty4 = new TestNetworkCallback(); + TestNetworkCallback cFoo = new TestNetworkCallback(); + TestNetworkCallback cBar = new TestNetworkCallback(); + TestNetworkCallback[] emptyCallbacks = new TestNetworkCallback[] { + cEmpty1, cEmpty2, cEmpty3, cEmpty4 }; + + mCm.registerNetworkCallback(rEmpty1, cEmpty1); + mCm.registerNetworkCallback(rEmpty2, cEmpty2); + mCm.registerNetworkCallback(rEmpty3, cEmpty3); + mCm.registerNetworkCallback(rEmpty4, cEmpty4); + mCm.registerNetworkCallback(rFoo, cFoo); + mCm.registerNetworkCallback(rBar, cBar); + + LocalStringNetworkSpecifier nsFoo = new LocalStringNetworkSpecifier("foo"); + LocalStringNetworkSpecifier nsBar = new LocalStringNetworkSpecifier("bar"); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, null /* specifier */, + cEmpty1, cEmpty2, cEmpty3, cEmpty4); + assertNoCallbacks(cFoo, cBar); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setNetworkSpecifier(nsFoo); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, nsFoo, + cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo); + cBar.assertNoCallback(); + assertEquals(nsFoo, + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier()); + assertNoCallbacks(cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setNetworkSpecifier(nsBar); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, nsBar, + cEmpty1, cEmpty2, cEmpty3, cEmpty4, cBar); + cFoo.assertNoCallback(); + assertEquals(nsBar, + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier()); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cBar); + cFoo.assertNoCallback(); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.setNetworkSpecifier(new ConfidentialMatchAllNetworkSpecifier()); + mWiFiNetworkAgent.connect(false); + expectAvailableCallbacksUnvalidatedWithSpecifier(mWiFiNetworkAgent, null /* specifier */, + cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo, cBar); + assertNull( + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).getNetworkSpecifier()); + + mWiFiNetworkAgent.disconnect(); + expectOnLost(mWiFiNetworkAgent, cEmpty1, cEmpty2, cEmpty3, cEmpty4, cFoo, cBar); + } + + /** + * @return the context's attribution tag + */ + private String getAttributionTag() { + return mContext.getAttributionTag(); + } + + @Test + public void testInvalidNetworkSpecifier() { + assertThrows(IllegalArgumentException.class, () -> { + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.setNetworkSpecifier(new MatchAllNetworkSpecifier()); + }); + + assertThrows(IllegalArgumentException.class, () -> { + NetworkCapabilities networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(new MatchAllNetworkSpecifier()); + mService.requestNetwork(Process.INVALID_UID, networkCapabilities, + NetworkRequest.Type.REQUEST.ordinal(), null, 0, null, + ConnectivityManager.TYPE_WIFI, NetworkCallback.FLAG_NONE, + mContext.getPackageName(), getAttributionTag()); + }); + + class NonParcelableSpecifier extends NetworkSpecifier { + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + return false; + } + }; + class ParcelableSpecifier extends NonParcelableSpecifier implements Parcelable { + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel p, int flags) {} + } + + final NetworkRequest.Builder builder = + new NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET); + assertThrows(ClassCastException.class, () -> { + builder.setNetworkSpecifier(new NonParcelableSpecifier()); + Parcel parcelW = Parcel.obtain(); + builder.build().writeToParcel(parcelW, 0); + }); + + final NetworkRequest nr = + new NetworkRequest.Builder().addTransportType(TRANSPORT_ETHERNET) + .setNetworkSpecifier(new ParcelableSpecifier()) + .build(); + assertNotNull(nr); + + assertThrows(BadParcelableException.class, () -> { + Parcel parcelW = Parcel.obtain(); + nr.writeToParcel(parcelW, 0); + byte[] bytes = parcelW.marshall(); + parcelW.recycle(); + + Parcel parcelR = Parcel.obtain(); + parcelR.unmarshall(bytes, 0, bytes.length); + parcelR.setDataPosition(0); + NetworkRequest rereadNr = NetworkRequest.CREATOR.createFromParcel(parcelR); + }); + } + + @Test + public void testNetworkRequestUidSpoofSecurityException() throws Exception { + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + NetworkRequest networkRequest = newWifiRequestBuilder().build(); + TestNetworkCallback networkCallback = new TestNetworkCallback(); + doThrow(new SecurityException()).when(mAppOpsManager).checkPackage(anyInt(), anyString()); + assertThrows(SecurityException.class, () -> { + mCm.requestNetwork(networkRequest, networkCallback); + }); + } + + @Test + public void testInvalidSignalStrength() { + NetworkRequest r = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_WIFI) + .setSignalStrength(-75) + .build(); + // Registering a NetworkCallback with signal strength but w/o NETWORK_SIGNAL_STRENGTH_WAKEUP + // permission should get SecurityException. + assertThrows(SecurityException.class, () -> + mCm.registerNetworkCallback(r, new NetworkCallback())); + + assertThrows(SecurityException.class, () -> + mCm.registerNetworkCallback(r, PendingIntent.getService( + mServiceContext, 0 /* requestCode */, new Intent(), FLAG_IMMUTABLE))); + + // Requesting a Network with signal strength should get IllegalArgumentException. + assertThrows(IllegalArgumentException.class, () -> + mCm.requestNetwork(r, new NetworkCallback())); + + assertThrows(IllegalArgumentException.class, () -> + mCm.requestNetwork(r, PendingIntent.getService( + mServiceContext, 0 /* requestCode */, new Intent(), FLAG_IMMUTABLE))); + } + + @Test + public void testRegisterDefaultNetworkCallback() throws Exception { + // NETWORK_SETTINGS is necessary to call registerSystemDefaultNetworkCallback. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, handler); + systemDefaultCallback.assertNoCallback(); + + // Create a TRANSPORT_CELLULAR request to keep the mobile interface up + // whenever Wi-Fi is up. Without this, the mobile network agent is + // reaped before any other activity can take place. + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + cellNetworkCallback.assertNoCallback(); + + // Bring up cell and expect CALLBACK_AVAILABLE. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + systemDefaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up wifi and expect CALLBACK_AVAILABLE. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + cellNetworkCallback.assertNoCallback(); + defaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring down cell. Expect no default network callback, since it wasn't the default. + mCellNetworkAgent.disconnect(); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultNetworkCallback.assertNoCallback(); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring up cell. Expect no default network callback, since it won't be the default. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultNetworkCallback.assertNoCallback(); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(systemDefaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + // Bring down wifi. Expect the default network callback to notified of LOST wifi + // followed by AVAILABLE cell. + mWiFiNetworkAgent.disconnect(); + cellNetworkCallback.assertNoCallback(); + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + mCellNetworkAgent.disconnect(); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(null, systemDefaultCallback.getLastAvailableNetwork()); + + mMockVpn.disconnect(); + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + systemDefaultCallback.assertNoCallback(); + waitForIdle(); + assertEquals(null, mCm.getActiveNetwork()); + } + + @Test + public void testAdditionalStateCallbacks() throws Exception { + // File a network request for mobile. + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + // Bring up the mobile network. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + // We should get onAvailable(), onCapabilitiesChanged(), and + // onLinkPropertiesChanged() in rapid succession. Additionally, we + // should get onCapabilitiesChanged() when the mobile network validates. + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + + // Update LinkProperties. + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("foonet_data0"); + mCellNetworkAgent.sendLinkProperties(lp); + // We should get onLinkPropertiesChanged(). + cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + + // Suspend the network. + mCellNetworkAgent.suspend(); + cellNetworkCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_SUSPENDED, + mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertEquals(NetworkInfo.State.SUSPENDED, mCm.getActiveNetworkInfo().getState()); + + // Register a garden variety default network request. + TestNetworkCallback dfltNetworkCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(dfltNetworkCallback); + // We should get onAvailable(), onCapabilitiesChanged(), onLinkPropertiesChanged(), + // as well as onNetworkSuspended() in rapid succession. + dfltNetworkCallback.expectAvailableAndSuspendedCallbacks(mCellNetworkAgent, true); + dfltNetworkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(dfltNetworkCallback); + + mCellNetworkAgent.resume(); + cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_SUSPENDED, + mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.RESUMED, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertEquals(NetworkInfo.State.CONNECTED, mCm.getActiveNetworkInfo().getState()); + + dfltNetworkCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(dfltNetworkCallback); + // This time onNetworkSuspended should not be called. + dfltNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + dfltNetworkCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(dfltNetworkCallback); + mCm.unregisterNetworkCallback(cellNetworkCallback); + } + + @Test + public void testRegisterPrivilegedDefaultCallbacksRequireNetworkSettings() throws Exception { + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false /* validated */); + + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + final TestNetworkCallback callback = new TestNetworkCallback(); + assertThrows(SecurityException.class, + () -> mCm.registerSystemDefaultNetworkCallback(callback, handler)); + callback.assertNoCallback(); + assertThrows(SecurityException.class, + () -> mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler)); + callback.assertNoCallback(); + + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + mCm.registerSystemDefaultNetworkCallback(callback, handler); + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + mCm.unregisterNetworkCallback(callback); + + mCm.registerDefaultNetworkCallbackForUid(APP1_UID, callback, handler); + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + mCm.unregisterNetworkCallback(callback); + } + + @Test + public void testNetworkCallbackWithNullUids() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Attempt to file a callback for networks applying to another UID. This does not actually + // work, because this code does not currently have permission to do so. The callback behaves + // exactly the same as the one registered just above. + final int otherUid = UserHandle.getUid(RESTRICTED_USER, VPN_UID); + final NetworkRequest otherUidRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .setUids(UidRange.toIntRanges(uidRangesForUids(otherUid))) + .build(); + final TestNetworkCallback otherUidCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(otherUidRequest, otherUidCallback); + + final NetworkRequest includeOtherUidsRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .setIncludeOtherUidNetworks(true) + .build(); + final TestNetworkCallback includeOtherUidsCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(includeOtherUidsRequest, includeOtherUidsCallback); + + // Both callbacks see a network with no specifier that applies to their UID. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + otherUidCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + includeOtherUidsCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + otherUidCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + includeOtherUidsCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Only the includeOtherUidsCallback sees a VPN that does not apply to its UID. + final UidRange range = UidRange.createForUser(UserHandle.of(RESTRICTED_USER)); + final Set<UidRange> vpnRanges = Collections.singleton(range); + mMockVpn.establish(new LinkProperties(), VPN_UID, vpnRanges); + includeOtherUidsCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + callback.assertNoCallback(); + otherUidCallback.assertNoCallback(); + + mMockVpn.disconnect(); + includeOtherUidsCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + callback.assertNoCallback(); + otherUidCallback.assertNoCallback(); + } + + private static class RedactableNetworkSpecifier extends NetworkSpecifier { + public static final int ID_INVALID = -1; + + public final int networkId; + + RedactableNetworkSpecifier(int networkId) { + this.networkId = networkId; + } + + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + return other instanceof RedactableNetworkSpecifier + && this.networkId == ((RedactableNetworkSpecifier) other).networkId; + } + + @Override + public NetworkSpecifier redact() { + return new RedactableNetworkSpecifier(ID_INVALID); + } + } + + @Test + public void testNetworkCallbackWithNullUidsRedactsSpecifier() throws Exception { + final RedactableNetworkSpecifier specifier = new RedactableNetworkSpecifier(42); + final NetworkRequest request = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Attempt to file a callback for networks applying to another UID. This does not actually + // work, because this code does not currently have permission to do so. The callback behaves + // exactly the same as the one registered just above. + final int otherUid = UserHandle.getUid(RESTRICTED_USER, VPN_UID); + final NetworkRequest otherUidRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .setUids(UidRange.toIntRanges(uidRangesForUids(otherUid))) + .build(); + final TestNetworkCallback otherUidCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(otherUidRequest, otherUidCallback); + + final NetworkRequest includeOtherUidsRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier) + .setIncludeOtherUidNetworks(true) + .build(); + final TestNetworkCallback includeOtherUidsCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(includeOtherUidsRequest, callback); + + // Only the regular callback sees the network, because callbacks filed with no UID have + // their specifiers redacted. + final LinkProperties emptyLp = new LinkProperties(); + final NetworkCapabilities ncTemplate = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .setNetworkSpecifier(specifier); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, emptyLp, ncTemplate); + mWiFiNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + otherUidCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + includeOtherUidsCallback.assertNoCallback(); + } + + private void setCaptivePortalMode(int mode) { + ContentResolver cr = mServiceContext.getContentResolver(); + Settings.Global.putInt(cr, ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE, mode); + } + + private void setAlwaysOnNetworks(boolean enable) { + ContentResolver cr = mServiceContext.getContentResolver(); + Settings.Global.putInt(cr, ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON, + enable ? 1 : 0); + mService.updateAlwaysOnNetworks(); + waitForIdle(); + } + + private void setPrivateDnsSettings(int mode, String specifier) { + ConnectivitySettingsManager.setPrivateDnsMode(mServiceContext, mode); + ConnectivitySettingsManager.setPrivateDnsHostname(mServiceContext, specifier); + mService.updatePrivateDnsSettings(); + waitForIdle(); + } + + private boolean isForegroundNetwork(TestNetworkAgentWrapper network) { + NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork()); + assertNotNull(nc); + return nc.hasCapability(NET_CAPABILITY_FOREGROUND); + } + + @Test + public void testBackgroundNetworks() throws Exception { + // Create a cellular background request. + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + final TestNetworkCallback cellBgCallback = new TestNetworkCallback(); + mCm.requestBackgroundNetwork(new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(), + cellBgCallback, mCsHandlerThread.getThreadHandler()); + + // Make callbacks for monitoring. + final NetworkRequest request = new NetworkRequest.Builder().build(); + final NetworkRequest fgRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_FOREGROUND).build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback fgCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + mCm.registerNetworkCallback(fgRequest, fgCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + fgCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + + // When wifi connects, cell lingers. + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + fgCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + fgCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + fgCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // When lingering is complete, cell is still there but is now in the background. + waitForIdle(); + int timeoutMs = TEST_LINGER_DELAY_MS + TEST_LINGER_DELAY_MS / 4; + fgCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, timeoutMs); + // Expect a network capabilities update sans FOREGROUND. + callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + assertFalse(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // File a cell request and check that cell comes into the foreground. + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + final TestNetworkCallback cellCallback = new TestNetworkCallback(); + mCm.requestNetwork(cellRequest, cellCallback); + cellCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + fgCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + // Expect a network capabilities update with FOREGROUND, because the most recent + // request causes its state to change. + cellCallback.expectCapabilitiesWith(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + callback.expectCapabilitiesWith(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // Release the request. The network immediately goes into the background, since it was not + // lingering. + mCm.unregisterNetworkCallback(cellCallback); + fgCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + // Expect a network capabilities update sans FOREGROUND. + callback.expectCapabilitiesWithout(NET_CAPABILITY_FOREGROUND, mCellNetworkAgent); + assertFalse(isForegroundNetwork(mCellNetworkAgent)); + assertTrue(isForegroundNetwork(mWiFiNetworkAgent)); + + // Disconnect wifi and check that cell is foreground again. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + fgCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + fgCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertTrue(isForegroundNetwork(mCellNetworkAgent)); + + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(fgCallback); + mCm.unregisterNetworkCallback(cellBgCallback); + } + + @Ignore // This test has instrinsic chances of spurious failures: ignore for continuous testing. + public void benchmarkRequestRegistrationAndCallbackDispatch() throws Exception { + // TODO: turn this unit test into a real benchmarking test. + // Benchmarks connecting and switching performance in the presence of a large number of + // NetworkRequests. + // 1. File NUM_REQUESTS requests. + // 2. Have a network connect. Wait for NUM_REQUESTS onAvailable callbacks to fire. + // 3. Have a new network connect and outscore the previous. Wait for NUM_REQUESTS onLosing + // and NUM_REQUESTS onAvailable callbacks to fire. + // See how long it took. + final int NUM_REQUESTS = 90; + final int REGISTER_TIME_LIMIT_MS = 200; + final int CONNECT_TIME_LIMIT_MS = 60; + final int SWITCH_TIME_LIMIT_MS = 60; + final int UNREGISTER_TIME_LIMIT_MS = 20; + + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + final NetworkCallback[] callbacks = new NetworkCallback[NUM_REQUESTS]; + final CountDownLatch availableLatch = new CountDownLatch(NUM_REQUESTS); + final CountDownLatch losingLatch = new CountDownLatch(NUM_REQUESTS); + + for (int i = 0; i < NUM_REQUESTS; i++) { + callbacks[i] = new NetworkCallback() { + @Override public void onAvailable(Network n) { availableLatch.countDown(); } + @Override public void onLosing(Network n, int t) { losingLatch.countDown(); } + }; + } + + assertRunsInAtMost("Registering callbacks", REGISTER_TIME_LIMIT_MS, () -> { + for (NetworkCallback cb : callbacks) { + mCm.registerNetworkCallback(request, cb); + } + }); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + // Don't request that the network validate, because otherwise connect() will block until + // the network gets NET_CAPABILITY_VALIDATED, after all the callbacks below have fired, + // and we won't actually measure anything. + mCellNetworkAgent.connect(false); + + long onAvailableDispatchingDuration = durationOf(() -> { + await(availableLatch, 10 * CONNECT_TIME_LIMIT_MS); + }); + Log.d(TAG, String.format("Dispatched %d of %d onAvailable callbacks in %dms", + NUM_REQUESTS - availableLatch.getCount(), NUM_REQUESTS, + onAvailableDispatchingDuration)); + assertTrue(String.format("Dispatching %d onAvailable callbacks in %dms, expected %dms", + NUM_REQUESTS, onAvailableDispatchingDuration, CONNECT_TIME_LIMIT_MS), + onAvailableDispatchingDuration <= CONNECT_TIME_LIMIT_MS); + + // Give wifi a high enough score that we'll linger cell when wifi comes up. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.adjustScore(40); + mWiFiNetworkAgent.connect(false); + + long onLostDispatchingDuration = durationOf(() -> { + await(losingLatch, 10 * SWITCH_TIME_LIMIT_MS); + }); + Log.d(TAG, String.format("Dispatched %d of %d onLosing callbacks in %dms", + NUM_REQUESTS - losingLatch.getCount(), NUM_REQUESTS, onLostDispatchingDuration)); + assertTrue(String.format("Dispatching %d onLosing callbacks in %dms, expected %dms", + NUM_REQUESTS, onLostDispatchingDuration, SWITCH_TIME_LIMIT_MS), + onLostDispatchingDuration <= SWITCH_TIME_LIMIT_MS); + + assertRunsInAtMost("Unregistering callbacks", UNREGISTER_TIME_LIMIT_MS, () -> { + for (NetworkCallback cb : callbacks) { + mCm.unregisterNetworkCallback(cb); + } + }); + } + + @Test + public void testMobileDataAlwaysOn() throws Exception { + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid()); + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + + final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NET_CAPABILITY_INTERNET); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + + // Register the factory and expect it to start looking for a network. + testFactory.register(); + + try { + // Expect the factory to receive the default network request. + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + assertTrue(testFactory.getMyStartRequested()); + + // Bring up wifi. The factory stops looking for a network. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + // Score 60 - 40 penalty for not validated yet, then 60 when it validates + mWiFiNetworkAgent.connect(true); + // The network connects with a low score, so the offer can still beat it and + // nothing happens. Then the network validates, and the offer with its filter score + // of 40 can no longer beat it and the request is removed. + testFactory.expectRequestRemove(); + testFactory.assertRequestCountEquals(0); + + assertFalse(testFactory.getMyStartRequested()); + + // Turn on mobile data always on. This request will not match the wifi request, so + // it will be sent to the test factory whose filters allow to see it. + setAlwaysOnNetworks(true); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + + assertTrue(testFactory.getMyStartRequested()); + + // Bring up cell data and check that the factory stops looking. + assertLength(1, mCm.getAllNetworks()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false); + cellNetworkCallback.expectAvailableCallbacks(mCellNetworkAgent, false, false, false, + TEST_CALLBACK_TIMEOUT_MS); + // When cell connects, it will satisfy the "mobile always on request" right away + // by virtue of being the only network that can satisfy the request. However, its + // score is low (50 - 40 = 10) so the test factory can still hope to beat it. + expectNoRequestChanged(testFactory); + + // Next, cell validates. This gives it a score of 50 and the test factory can't + // hope to beat that according to its filters. It will see the message that its + // offer is now unnecessary. + mCellNetworkAgent.setNetworkValid(true); + // Need a trigger point to let NetworkMonitor tell ConnectivityService that network is + // validated – see testPartialConnectivity. + mCm.reportNetworkConnectivity(mCellNetworkAgent.getNetwork(), true); + cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mCellNetworkAgent); + testFactory.expectRequestRemove(); + testFactory.assertRequestCountEquals(0); + // Accordingly, the factory shouldn't be started. + assertFalse(testFactory.getMyStartRequested()); + + // Check that cell data stays up. + waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertLength(2, mCm.getAllNetworks()); + + // Cell disconnects. There is still the "mobile data always on" request outstanding, + // and the test factory should see it now that it isn't hopelessly outscored. + mCellNetworkAgent.disconnect(); + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + assertLength(1, mCm.getAllNetworks()); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + + // Reconnect cell validated, see the request disappear again. Then withdraw the + // mobile always on request. This will tear down cell, and there shouldn't be a + // blip where the test factory briefly sees the request or anything. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertLength(2, mCm.getAllNetworks()); + testFactory.expectRequestRemove(); + testFactory.assertRequestCountEquals(0); + setAlwaysOnNetworks(false); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(0); + assertFalse(testFactory.getMyStartRequested()); + // ... and cell data to be torn down immediately since it is no longer nascent. + cellNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + waitForIdle(); + assertLength(1, mCm.getAllNetworks()); + } finally { + testFactory.terminate(); + mCm.unregisterNetworkCallback(cellNetworkCallback); + handlerThread.quit(); + } + } + + @Test + public void testAvoidBadWifiSetting() throws Exception { + final ContentResolver cr = mServiceContext.getContentResolver(); + final String settingName = ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI; + + mPolicyTracker.mConfigRestrictsAvoidBadWifi = false; + String[] values = new String[] {null, "0", "1"}; + for (int i = 0; i < values.length; i++) { + Settings.Global.putInt(cr, settingName, 1); + mPolicyTracker.reevaluate(); + waitForIdle(); + String msg = String.format("config=false, setting=%s", values[i]); + assertTrue(mService.avoidBadWifi()); + assertFalse(msg, mPolicyTracker.shouldNotifyWifiUnvalidated()); + } + + mPolicyTracker.mConfigRestrictsAvoidBadWifi = true; + + Settings.Global.putInt(cr, settingName, 0); + mPolicyTracker.reevaluate(); + waitForIdle(); + assertFalse(mService.avoidBadWifi()); + assertFalse(mPolicyTracker.shouldNotifyWifiUnvalidated()); + + Settings.Global.putInt(cr, settingName, 1); + mPolicyTracker.reevaluate(); + waitForIdle(); + assertTrue(mService.avoidBadWifi()); + assertFalse(mPolicyTracker.shouldNotifyWifiUnvalidated()); + + Settings.Global.putString(cr, settingName, null); + mPolicyTracker.reevaluate(); + waitForIdle(); + assertFalse(mService.avoidBadWifi()); + assertTrue(mPolicyTracker.shouldNotifyWifiUnvalidated()); + } + + @Ignore("Refactoring in progress b/178071397") + @Test + public void testAvoidBadWifi() throws Exception { + final ContentResolver cr = mServiceContext.getContentResolver(); + + // Pretend we're on a carrier that restricts switching away from bad wifi. + mPolicyTracker.mConfigRestrictsAvoidBadWifi = true; + + // File a request for cell to ensure it doesn't go down. + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + NetworkRequest validatedWifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_VALIDATED) + .build(); + TestNetworkCallback validatedWifiCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback); + + Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 0); + mPolicyTracker.reevaluate(); + + // Bring up validated cell. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + Network cellNetwork = mCellNetworkAgent.getNetwork(); + + // Bring up validated wifi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + validatedWifiCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + Network wifiNetwork = mWiFiNetworkAgent.getNetwork(); + + // Fail validation on wifi. + mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(wifiNetwork, false); + defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Because avoid bad wifi is off, we don't switch to cellular. + defaultCallback.assertNoCallback(); + assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertEquals(mCm.getActiveNetwork(), wifiNetwork); + + // Simulate switching to a carrier that does not restrict avoiding bad wifi, and expect + // that we switch back to cell. + mPolicyTracker.mConfigRestrictsAvoidBadWifi = false; + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // Switch back to a restrictive carrier. + mPolicyTracker.mConfigRestrictsAvoidBadWifi = true; + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mCm.getActiveNetwork(), wifiNetwork); + + // Simulate the user selecting "switch" on the dialog, and check that we switch to cell. + mCm.setAvoidUnvalidated(wifiNetwork); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // Disconnect and reconnect wifi to clear the one-time switch above. + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + validatedWifiCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + wifiNetwork = mWiFiNetworkAgent.getNetwork(); + + // Fail validation on wifi and expect the dialog to appear. + mWiFiNetworkAgent.setNetworkInvalid(false /* isStrictMode */); + mCm.reportNetworkConnectivity(wifiNetwork, false); + defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Simulate the user selecting "switch" and checking the don't ask again checkbox. + Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1); + mPolicyTracker.reevaluate(); + + // We now switch to cell. + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability( + NET_CAPABILITY_VALIDATED)); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // Simulate the user turning the cellular fallback setting off and then on. + // We switch to wifi and then to cell. + Settings.Global.putString(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null); + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mCm.getActiveNetwork(), wifiNetwork); + Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1); + mPolicyTracker.reevaluate(); + defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertEquals(mCm.getActiveNetwork(), cellNetwork); + + // If cell goes down, we switch to wifi. + mCellNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + validatedWifiCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(cellNetworkCallback); + mCm.unregisterNetworkCallback(validatedWifiCallback); + mCm.unregisterNetworkCallback(defaultCallback); + } + + @Test + public void testMeteredMultipathPreferenceSetting() throws Exception { + final ContentResolver cr = mServiceContext.getContentResolver(); + final String settingName = ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE; + + for (int config : Arrays.asList(0, 3, 2)) { + for (String setting: Arrays.asList(null, "0", "2", "1")) { + mPolicyTracker.mConfigMeteredMultipathPreference = config; + Settings.Global.putString(cr, settingName, setting); + mPolicyTracker.reevaluate(); + waitForIdle(); + + final int expected = (setting != null) ? Integer.parseInt(setting) : config; + String msg = String.format("config=%d, setting=%s", config, setting); + assertEquals(msg, expected, mCm.getMultipathPreference(null)); + } + } + } + + /** + * Validate that a satisfied network request does not trigger onUnavailable() once the + * time-out period expires. + */ + @Test + public void testSatisfiedNetworkRequestDoesNotTriggerOnUnavailable() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(nr, networkCallback, TEST_REQUEST_TIMEOUT_MS); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacks(mWiFiNetworkAgent, false, false, false, + TEST_CALLBACK_TIMEOUT_MS); + + // pass timeout and validate that UNAVAILABLE is not called + networkCallback.assertNoCallback(); + } + + /** + * Validate that a satisfied network request followed by a disconnected (lost) network does + * not trigger onUnavailable() once the time-out period expires. + */ + @Test + public void testSatisfiedThenLostNetworkRequestDoesNotTriggerOnUnavailable() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.requestNetwork(nr, networkCallback, TEST_REQUEST_TIMEOUT_MS); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacks(mWiFiNetworkAgent, false, false, false, + TEST_CALLBACK_TIMEOUT_MS); + mWiFiNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + // Validate that UNAVAILABLE is not called + networkCallback.assertNoCallback(); + } + + /** + * Validate that when a time-out is specified for a network request the onUnavailable() + * callback is called when time-out expires. Then validate that if network request is + * (somehow) satisfied - the callback isn't called later. + */ + @Test + public void testTimedoutNetworkRequest() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + final int timeoutMs = 10; + mCm.requestNetwork(nr, networkCallback, timeoutMs); + + // pass timeout and validate that UNAVAILABLE is called + networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null); + + // create a network satisfying request - validate that request not triggered + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.assertNoCallback(); + } + + /** + * Validate that when a network request is unregistered (cancelled), no posterior event can + * trigger the callback. + */ + @Test + public void testNoCallbackAfterUnregisteredNetworkRequest() throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + final int timeoutMs = 10; + + mCm.requestNetwork(nr, networkCallback, timeoutMs); + mCm.unregisterNetworkCallback(networkCallback); + // Regardless of the timeout, unregistering the callback in ConnectivityManager ensures + // that this callback will not be called. + networkCallback.assertNoCallback(); + + // create a network satisfying request - validate that request not triggered + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + networkCallback.assertNoCallback(); + } + + @Test + public void testUnfulfillableNetworkRequest() throws Exception { + runUnfulfillableNetworkRequest(false); + } + + @Test + public void testUnfulfillableNetworkRequestAfterUnregister() throws Exception { + runUnfulfillableNetworkRequest(true); + } + + /** + * Validate the callback flow for a factory releasing a request as unfulfillable. + */ + private void runUnfulfillableNetworkRequest(boolean preUnregister) throws Exception { + NetworkRequest nr = new NetworkRequest.Builder().addTransportType( + NetworkCapabilities.TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + + final HandlerThread handlerThread = new HandlerThread("testUnfulfillableNetworkRequest"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + + // Register the factory and expect it to receive the default request. + testFactory.register(); + testFactory.expectRequestAdd(); + + try { + // Now file the test request and expect it. + mCm.requestNetwork(nr, networkCallback); + final NetworkRequest newRequest = testFactory.expectRequestAdd().request; + + if (preUnregister) { + mCm.unregisterNetworkCallback(networkCallback); + + // The request has been released : the factory should see it removed + // immediately. + testFactory.expectRequestRemove(); + + // Simulate the factory releasing the request as unfulfillable: no-op since + // the callback has already been unregistered (but a test that no exceptions are + // thrown). + testFactory.triggerUnfulfillable(newRequest); + } else { + // Simulate the factory releasing the request as unfulfillable and expect + // onUnavailable! + testFactory.triggerUnfulfillable(newRequest); + + networkCallback.expectCallback(CallbackEntry.UNAVAILABLE, (Network) null); + + // Declaring a request unfulfillable releases it automatically. + testFactory.expectRequestRemove(); + + // unregister network callback - a no-op (since already freed by the + // on-unavailable), but should not fail or throw exceptions. + mCm.unregisterNetworkCallback(networkCallback); + + // The factory should not see any further removal, as this request has + // already been removed. + } + } finally { + testFactory.terminate(); + handlerThread.quit(); + } + } + + private static class TestKeepaliveCallback extends PacketKeepaliveCallback { + + public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR } + + private class CallbackValue { + public CallbackType callbackType; + public int error; + + public CallbackValue(CallbackType type) { + this.callbackType = type; + this.error = PacketKeepalive.SUCCESS; + assertTrue("onError callback must have error", type != CallbackType.ON_ERROR); + } + + public CallbackValue(CallbackType type, int error) { + this.callbackType = type; + this.error = error; + assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR); + } + + @Override + public boolean equals(Object o) { + return o instanceof CallbackValue && + 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(CallbackType.ON_STARTED)); + } + + @Override + public void onStopped() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED)); + } + + @Override + public void onError(int error) { + mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + private void expectCallback(CallbackValue callbackValue) throws InterruptedException { + assertEquals(callbackValue, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + public void expectStarted() throws Exception { + expectCallback(new CallbackValue(CallbackType.ON_STARTED)); + } + + public void expectStopped() throws Exception { + expectCallback(new CallbackValue(CallbackType.ON_STOPPED)); + } + + public void expectError(int error) throws Exception { + expectCallback(new CallbackValue(CallbackType.ON_ERROR, error)); + } + } + + private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback { + + public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR }; + + private class CallbackValue { + public CallbackType callbackType; + public int error; + + CallbackValue(CallbackType type) { + this.callbackType = type; + this.error = SocketKeepalive.SUCCESS; + assertTrue("onError callback must have error", type != CallbackType.ON_ERROR); + } + + CallbackValue(CallbackType type, int error) { + this.callbackType = type; + this.error = error; + assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR); + } + + @Override + public boolean equals(Object o) { + return o instanceof CallbackValue + && 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 LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>(); + private final Executor mExecutor; + + TestSocketKeepaliveCallback(@NonNull Executor executor) { + mExecutor = executor; + } + + @Override + public void onStarted() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED)); + } + + @Override + public void onStopped() { + mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED)); + } + + @Override + public void onError(int error) { + mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + private void expectCallback(CallbackValue callbackValue) throws InterruptedException { + assertEquals(callbackValue, mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + } + + public void expectStarted() throws InterruptedException { + expectCallback(new CallbackValue(CallbackType.ON_STARTED)); + } + + public void expectStopped() throws InterruptedException { + expectCallback(new CallbackValue(CallbackType.ON_STOPPED)); + } + + public void expectError(int error) throws InterruptedException { + expectCallback(new CallbackValue(CallbackType.ON_ERROR, error)); + } + + public void assertNoCallback() { + waitForIdleSerialExecutor(mExecutor, TIMEOUT_MS); + CallbackValue cv = mCallbacks.peek(); + assertNull("Unexpected callback: " + cv, cv); + } + } + + private Network connectKeepaliveNetwork(LinkProperties lp) throws Exception { + // Ensure the network is disconnected before anything else occurs + if (mWiFiNetworkAgent != null) { + assertNull(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork())); + } + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + mWiFiNetworkAgent.connect(true); + b.expectBroadcast(); + verifyActiveNetwork(TRANSPORT_WIFI); + mWiFiNetworkAgent.sendLinkProperties(lp); + waitForIdle(); + return mWiFiNetworkAgent.getNetwork(); + } + + @Test + public void testPacketKeepalives() throws Exception { + InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35"); + InetAddress myIPv6 = InetAddress.getByName("2001:db8::1"); + InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888"); + + final int validKaInterval = 15; + final int invalidKaInterval = 9; + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + + Network notMyNet = new Network(61234); + Network myNet = connectKeepaliveNetwork(lp); + + TestKeepaliveCallback callback = new TestKeepaliveCallback(); + PacketKeepalive ka; + + // Attempt to start keepalives with invalid parameters and check for errors. + ka = mCm.startNattKeepalive(notMyNet, validKaInterval, callback, myIPv4, 1234, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK); + + ka = mCm.startNattKeepalive(myNet, invalidKaInterval, callback, myIPv4, 1234, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_INTERVAL); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 1234, dstIPv6); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + + // NAT-T is only supported for IPv4. + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv6, 1234, dstIPv6); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_PORT); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 123456, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_INVALID_PORT); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectError(PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED); + + // Check that a started keepalive can be stopped. + mWiFiNetworkAgent.setStartKeepaliveEvent(PacketKeepalive.SUCCESS); + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + mWiFiNetworkAgent.setStopKeepaliveEvent(PacketKeepalive.SUCCESS); + ka.stop(); + callback.expectStopped(); + + // Check that deleting the IP address stops the keepalive. + LinkProperties bogusLp = new LinkProperties(lp); + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25)); + bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25)); + mWiFiNetworkAgent.sendLinkProperties(bogusLp); + callback.expectError(PacketKeepalive.ERROR_INVALID_IP_ADDRESS); + mWiFiNetworkAgent.sendLinkProperties(lp); + + // Check that a started keepalive is stopped correctly when the network disconnects. + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + callback.expectError(PacketKeepalive.ERROR_INVALID_NETWORK); + + // ... and that stopping it after that has no adverse effects. + waitForIdle(); + final Network myNetAlias = myNet; + assertNull(mCm.getNetworkCapabilities(myNetAlias)); + ka.stop(); + + // Reconnect. + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(PacketKeepalive.SUCCESS); + + // Check that keepalive slots start from 1 and increment. The first one gets slot 1. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + ka = mCm.startNattKeepalive(myNet, validKaInterval, callback, myIPv4, 12345, dstIPv4); + callback.expectStarted(); + + // The second one gets slot 2. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(2); + TestKeepaliveCallback callback2 = new TestKeepaliveCallback(); + PacketKeepalive ka2 = mCm.startNattKeepalive( + myNet, validKaInterval, callback2, myIPv4, 6789, dstIPv4); + callback2.expectStarted(); + + // Now stop the first one and create a third. This also gets slot 1. + ka.stop(); + callback.expectStopped(); + + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + TestKeepaliveCallback callback3 = new TestKeepaliveCallback(); + PacketKeepalive ka3 = mCm.startNattKeepalive( + myNet, validKaInterval, callback3, myIPv4, 9876, dstIPv4); + callback3.expectStarted(); + + ka2.stop(); + callback2.expectStopped(); + + ka3.stop(); + callback3.expectStopped(); + } + + // Helper method to prepare the executor and run test + private void runTestWithSerialExecutors(ExceptionUtils.ThrowingConsumer<Executor> functor) + throws Exception { + final ExecutorService executorSingleThread = Executors.newSingleThreadExecutor(); + final Executor executorInline = (Runnable r) -> r.run(); + functor.accept(executorSingleThread); + executorSingleThread.shutdown(); + functor.accept(executorInline); + } + + @Test + public void testNattSocketKeepalives() throws Exception { + runTestWithSerialExecutors(executor -> doTestNattSocketKeepalivesWithExecutor(executor)); + runTestWithSerialExecutors(executor -> doTestNattSocketKeepalivesFdWithExecutor(executor)); + } + + private void doTestNattSocketKeepalivesWithExecutor(Executor executor) throws Exception { + // TODO: 1. Move this outside of ConnectivityServiceTest. + // 2. Make test to verify that Nat-T keepalive socket is created by IpSecService. + // 3. Mock ipsec service. + final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + final InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35"); + final InetAddress myIPv6 = InetAddress.getByName("2001:db8::1"); + final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + final InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888"); + + final int validKaInterval = 15; + final int invalidKaInterval = 9; + + final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(); + final int srcPort = testSocket.getPort(); + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + + Network notMyNet = new Network(61234); + Network myNet = connectKeepaliveNetwork(lp); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor); + + // Attempt to start keepalives with invalid parameters and check for errors. + // Invalid network. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + notMyNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + } + + // Invalid interval. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(invalidKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_INTERVAL); + } + + // Invalid destination. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + } + + // Invalid source; + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv6, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + } + + // NAT-T is only supported for IPv4. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv6, dstIPv6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + } + + // Basic check before testing started keepalive. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_UNSUPPORTED); + } + + // Check that a started keepalive can be stopped. + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + mWiFiNetworkAgent.setStopKeepaliveEvent(SocketKeepalive.SUCCESS); + ka.stop(); + callback.expectStopped(); + + // Check that keepalive could be restarted. + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + callback.expectStopped(); + + // Check that keepalive can be restarted without waiting for callback. + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + ka.start(validKaInterval); + callback.expectStopped(); + callback.expectStarted(); + ka.stop(); + callback.expectStopped(); + } + + // Check that deleting the IP address stops the keepalive. + LinkProperties bogusLp = new LinkProperties(lp); + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25)); + bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25)); + mWiFiNetworkAgent.sendLinkProperties(bogusLp); + callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + mWiFiNetworkAgent.sendLinkProperties(lp); + } + + // Check that a started keepalive is stopped correctly when the network disconnects. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + + // ... and that stopping it after that has no adverse effects. + waitForIdle(); + final Network myNetAlias = myNet; + assertNull(mCm.getNetworkCapabilities(myNetAlias)); + ka.stop(); + callback.assertNoCallback(); + } + + // Reconnect. + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + + // Check that a stop followed by network disconnects does not result in crash. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + // Delay the response of keepalive events in networkAgent long enough to make sure + // the follow-up network disconnection will be processed first. + mWiFiNetworkAgent.setKeepaliveResponseDelay(3 * TIMEOUT_MS); + ka.stop(); + + // Make sure the stop has been processed. Wait for executor idle is needed to prevent + // flaky since the actual stop call to the service is delegated to executor thread. + waitForIdleSerialExecutor(executor, TIMEOUT_MS); + waitForIdle(); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + callback.expectStopped(); + callback.assertNoCallback(); + } + + // Reconnect. + waitForIdle(); + myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + + // Check that keepalive slots start from 1 and increment. The first one gets slot 1. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(1); + int srcPort2 = 0; + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocket, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + + // The second one gets slot 2. + mWiFiNetworkAgent.setExpectedKeepaliveSlot(2); + final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket(); + srcPort2 = testSocket2.getPort(); + TestSocketKeepaliveCallback callback2 = new TestSocketKeepaliveCallback(executor); + try (SocketKeepalive ka2 = mCm.createSocketKeepalive( + myNet, testSocket2, myIPv4, dstIPv4, executor, callback2)) { + ka2.start(validKaInterval); + callback2.expectStarted(); + + ka.stop(); + callback.expectStopped(); + + ka2.stop(); + callback2.expectStopped(); + + testSocket.close(); + testSocket2.close(); + } + } + + // Check that there is no port leaked after all keepalives and sockets are closed. + // TODO: enable this check after ensuring a valid free port. See b/129512753#comment7. + // assertFalse(isUdpPortInUse(srcPort)); + // assertFalse(isUdpPortInUse(srcPort2)); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent = null; + } + + @Test + public void testTcpSocketKeepalives() throws Exception { + runTestWithSerialExecutors(executor -> doTestTcpSocketKeepalivesWithExecutor(executor)); + } + + private void doTestTcpSocketKeepalivesWithExecutor(Executor executor) throws Exception { + final int srcPortV4 = 12345; + final int srcPortV6 = 23456; + final InetAddress myIPv4 = InetAddress.getByName("127.0.0.1"); + final InetAddress myIPv6 = InetAddress.getByName("::1"); + + final int validKaInterval = 15; + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv6, 64)); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + lp.addRoute(new RouteInfo(InetAddress.getByName("127.0.0.254"))); + + final Network notMyNet = new Network(61234); + final Network myNet = connectKeepaliveNetwork(lp); + + final Socket testSocketV4 = new Socket(); + final Socket testSocketV6 = new Socket(); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor); + + // Attempt to start Tcp keepalives with invalid parameters and check for errors. + // Invalid network. + try (SocketKeepalive ka = mCm.createSocketKeepalive( + notMyNet, testSocketV4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK); + } + + // Invalid Socket (socket is not bound with IPv4 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + // Invalid Socket (socket is not bound with IPv6 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + // Bind the socket address + testSocketV4.bind(new InetSocketAddress(myIPv4, srcPortV4)); + testSocketV6.bind(new InetSocketAddress(myIPv6, srcPortV6)); + + // Invalid Socket (socket is bound with IPv4 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV4, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + // Invalid Socket (socket is bound with IPv6 address). + try (SocketKeepalive ka = mCm.createSocketKeepalive( + myNet, testSocketV6, executor, callback)) { + ka.start(validKaInterval); + callback.expectError(SocketKeepalive.ERROR_INVALID_SOCKET); + } + + testSocketV4.close(); + testSocketV6.close(); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent = null; + } + + private void doTestNattSocketKeepalivesFdWithExecutor(Executor executor) throws Exception { + final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129"); + final InetAddress anyIPv4 = InetAddress.getByName("0.0.0.0"); + final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8"); + final int validKaInterval = 15; + + // Prepare the target network. + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("wlan12"); + lp.addLinkAddress(new LinkAddress(myIPv4, 25)); + lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + Network myNet = connectKeepaliveNetwork(lp); + mWiFiNetworkAgent.setStartKeepaliveEvent(SocketKeepalive.SUCCESS); + mWiFiNetworkAgent.setStopKeepaliveEvent(SocketKeepalive.SUCCESS); + + TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback(executor); + + // Prepare the target file descriptor, keep only one instance. + final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(); + final int srcPort = testSocket.getPort(); + final ParcelFileDescriptor testPfd = + ParcelFileDescriptor.dup(testSocket.getFileDescriptor()); + testSocket.close(); + assertTrue(isUdpPortInUse(srcPort)); + + // Start keepalive and explicit make the variable goes out of scope with try-with-resources + // block. + try (SocketKeepalive ka = mCm.createNattKeepalive( + myNet, testPfd, myIPv4, dstIPv4, executor, callback)) { + ka.start(validKaInterval); + callback.expectStarted(); + ka.stop(); + callback.expectStopped(); + } + + // Check that the ParcelFileDescriptor is still valid after keepalive stopped, + // ErrnoException with EBADF will be thrown if the socket is closed when checking local + // address. + assertTrue(isUdpPortInUse(srcPort)); + final InetSocketAddress sa = + (InetSocketAddress) Os.getsockname(testPfd.getFileDescriptor()); + assertEquals(anyIPv4, sa.getAddress()); + + testPfd.close(); + // TODO: enable this check after ensuring a valid free port. See b/129512753#comment7. + // assertFalse(isUdpPortInUse(srcPort)); + + mWiFiNetworkAgent.disconnect(); + mWiFiNetworkAgent.expectDisconnected(); + mWiFiNetworkAgent = null; + } + + private static boolean isUdpPortInUse(int port) { + try (DatagramSocket ignored = new DatagramSocket(port)) { + return false; + } catch (IOException alreadyInUse) { + return true; + } + } + + @Test + public void testGetCaptivePortalServerUrl() throws Exception { + String url = mCm.getCaptivePortalServerUrl(); + assertEquals("http://connectivitycheck.gstatic.com/generate_204", url); + } + + private static class TestNetworkPinner extends NetworkPinner { + public static boolean awaitPin(int timeoutMs) throws InterruptedException { + synchronized(sLock) { + if (sNetwork == null) { + sLock.wait(timeoutMs); + } + return sNetwork != null; + } + } + + public static boolean awaitUnpin(int timeoutMs) throws InterruptedException { + synchronized(sLock) { + if (sNetwork != null) { + sLock.wait(timeoutMs); + } + return sNetwork == null; + } + } + } + + private void assertPinnedToWifiWithCellDefault() { + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess()); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + } + + private void assertPinnedToWifiWithWifiDefault() { + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess()); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + } + + private void assertNotPinnedToWifi() { + assertNull(mCm.getBoundNetworkForProcess()); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + } + + @Test + public void testNetworkPinner() throws Exception { + NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .build(); + assertNull(mCm.getBoundNetworkForProcess()); + + TestNetworkPinner.pin(mServiceContext, wifiRequest); + assertNull(mCm.getBoundNetworkForProcess()); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + + // When wi-fi connects, expect to be pinned. + assertTrue(TestNetworkPinner.awaitPin(100)); + assertPinnedToWifiWithCellDefault(); + + // Disconnect and expect the pin to drop. + mWiFiNetworkAgent.disconnect(); + assertTrue(TestNetworkPinner.awaitUnpin(100)); + assertNotPinnedToWifi(); + + // Reconnecting does not cause the pin to come back. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + assertFalse(TestNetworkPinner.awaitPin(100)); + assertNotPinnedToWifi(); + + // Pinning while connected causes the pin to take effect immediately. + TestNetworkPinner.pin(mServiceContext, wifiRequest); + assertTrue(TestNetworkPinner.awaitPin(100)); + assertPinnedToWifiWithCellDefault(); + + // Explicitly unpin and expect to use the default network again. + TestNetworkPinner.unpin(); + assertNotPinnedToWifi(); + + // Disconnect cell and wifi. + ExpectedBroadcast b = registerConnectivityBroadcast(3); // cell down, wifi up, wifi down. + mCellNetworkAgent.disconnect(); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + + // Pinning takes effect even if the pinned network is the default when the pin is set... + TestNetworkPinner.pin(mServiceContext, wifiRequest); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + assertTrue(TestNetworkPinner.awaitPin(100)); + assertPinnedToWifiWithWifiDefault(); + + // ... and is maintained even when that network is no longer the default. + b = registerConnectivityBroadcast(1); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mCellNetworkAgent.connect(true); + b.expectBroadcast(); + assertPinnedToWifiWithCellDefault(); + } + + @Test + public void testNetworkCallbackMaximum() throws Exception { + final int MAX_REQUESTS = 100; + final int CALLBACKS = 89; + final int INTENTS = 11; + final int SYSTEM_ONLY_MAX_REQUESTS = 250; + assertEquals(MAX_REQUESTS, CALLBACKS + INTENTS); + + NetworkRequest networkRequest = new NetworkRequest.Builder().build(); + ArrayList<Object> registered = new ArrayList<>(); + + int j = 0; + while (j++ < CALLBACKS / 2) { + NetworkCallback cb = new NetworkCallback(); + mCm.requestNetwork(networkRequest, cb); + registered.add(cb); + } + while (j++ < CALLBACKS) { + NetworkCallback cb = new NetworkCallback(); + mCm.registerNetworkCallback(networkRequest, cb); + registered.add(cb); + } + j = 0; + while (j++ < INTENTS / 2) { + final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("a" + j), FLAG_IMMUTABLE); + mCm.requestNetwork(networkRequest, pi); + registered.add(pi); + } + while (j++ < INTENTS) { + final PendingIntent pi = PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("b" + j), FLAG_IMMUTABLE); + mCm.registerNetworkCallback(networkRequest, pi); + registered.add(pi); + } + + // Test that the limit is enforced when MAX_REQUESTS simultaneous requests are added. + assertThrows(TooManyRequestsException.class, () -> + mCm.requestNetwork(networkRequest, new NetworkCallback()) + ); + assertThrows(TooManyRequestsException.class, () -> + mCm.registerNetworkCallback(networkRequest, new NetworkCallback()) + ); + assertThrows(TooManyRequestsException.class, () -> + mCm.requestNetwork(networkRequest, + PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("c"), FLAG_IMMUTABLE)) + ); + assertThrows(TooManyRequestsException.class, () -> + mCm.registerNetworkCallback(networkRequest, + PendingIntent.getBroadcast(mContext, 0 /* requestCode */, + new Intent("d"), FLAG_IMMUTABLE)) + ); + + // The system gets another SYSTEM_ONLY_MAX_REQUESTS slots. + final Handler handler = new Handler(ConnectivityThread.getInstanceLooper()); + withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> { + ArrayList<NetworkCallback> systemRegistered = new ArrayList<>(); + for (int i = 0; i < SYSTEM_ONLY_MAX_REQUESTS - 1; i++) { + NetworkCallback cb = new NetworkCallback(); + if (i % 2 == 0) { + mCm.registerDefaultNetworkCallbackForUid(1000000 + i, cb, handler); + } else { + mCm.registerNetworkCallback(networkRequest, cb); + } + systemRegistered.add(cb); + } + waitForIdle(); + + assertThrows(TooManyRequestsException.class, () -> + mCm.registerDefaultNetworkCallbackForUid(1001042, new NetworkCallback(), + handler)); + assertThrows(TooManyRequestsException.class, () -> + mCm.registerNetworkCallback(networkRequest, new NetworkCallback())); + + for (NetworkCallback callback : systemRegistered) { + mCm.unregisterNetworkCallback(callback); + } + waitForIdle(); // Wait for requests to be unregistered before giving up the permission. + }); + + for (Object o : registered) { + if (o instanceof NetworkCallback) { + mCm.unregisterNetworkCallback((NetworkCallback)o); + } + if (o instanceof PendingIntent) { + mCm.unregisterNetworkCallback((PendingIntent)o); + } + } + waitForIdle(); + + // Test that the limit is not hit when MAX_REQUESTS requests are added and removed. + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.requestNetwork(networkRequest, networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerDefaultNetworkCallback(networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerDefaultNetworkCallback(networkCallback); + mCm.unregisterNetworkCallback(networkCallback); + } + waitForIdle(); + + withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> { + for (int i = 0; i < MAX_REQUESTS; i++) { + NetworkCallback networkCallback = new NetworkCallback(); + mCm.registerDefaultNetworkCallbackForUid(1000000 + i, networkCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + mCm.unregisterNetworkCallback(networkCallback); + } + }); + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("e" + i), FLAG_IMMUTABLE); + mCm.requestNetwork(networkRequest, pendingIntent); + mCm.unregisterNetworkCallback(pendingIntent); + } + waitForIdle(); + + for (int i = 0; i < MAX_REQUESTS; i++) { + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("f" + i), FLAG_IMMUTABLE); + mCm.registerNetworkCallback(networkRequest, pendingIntent); + mCm.unregisterNetworkCallback(pendingIntent); + } + } + + @Test + public void testNetworkInfoOfTypeNone() throws Exception { + ExpectedBroadcast b = registerConnectivityBroadcast(1); + + verifyNoNetwork(); + TestNetworkAgentWrapper wifiAware = new TestNetworkAgentWrapper(TRANSPORT_WIFI_AWARE); + assertNull(mCm.getActiveNetworkInfo()); + + Network[] allNetworks = mCm.getAllNetworks(); + assertLength(1, allNetworks); + Network network = allNetworks[0]; + NetworkCapabilities capabilities = mCm.getNetworkCapabilities(network); + assertTrue(capabilities.hasTransport(TRANSPORT_WIFI_AWARE)); + + final NetworkRequest request = + new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI_AWARE).build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up wifi aware network. + wifiAware.connect(false, false, false /* isStrictMode */); + callback.expectAvailableCallbacksUnvalidated(wifiAware); + + assertNull(mCm.getActiveNetworkInfo()); + assertNull(mCm.getActiveNetwork()); + // TODO: getAllNetworkInfo is dirty and returns a non-empty array right from the start + // of this test. Fix it and uncomment the assert below. + //assertEmpty(mCm.getAllNetworkInfo()); + + // Disconnect wifi aware network. + wifiAware.disconnect(); + callback.expectCallbackThat(TIMEOUT_MS, (info) -> info instanceof CallbackEntry.Lost); + mCm.unregisterNetworkCallback(callback); + + verifyNoNetwork(); + b.expectNoBroadcast(10); + } + + @Test + public void testDeprecatedAndUnsupportedOperations() throws Exception { + final int TYPE_NONE = ConnectivityManager.TYPE_NONE; + assertNull(mCm.getNetworkInfo(TYPE_NONE)); + assertNull(mCm.getNetworkForType(TYPE_NONE)); + assertNull(mCm.getLinkProperties(TYPE_NONE)); + assertFalse(mCm.isNetworkSupported(TYPE_NONE)); + + assertThrows(IllegalArgumentException.class, + () -> mCm.networkCapabilitiesForType(TYPE_NONE)); + + Class<UnsupportedOperationException> unsupported = UnsupportedOperationException.class; + assertThrows(unsupported, () -> mCm.startUsingNetworkFeature(TYPE_WIFI, "")); + assertThrows(unsupported, () -> mCm.stopUsingNetworkFeature(TYPE_WIFI, "")); + // TODO: let test context have configuration application target sdk version + // and test that pre-M requesting for TYPE_NONE sends back APN_REQUEST_FAILED + assertThrows(unsupported, () -> mCm.startUsingNetworkFeature(TYPE_NONE, "")); + assertThrows(unsupported, () -> mCm.stopUsingNetworkFeature(TYPE_NONE, "")); + assertThrows(unsupported, () -> mCm.requestRouteToHostAddress(TYPE_NONE, null)); + } + + @Test + public void testLinkPropertiesEnsuresDirectlyConnectedRoutes() throws Exception { + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(WIFI_IFNAME); + LinkAddress myIpv4Address = new LinkAddress("192.168.12.3/24"); + RouteInfo myIpv4DefaultRoute = new RouteInfo((IpPrefix) null, + InetAddresses.parseNumericAddress("192.168.12.1"), lp.getInterfaceName()); + lp.addLinkAddress(myIpv4Address); + lp.addRoute(myIpv4DefaultRoute); + + // Verify direct routes are added when network agent is first registered in + // ConnectivityService. + TestNetworkAgentWrapper networkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp); + networkAgent.connect(true); + networkCallback.expectCallback(CallbackEntry.AVAILABLE, networkAgent); + networkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, networkAgent); + CallbackEntry.LinkPropertiesChanged cbi = + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + networkAgent); + networkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, networkAgent); + networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, networkAgent); + networkCallback.assertNoCallback(); + checkDirectlyConnectedRoutes(cbi.getLp(), Arrays.asList(myIpv4Address), + Arrays.asList(myIpv4DefaultRoute)); + checkDirectlyConnectedRoutes(mCm.getLinkProperties(networkAgent.getNetwork()), + Arrays.asList(myIpv4Address), Arrays.asList(myIpv4DefaultRoute)); + + // Verify direct routes are added during subsequent link properties updates. + LinkProperties newLp = new LinkProperties(lp); + LinkAddress myIpv6Address1 = new LinkAddress("fe80::cafe/64"); + LinkAddress myIpv6Address2 = new LinkAddress("2001:db8::2/64"); + newLp.addLinkAddress(myIpv6Address1); + newLp.addLinkAddress(myIpv6Address2); + networkAgent.sendLinkProperties(newLp); + cbi = networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, networkAgent); + networkCallback.assertNoCallback(); + checkDirectlyConnectedRoutes(cbi.getLp(), + Arrays.asList(myIpv4Address, myIpv6Address1, myIpv6Address2), + Arrays.asList(myIpv4DefaultRoute)); + mCm.unregisterNetworkCallback(networkCallback); + } + + private void expectNotifyNetworkStatus(List<Network> networks, String defaultIface, + Integer vpnUid, String vpnIfname, List<String> underlyingIfaces) throws Exception { + ArgumentCaptor<List<Network>> networksCaptor = ArgumentCaptor.forClass(List.class); + ArgumentCaptor<List<UnderlyingNetworkInfo>> vpnInfosCaptor = + ArgumentCaptor.forClass(List.class); + + verify(mStatsManager, atLeastOnce()).notifyNetworkStatus(networksCaptor.capture(), + any(List.class), eq(defaultIface), vpnInfosCaptor.capture()); + + assertSameElements(networksCaptor.getValue(), networks); + + List<UnderlyingNetworkInfo> infos = vpnInfosCaptor.getValue(); + if (vpnUid != null) { + assertEquals("Should have exactly one VPN:", 1, infos.size()); + UnderlyingNetworkInfo info = infos.get(0); + assertEquals("Unexpected VPN owner:", (int) vpnUid, info.getOwnerUid()); + assertEquals("Unexpected VPN interface:", vpnIfname, info.getInterface()); + assertSameElements(underlyingIfaces, info.getUnderlyingInterfaces()); + } else { + assertEquals(0, infos.size()); + return; + } + } + + private void expectNotifyNetworkStatus( + List<Network> networks, String defaultIface) throws Exception { + expectNotifyNetworkStatus(networks, defaultIface, null, null, List.of()); + } + + @Test + public void testStatsIfacesChanged() throws Exception { + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + final List<Network> onlyCell = List.of(mCellNetworkAgent.getNetwork()); + final List<Network> onlyWifi = List.of(mWiFiNetworkAgent.getNetwork()); + + LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName(WIFI_IFNAME); + + // Simple connection should have updated ifaces + mCellNetworkAgent.connect(false); + mCellNetworkAgent.sendLinkProperties(cellLp); + waitForIdle(); + expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Default network switch should update ifaces. + mWiFiNetworkAgent.connect(false); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + waitForIdle(); + assertEquals(wifiLp, mService.getActiveLinkProperties()); + expectNotifyNetworkStatus(onlyWifi, WIFI_IFNAME); + reset(mStatsManager); + + // Disconnect should update ifaces. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Metered change should update ifaces + mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + waitForIdle(); + expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + mCellNetworkAgent.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + waitForIdle(); + expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Temp metered change shouldn't update ifaces + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + waitForIdle(); + verify(mStatsManager, never()).notifyNetworkStatus(eq(onlyCell), + any(List.class), eq(MOBILE_IFNAME), any(List.class)); + reset(mStatsManager); + + // Roaming change should update ifaces + mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING); + waitForIdle(); + expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME); + reset(mStatsManager); + + // Test VPNs. + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(VPN_IFNAME); + + mMockVpn.establishForMyUid(lp); + assertUidRangesUpdatedForMyUid(true); + + final List<Network> cellAndVpn = + List.of(mCellNetworkAgent.getNetwork(), mMockVpn.getNetwork()); + + // A VPN with default (null) underlying networks sets the underlying network's interfaces... + expectNotifyNetworkStatus(cellAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(MOBILE_IFNAME)); + + // ...and updates them as the default network switches. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + final Network[] onlyNull = new Network[]{null}; + final List<Network> wifiAndVpn = + List.of(mWiFiNetworkAgent.getNetwork(), mMockVpn.getNetwork()); + final List<Network> cellAndWifi = + List.of(mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork()); + final Network[] cellNullAndWifi = + new Network[]{mCellNetworkAgent.getNetwork(), null, mWiFiNetworkAgent.getNetwork()}; + + waitForIdle(); + assertEquals(wifiLp, mService.getActiveLinkProperties()); + expectNotifyNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(WIFI_IFNAME)); + reset(mStatsManager); + + // A VPN that sets its underlying networks passes the underlying interfaces, and influences + // the default interface sent to NetworkStatsService by virtue of applying to the system + // server UID (or, in this test, to the test's UID). This is the reason for sending + // MOBILE_IFNAME even though the default network is wifi. + // TODO: fix this to pass in the actual default network interface. Whether or not the VPN + // applies to the system server UID should not have any bearing on network stats. + mMockVpn.setUnderlyingNetworks(onlyCell.toArray(new Network[0])); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(MOBILE_IFNAME)); + reset(mStatsManager); + + mMockVpn.setUnderlyingNetworks(cellAndWifi.toArray(new Network[0])); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(MOBILE_IFNAME, WIFI_IFNAME)); + reset(mStatsManager); + + // Null underlying networks are ignored. + mMockVpn.setUnderlyingNetworks(cellNullAndWifi); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(MOBILE_IFNAME, WIFI_IFNAME)); + reset(mStatsManager); + + // If an underlying network disconnects, that interface should no longer be underlying. + // This doesn't actually work because disconnectAndDestroyNetwork only notifies + // NetworkStatsService before the underlying network is actually removed. So the underlying + // network will only be removed if notifyIfacesChangedForNetworkStats is called again. This + // could result in incorrect data usage measurements if the interface used by the + // disconnected network is reused by a system component that does not register an agent for + // it (e.g., tethering). + mCellNetworkAgent.disconnect(); + waitForIdle(); + assertNull(mService.getLinkProperties(mCellNetworkAgent.getNetwork())); + expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(MOBILE_IFNAME, WIFI_IFNAME)); + + // Confirm that we never tell NetworkStatsService that cell is no longer the underlying + // network for the VPN... + verify(mStatsManager, never()).notifyNetworkStatus(any(List.class), + any(List.class), any() /* anyString() doesn't match null */, + argThat(infos -> infos.get(0).getUnderlyingInterfaces().size() == 1 + && WIFI_IFNAME.equals(infos.get(0).getUnderlyingInterfaces().get(0)))); + verifyNoMoreInteractions(mStatsManager); + reset(mStatsManager); + + // ... but if something else happens that causes notifyIfacesChangedForNetworkStats to be + // called again, it does. For example, connect Ethernet, but with a low score, such that it + // does not become the default network. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.setScore( + new NetworkScore.Builder().setLegacyInt(30).setExiting(true).build()); + mEthernetNetworkAgent.connect(false); + waitForIdle(); + verify(mStatsManager).notifyNetworkStatus(any(List.class), + any(List.class), any() /* anyString() doesn't match null */, + argThat(vpnInfos -> vpnInfos.get(0).getUnderlyingInterfaces().size() == 1 + && WIFI_IFNAME.equals(vpnInfos.get(0).getUnderlyingInterfaces().get(0)))); + mEthernetNetworkAgent.disconnect(); + waitForIdle(); + reset(mStatsManager); + + // When a VPN declares no underlying networks (i.e., no connectivity), getAllVpnInfo + // does not return the VPN, so CS does not pass it to NetworkStatsService. This causes + // NetworkStatsFactory#adjustForTunAnd464Xlat not to attempt any VPN data migration, which + // is probably a performance improvement (though it's very unlikely that a VPN would declare + // no underlying networks). + // Also, for the same reason as above, the active interface passed in is null. + mMockVpn.setUnderlyingNetworks(new Network[0]); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, null); + reset(mStatsManager); + + // Specifying only a null underlying network is the same as no networks. + mMockVpn.setUnderlyingNetworks(onlyNull); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, null); + reset(mStatsManager); + + // Specifying networks that are all disconnected is the same as specifying no networks. + mMockVpn.setUnderlyingNetworks(onlyCell.toArray(new Network[0])); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, null); + reset(mStatsManager); + + // Passing in null again means follow the default network again. + mMockVpn.setUnderlyingNetworks(null); + waitForIdle(); + expectNotifyNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME, + List.of(WIFI_IFNAME)); + reset(mStatsManager); + } + + @Test + public void testBasicDnsConfigurationPushed() throws Exception { + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + + // Clear any interactions that occur as a result of CS starting up. + reset(mMockDnsResolver); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + waitForIdle(); + verify(mMockDnsResolver, never()).setResolverConfiguration(any()); + verifyNoMoreInteractions(mMockDnsResolver); + + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does + // "is-reachable" testing in order to not program netd with unreachable + // nameservers that it might try repeated to validate. + cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), + MOBILE_IFNAME)); + cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), + MOBILE_IFNAME)); + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(false); + waitForIdle(); + + verify(mMockDnsResolver, times(1)).createNetworkCache( + eq(mCellNetworkAgent.getNetwork().netId)); + // CS tells dnsresolver about the empty DNS config for this network. + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any()); + reset(mMockDnsResolver); + + cellLp.addDnsServer(InetAddress.getByName("2001:db8::1")); + mCellNetworkAgent.sendLinkProperties(cellLp); + waitForIdle(); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(1, resolvrParams.servers.length); + assertTrue(ArrayUtils.contains(resolvrParams.servers, "2001:db8::1")); + // Opportunistic mode. + assertTrue(ArrayUtils.contains(resolvrParams.tlsServers, "2001:db8::1")); + reset(mMockDnsResolver); + + cellLp.addDnsServer(InetAddress.getByName("192.0.2.1")); + mCellNetworkAgent.sendLinkProperties(cellLp); + waitForIdle(); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[]{"2001:db8::1", "192.0.2.1"})); + // Opportunistic mode. + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[]{"2001:db8::1", "192.0.2.1"})); + reset(mMockDnsResolver); + + final String TLS_SPECIFIER = "tls.example.com"; + final String TLS_SERVER6 = "2001:db8:53::53"; + final InetAddress[] TLS_IPS = new InetAddress[]{ InetAddress.getByName(TLS_SERVER6) }; + final String[] TLS_SERVERS = new String[]{ TLS_SERVER6 }; + mCellNetworkAgent.mNmCallbacks.notifyPrivateDnsConfigResolved( + new PrivateDnsConfig(TLS_SPECIFIER, TLS_IPS).toParcel()); + + waitForIdle(); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[]{"2001:db8::1", "192.0.2.1"})); + reset(mMockDnsResolver); + } + + @Test + public void testDnsConfigurationTransTypesPushed() throws Exception { + // Clear any interactions that occur as a result of CS starting up. + reset(mMockDnsResolver); + + final NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + verify(mMockDnsResolver, times(1)).createNetworkCache( + eq(mWiFiNetworkAgent.getNetwork().netId)); + verify(mMockDnsResolver, times(2)).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + final ResolverParamsParcel resolverParams = mResolverParamsParcelCaptor.getValue(); + assertContainsExactly(resolverParams.transportTypes, TRANSPORT_WIFI); + reset(mMockDnsResolver); + } + + @Test + public void testPrivateDnsNotification() throws Exception { + NetworkRequest request = new NetworkRequest.Builder() + .clearCapabilities().addCapability(NET_CAPABILITY_INTERNET) + .build(); + TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + // Bring up wifi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + // Private DNS resolution failed, checking if the notification will be shown or not. + mWiFiNetworkAgent.setNetworkInvalid(true /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + // If network validation failed, NetworkMonitor will re-evaluate the network. + // ConnectivityService should filter the redundant notification. This part is trying to + // simulate that situation and check if ConnectivityService could filter that case. + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + verify(mNotificationManager, timeout(TIMEOUT_MS).times(1)).notify(anyString(), + eq(NotificationType.PRIVATE_DNS_BROKEN.eventId), any()); + // If private DNS resolution successful, the PRIVATE_DNS_BROKEN notification shouldn't be + // shown. + mWiFiNetworkAgent.setNetworkValid(true /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + verify(mNotificationManager, timeout(TIMEOUT_MS).times(1)).cancel(anyString(), + eq(NotificationType.PRIVATE_DNS_BROKEN.eventId)); + // If private DNS resolution failed again, the PRIVATE_DNS_BROKEN notification should be + // shown again. + mWiFiNetworkAgent.setNetworkInvalid(true /* isStrictMode */); + mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + waitForIdle(); + verify(mNotificationManager, timeout(TIMEOUT_MS).times(2)).notify(anyString(), + eq(NotificationType.PRIVATE_DNS_BROKEN.eventId), any()); + } + + @Test + public void testPrivateDnsSettingsChange() throws Exception { + // Clear any interactions that occur as a result of CS starting up. + reset(mMockDnsResolver); + + // The default on Android is opportunistic mode ("Automatic"). + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + waitForIdle(); + // CS tells netd about the empty DNS config for this network. + verify(mMockDnsResolver, never()).setResolverConfiguration(any()); + verifyNoMoreInteractions(mMockDnsResolver); + + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does + // "is-reachable" testing in order to not program netd with unreachable + // nameservers that it might try repeated to validate. + cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), + MOBILE_IFNAME)); + cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), + MOBILE_IFNAME)); + cellLp.addDnsServer(InetAddress.getByName("2001:db8::1")); + cellLp.addDnsServer(InetAddress.getByName("192.0.2.1")); + + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(false); + waitForIdle(); + verify(mMockDnsResolver, times(1)).createNetworkCache( + eq(mCellNetworkAgent.getNetwork().netId)); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[] { "2001:db8::1", "192.0.2.1" })); + // Opportunistic mode. + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[] { "2001:db8::1", "192.0.2.1" })); + reset(mMockDnsResolver); + cellNetworkCallback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, + mCellNetworkAgent); + CallbackEntry.LinkPropertiesChanged cbi = cellNetworkCallback.expectCallback( + CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + + setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com"); + verify(mMockDnsResolver, times(1)).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[] { "2001:db8::1", "192.0.2.1" })); + reset(mMockDnsResolver); + cellNetworkCallback.assertNoCallback(); + + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(2, resolvrParams.servers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.servers, + new String[] { "2001:db8::1", "192.0.2.1" })); + assertEquals(2, resolvrParams.tlsServers.length); + assertTrue(ArrayUtils.containsAll(resolvrParams.tlsServers, + new String[] { "2001:db8::1", "192.0.2.1" })); + reset(mMockDnsResolver); + cellNetworkCallback.assertNoCallback(); + + setPrivateDnsSettings(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, "strict.example.com"); + // Can't test dns configuration for strict mode without properly mocking + // out the DNS lookups, but can test that LinkProperties is updated. + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertTrue(cbi.getLp().isPrivateDnsActive()); + assertEquals("strict.example.com", cbi.getLp().getPrivateDnsServerName()); + } + + private PrivateDnsValidationEventParcel makePrivateDnsValidationEvent( + final int netId, final String ipAddress, final String hostname, final int validation) { + final PrivateDnsValidationEventParcel event = new PrivateDnsValidationEventParcel(); + event.netId = netId; + event.ipAddress = ipAddress; + event.hostname = hostname; + event.validation = validation; + return event; + } + + @Test + public void testLinkPropertiesWithPrivateDnsValidationEvents() throws Exception { + // The default on Android is opportunistic mode ("Automatic"). + setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com"); + + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.requestNetwork(cellRequest, cellNetworkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + waitForIdle(); + LinkProperties lp = new LinkProperties(); + mCellNetworkAgent.sendLinkProperties(lp); + mCellNetworkAgent.connect(false); + waitForIdle(); + cellNetworkCallback.expectCallback(CallbackEntry.AVAILABLE, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, + mCellNetworkAgent); + CallbackEntry.LinkPropertiesChanged cbi = cellNetworkCallback.expectCallback( + CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + cellNetworkCallback.expectCallback(CallbackEntry.BLOCKED_STATUS, mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + Set<InetAddress> dnsServers = new HashSet<>(); + checkDnsServers(cbi.getLp(), dnsServers); + + // Send a validation event for a server that is not part of the current + // resolver config. The validation event should be ignored. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, "", + "145.100.185.18", VALIDATION_RESULT_SUCCESS)); + cellNetworkCallback.assertNoCallback(); + + // Add a dns server to the LinkProperties. + LinkProperties lp2 = new LinkProperties(lp); + lp2.addDnsServer(InetAddress.getByName("145.100.185.16")); + mCellNetworkAgent.sendLinkProperties(lp2); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + dnsServers.add(InetAddress.getByName("145.100.185.16")); + checkDnsServers(cbi.getLp(), dnsServers); + + // Send a validation event containing a hostname that is not part of + // the current resolver config. The validation event should be ignored. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, + "145.100.185.16", "hostname", VALIDATION_RESULT_SUCCESS)); + cellNetworkCallback.assertNoCallback(); + + // Send a validation event where validation failed. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, + "145.100.185.16", "", VALIDATION_RESULT_FAILURE)); + cellNetworkCallback.assertNoCallback(); + + // Send a validation event where validation succeeded for a server in + // the current resolver config. A LinkProperties callback with updated + // private dns fields should be sent. + mService.mResolverUnsolEventCallback.onPrivateDnsValidationEvent( + makePrivateDnsValidationEvent(mCellNetworkAgent.getNetwork().netId, + "145.100.185.16", "", VALIDATION_RESULT_SUCCESS)); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertTrue(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + checkDnsServers(cbi.getLp(), dnsServers); + + // The private dns fields in LinkProperties should be preserved when + // the network agent sends unrelated changes. + LinkProperties lp3 = new LinkProperties(lp2); + lp3.setMtu(1300); + mCellNetworkAgent.sendLinkProperties(lp3); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertTrue(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + checkDnsServers(cbi.getLp(), dnsServers); + assertEquals(1300, cbi.getLp().getMtu()); + + // Removing the only validated server should affect the private dns + // fields in LinkProperties. + LinkProperties lp4 = new LinkProperties(lp3); + lp4.removeDnsServer(InetAddress.getByName("145.100.185.16")); + mCellNetworkAgent.sendLinkProperties(lp4); + cbi = cellNetworkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + cellNetworkCallback.assertNoCallback(); + assertFalse(cbi.getLp().isPrivateDnsActive()); + assertNull(cbi.getLp().getPrivateDnsServerName()); + dnsServers.remove(InetAddress.getByName("145.100.185.16")); + checkDnsServers(cbi.getLp(), dnsServers); + assertEquals(1300, cbi.getLp().getMtu()); + } + + private void checkDirectlyConnectedRoutes(Object callbackObj, + Collection<LinkAddress> linkAddresses, Collection<RouteInfo> otherRoutes) { + assertTrue(callbackObj instanceof LinkProperties); + LinkProperties lp = (LinkProperties) callbackObj; + + Set<RouteInfo> expectedRoutes = new ArraySet<>(); + expectedRoutes.addAll(otherRoutes); + for (LinkAddress address : linkAddresses) { + RouteInfo localRoute = new RouteInfo(address, null, lp.getInterfaceName()); + // Duplicates in linkAddresses are considered failures + assertTrue(expectedRoutes.add(localRoute)); + } + List<RouteInfo> observedRoutes = lp.getRoutes(); + assertEquals(expectedRoutes.size(), observedRoutes.size()); + assertTrue(observedRoutes.containsAll(expectedRoutes)); + } + + private static void checkDnsServers(Object callbackObj, Set<InetAddress> dnsServers) { + assertTrue(callbackObj instanceof LinkProperties); + LinkProperties lp = (LinkProperties) callbackObj; + assertEquals(dnsServers.size(), lp.getDnsServers().size()); + assertTrue(lp.getDnsServers().containsAll(dnsServers)); + } + + @Test + public void testApplyUnderlyingCapabilities() throws Exception { + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mCellNetworkAgent.connect(false /* validated */); + mWiFiNetworkAgent.connect(false /* validated */); + + final NetworkCapabilities cellNc = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .setLinkDownstreamBandwidthKbps(10); + final NetworkCapabilities wifiNc = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_METERED) + .addCapability(NET_CAPABILITY_NOT_ROAMING) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .setLinkUpstreamBandwidthKbps(20); + mCellNetworkAgent.setNetworkCapabilities(cellNc, true /* sendToConnectivityService */); + mWiFiNetworkAgent.setNetworkCapabilities(wifiNc, true /* sendToConnectivityService */); + waitForIdle(); + + final Network mobile = mCellNetworkAgent.getNetwork(); + final Network wifi = mWiFiNetworkAgent.getNetwork(); + + final NetworkCapabilities initialCaps = new NetworkCapabilities(); + initialCaps.addTransportType(TRANSPORT_VPN); + initialCaps.addCapability(NET_CAPABILITY_INTERNET); + initialCaps.removeCapability(NET_CAPABILITY_NOT_VPN); + + final NetworkCapabilities withNoUnderlying = new NetworkCapabilities(); + withNoUnderlying.addCapability(NET_CAPABILITY_INTERNET); + withNoUnderlying.addCapability(NET_CAPABILITY_NOT_CONGESTED); + withNoUnderlying.addCapability(NET_CAPABILITY_NOT_ROAMING); + withNoUnderlying.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + withNoUnderlying.addTransportType(TRANSPORT_VPN); + withNoUnderlying.removeCapability(NET_CAPABILITY_NOT_VPN); + + final NetworkCapabilities withMobileUnderlying = new NetworkCapabilities(withNoUnderlying); + withMobileUnderlying.addTransportType(TRANSPORT_CELLULAR); + withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING); + withMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + withMobileUnderlying.setLinkDownstreamBandwidthKbps(10); + + final NetworkCapabilities withWifiUnderlying = new NetworkCapabilities(withNoUnderlying); + withWifiUnderlying.addTransportType(TRANSPORT_WIFI); + withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED); + withWifiUnderlying.setLinkUpstreamBandwidthKbps(20); + + final NetworkCapabilities withWifiAndMobileUnderlying = + new NetworkCapabilities(withNoUnderlying); + withWifiAndMobileUnderlying.addTransportType(TRANSPORT_CELLULAR); + withWifiAndMobileUnderlying.addTransportType(TRANSPORT_WIFI); + withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED); + withWifiAndMobileUnderlying.removeCapability(NET_CAPABILITY_NOT_ROAMING); + withWifiAndMobileUnderlying.setLinkDownstreamBandwidthKbps(10); + withWifiAndMobileUnderlying.setLinkUpstreamBandwidthKbps(20); + + final NetworkCapabilities initialCapsNotMetered = new NetworkCapabilities(initialCaps); + initialCapsNotMetered.addCapability(NET_CAPABILITY_NOT_METERED); + + NetworkCapabilities caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{}, initialCapsNotMetered, caps); + assertEquals(withNoUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{null}, initialCapsNotMetered, caps); + assertEquals(withNoUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{mobile}, initialCapsNotMetered, caps); + assertEquals(withMobileUnderlying, caps); + + mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCapsNotMetered, caps); + assertEquals(withWifiUnderlying, caps); + + withWifiUnderlying.removeCapability(NET_CAPABILITY_NOT_METERED); + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{wifi}, initialCaps, caps); + assertEquals(withWifiUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{mobile, wifi}, initialCaps, caps); + assertEquals(withWifiAndMobileUnderlying, caps); + + withWifiUnderlying.addCapability(NET_CAPABILITY_NOT_METERED); + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi}, + initialCapsNotMetered, caps); + assertEquals(withWifiAndMobileUnderlying, caps); + + caps = new NetworkCapabilities(initialCaps); + mService.applyUnderlyingCapabilities(new Network[]{null, mobile, null, wifi}, + initialCapsNotMetered, caps); + assertEquals(withWifiAndMobileUnderlying, caps); + + mService.applyUnderlyingCapabilities(null, initialCapsNotMetered, caps); + assertEquals(withWifiUnderlying, caps); + } + + @Test + public void testVpnConnectDisconnectUnderlyingNetwork() throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN).build(); + + mCm.registerNetworkCallback(request, callback); + + // Bring up a VPN that specifies an underlying network that does not exist yet. + // Note: it's sort of meaningless for a VPN app to declare a network that doesn't exist yet, + // (and doing so is difficult without using reflection) but it's good to test that the code + // behaves approximately correctly. + mMockVpn.establishForMyUid(false, true, false); + assertUidRangesUpdatedForMyUid(true); + final Network wifiNetwork = new Network(mNetIdManager.peekNextNetId()); + mMockVpn.setUnderlyingNetworks(new Network[]{wifiNetwork}); + callback.expectAvailableCallbacksUnvalidated(mMockVpn); + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_VPN)); + assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_WIFI)); + + // Make that underlying network connect, and expect to see its capabilities immediately + // reflected in the VPN's capabilities. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + assertEquals(wifiNetwork, mWiFiNetworkAgent.getNetwork()); + mWiFiNetworkAgent.connect(false); + // TODO: the callback for the VPN happens before any callbacks are called for the wifi + // network that has just connected. There appear to be two issues here: + // 1. The VPN code will accept an underlying network as soon as getNetworkCapabilities() for + // it returns non-null (which happens very early, during handleRegisterNetworkAgent). + // This is not correct because that that point the network is not connected and cannot + // pass any traffic. + // 2. When a network connects, updateNetworkInfo propagates underlying network capabilities + // before rematching networks. + // Given that this scenario can't really happen, this is probably fine for now. + callback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_VPN)); + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasTransport(TRANSPORT_WIFI)); + + // Disconnect the network, and expect to see the VPN capabilities change accordingly. + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + callback.expectCapabilitiesThat(mMockVpn, (nc) -> + nc.getTransportTypes().length == 1 && nc.hasTransport(TRANSPORT_VPN)); + + mMockVpn.disconnect(); + mCm.unregisterNetworkCallback(callback); + } + + private void assertGetNetworkInfoOfGetActiveNetworkIsConnected(boolean expectedConnectivity) { + // What Chromium used to do before https://chromium-review.googlesource.com/2605304 + assertEquals("Unexpected result for getActiveNetworkInfo(getActiveNetwork())", + expectedConnectivity, mCm.getNetworkInfo(mCm.getActiveNetwork()).isConnected()); + } + + @Test + public void testVpnUnderlyingNetworkSuspended() throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + + // Connect a VPN. + mMockVpn.establishForMyUid(false /* validated */, true /* hasInternet */, + false /* isStrictMode */); + callback.expectAvailableCallbacksUnvalidated(mMockVpn); + + // Connect cellular data. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false /* validated */); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + + // Suspend the cellular network and expect the VPN to be suspended. + mCellNetworkAgent.suspend(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> !nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + callback.assertNoCallback(); + + assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.SUSPENDED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + // VPN's main underlying network is suspended, so no connectivity. + assertGetNetworkInfoOfGetActiveNetworkIsConnected(false); + + // Switch to another network. The VPN should no longer be suspended. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false /* validated */); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_WIFI)); + callback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + + // Unsuspend cellular and then switch back to it. The VPN remains not suspended. + mCellNetworkAgent.resume(); + callback.assertNoCallback(); + mWiFiNetworkAgent.disconnect(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + // Spurious double callback? + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + + // Suspend cellular and expect no connectivity. + mCellNetworkAgent.suspend(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> !nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + callback.assertNoCallback(); + + assertFalse(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.SUSPENDED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.SUSPENDED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(false); + + // Resume cellular and expect that connectivity comes back. + mCellNetworkAgent.resume(); + callback.expectCapabilitiesThat(mMockVpn, + nc -> nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) + && nc.hasTransport(TRANSPORT_CELLULAR)); + callback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + callback.assertNoCallback(); + + assertTrue(mCm.getNetworkCapabilities(mMockVpn.getNetwork()) + .hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertGetNetworkInfoOfGetActiveNetworkIsConnected(true); + } + + @Test + public void testVpnNetworkActive() throws Exception { + // NETWORK_SETTINGS is necessary to call registerSystemDefaultNetworkCallback. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final int uid = Process.myUid(); + + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback genericNotVpnNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + final NetworkRequest genericNotVpnRequest = new NetworkRequest.Builder().build(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN).build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(genericNotVpnRequest, genericNotVpnNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + mCm.registerDefaultNetworkCallback(defaultCallback); + mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + defaultCallback.assertNoCallback(); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false); + + genericNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + genericNotVpnNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + vpnNetworkCallback.assertNoCallback(); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + final Set<UidRange> ranges = uidRangesForUids(uid); + mMockVpn.registerAgent(ranges); + mMockVpn.setUnderlyingNetworks(new Network[0]); + + // VPN networks do not satisfy the default request and are automatically validated + // by NetworkMonitor + assertFalse(NetworkMonitorUtils.isValidationRequired( + mMockVpn.getAgent().getNetworkCapabilities())); + mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */); + + mMockVpn.connect(false); + + genericNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + assertEquals(mWiFiNetworkAgent.getNetwork(), + systemDefaultCallback.getLastAvailableNetwork()); + + ranges.clear(); + mMockVpn.setUids(ranges); + + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + + // TODO : The default network callback should actually get a LOST call here (also see the + // comment below for AVAILABLE). This is because ConnectivityService does not look at UID + // ranges at all when determining whether a network should be rematched. In practice, VPNs + // can't currently update their UIDs without disconnecting, so this does not matter too + // much, but that is the reason the test here has to check for an update to the + // capabilities instead of the expected LOST then AVAILABLE. + defaultCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn); + systemDefaultCallback.assertNoCallback(); + + ranges.add(new UidRange(uid, uid)); + mMockVpn.setUids(ranges); + + genericNetworkCallback.expectAvailableCallbacksValidated(mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectAvailableCallbacksValidated(mMockVpn); + // TODO : Here like above, AVAILABLE would be correct, but because this can't actually + // happen outside of the test, ConnectivityService does not rematch callbacks. + defaultCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED, mMockVpn); + systemDefaultCallback.assertNoCallback(); + + mWiFiNetworkAgent.disconnect(); + + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + genericNotVpnNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + wifiNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + vpnNetworkCallback.assertNoCallback(); + defaultCallback.assertNoCallback(); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + + mMockVpn.disconnect(); + + genericNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + genericNotVpnNetworkCallback.assertNoCallback(); + wifiNetworkCallback.assertNoCallback(); + vpnNetworkCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + systemDefaultCallback.assertNoCallback(); + assertEquals(null, mCm.getActiveNetwork()); + + mCm.unregisterNetworkCallback(genericNetworkCallback); + mCm.unregisterNetworkCallback(wifiNetworkCallback); + mCm.unregisterNetworkCallback(vpnNetworkCallback); + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(systemDefaultCallback); + } + + @Test + public void testVpnWithoutInternet() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + + defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + defaultCallback.assertNoCallback(); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.disconnect(); + defaultCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(defaultCallback); + } + + @Test + public void testVpnWithInternet() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + + defaultCallback.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.establishForMyUid(true /* validated */, true /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + assertEquals(defaultCallback.getLastAvailableNetwork(), mCm.getActiveNetwork()); + + mMockVpn.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + + mCm.unregisterNetworkCallback(defaultCallback); + } + + @Test + public void testVpnUnvalidated() throws Exception { + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + + // Bring up Ethernet. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + callback.assertNoCallback(); + + // Bring up a VPN that has the INTERNET capability, initially unvalidated. + mMockVpn.establishForMyUid(false /* validated */, true /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + // Even though the VPN is unvalidated, it becomes the default network for our app. + callback.expectAvailableCallbacksUnvalidated(mMockVpn); + callback.assertNoCallback(); + + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED)); + assertTrue(nc.hasCapability(NET_CAPABILITY_INTERNET)); + + assertFalse(NetworkMonitorUtils.isValidationRequired( + mMockVpn.getAgent().getNetworkCapabilities())); + assertTrue(NetworkMonitorUtils.isPrivateDnsValidationRequired( + mMockVpn.getAgent().getNetworkCapabilities())); + + // Pretend that the VPN network validates. + mMockVpn.getAgent().setNetworkValid(false /* isStrictMode */); + mMockVpn.getAgent().mNetworkMonitor.forceReevaluation(Process.myUid()); + // Expect to see the validated capability, but no other changes, because the VPN is already + // the default network for the app. + callback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mMockVpn); + callback.assertNoCallback(); + + mMockVpn.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mMockVpn); + callback.expectAvailableCallbacksValidated(mEthernetNetworkAgent); + } + + @Test + public void testVpnStartsWithUnderlyingCaps() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN) + .build(); + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + vpnNetworkCallback.assertNoCallback(); + + // Connect cell. It will become the default network, and in the absence of setting + // underlying networks explicitly it will become the sole underlying network for the vpn. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mCellNetworkAgent.connect(true); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + vpnNetworkCallback.expectAvailableCallbacks(mMockVpn.getNetwork(), + false /* suspended */, false /* validated */, false /* blocked */, TIMEOUT_MS); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn.getNetwork(), TIMEOUT_MS, + nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED)); + + final NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + assertTrue(nc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + assertTrue(nc.hasCapability(NET_CAPABILITY_VALIDATED)); + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + } + + private void assertDefaultNetworkCapabilities(int userId, NetworkAgentWrapper... networks) { + final NetworkCapabilities[] defaultCaps = mService.getDefaultNetworkCapabilitiesForUser( + userId, "com.android.calling.package", "com.test"); + final String defaultCapsString = Arrays.toString(defaultCaps); + assertEquals(defaultCapsString, defaultCaps.length, networks.length); + final Set<NetworkCapabilities> defaultCapsSet = new ArraySet<>(defaultCaps); + for (NetworkAgentWrapper network : networks) { + final NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork()); + final String msg = "Did not find " + nc + " in " + Arrays.toString(defaultCaps); + assertTrue(msg, defaultCapsSet.contains(nc)); + } + } + + @Test + public void testVpnSetUnderlyingNetworks() throws Exception { + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN) + .build(); + NetworkCapabilities nc; + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + vpnNetworkCallback.assertNoCallback(); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + assertFalse(nc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + // For safety reasons a VPN without underlying networks is considered metered. + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED)); + // A VPN without underlying networks is not suspended. + assertTrue(nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + + final int userId = UserHandle.getUserId(Process.myUid()); + assertDefaultNetworkCapabilities(userId /* no networks */); + + // Connect cell and use it as an underlying network. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mCellNetworkAgent.connect(true); + + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + mWiFiNetworkAgent.connect(true); + + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Don't disconnect, but note the VPN is not using wifi any more. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + // The return value of getDefaultNetworkCapabilitiesForUser always includes the default + // network (wifi) as well as the underlying networks (cell). + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Remove NOT_SUSPENDED from the only network and observe VPN is now suspended. + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && !caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + + // Add NOT_SUSPENDED again and observe VPN is no longer suspended. + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_SUSPENDED); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + + // Use Wifi but not cell. Note the VPN is now unmetered and not suspended. + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mWiFiNetworkAgent); + + // Use both again. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Cell is suspended again. As WiFi is not, this should not cause a callback. + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_SUSPENDED); + vpnNetworkCallback.assertNoCallback(); + + // Stop using WiFi. The VPN is suspended again. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && !caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.SUSPENDED, mMockVpn); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Use both again. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED) + && caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)); + vpnNetworkCallback.expectCallback(CallbackEntry.RESUMED, mMockVpn); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent, mWiFiNetworkAgent); + + // Disconnect cell. Receive update without even removing the dead network from the + // underlying networks – it's dead anyway. Not metered any more. + mCellNetworkAgent.disconnect(); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertDefaultNetworkCapabilities(userId, mWiFiNetworkAgent); + + // Disconnect wifi too. No underlying networks means this is now metered. + mWiFiNetworkAgent.disconnect(); + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + // When a network disconnects, the callbacks are fired before all state is updated, so for a + // short time, synchronous calls will behave as if the network is still connected. Wait for + // things to settle. + waitForIdle(); + assertDefaultNetworkCapabilities(userId /* no networks */); + + mMockVpn.disconnect(); + } + + @Test + public void testNullUnderlyingNetworks() throws Exception { + final int uid = Process.myUid(); + + final TestNetworkCallback vpnNetworkCallback = new TestNetworkCallback(); + final NetworkRequest vpnNetworkRequest = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .addTransportType(TRANSPORT_VPN) + .build(); + NetworkCapabilities nc; + mCm.registerNetworkCallback(vpnNetworkRequest, vpnNetworkCallback); + vpnNetworkCallback.assertNoCallback(); + + mMockVpn.establishForMyUid(true /* validated */, false /* hasInternet */, + false /* isStrictMode */); + assertUidRangesUpdatedForMyUid(true); + + vpnNetworkCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(nc.hasTransport(TRANSPORT_VPN)); + assertFalse(nc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(nc.hasTransport(TRANSPORT_WIFI)); + // By default, VPN is set to track default network (i.e. its underlying networks is null). + // In case of no default network, VPN is considered metered. + assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + + // Connect to Cell; Cell is the default network. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + + // Connect to WiFi; WiFi is the new default. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && caps.hasTransport(TRANSPORT_WIFI) + && caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + + // Disconnect Cell. The default network did not change, so there shouldn't be any changes in + // the capabilities. + mCellNetworkAgent.disconnect(); + + // Disconnect wifi too. Now we have no default network. + mWiFiNetworkAgent.disconnect(); + + vpnNetworkCallback.expectCapabilitiesThat(mMockVpn, + (caps) -> caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_CELLULAR) && !caps.hasTransport(TRANSPORT_WIFI) + && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)); + + mMockVpn.disconnect(); + } + + @Test + public void testRestrictedProfileAffectsVpnUidRanges() throws Exception { + // NETWORK_SETTINGS is necessary to see the UID ranges in NetworkCapabilities. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + // Bring up a VPN + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + callback.expectAvailableThenValidatedCallbacks(mMockVpn); + callback.assertNoCallback(); + + final int uid = Process.myUid(); + NetworkCapabilities nc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertNotNull("nc=" + nc, nc.getUids()); + assertEquals(nc.getUids(), UidRange.toIntRanges(uidRangesForUids(uid))); + assertVpnTransportInfo(nc, VpnManager.TYPE_VPN_SERVICE); + + // Set an underlying network and expect to see the VPN transports change. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_WIFI)); + callback.expectCapabilitiesThat(mWiFiNetworkAgent, (caps) + -> caps.hasCapability(NET_CAPABILITY_VALIDATED)); + + when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER)) + .thenReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)); + + final Intent addedIntent = new Intent(ACTION_USER_ADDED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + + // Send a USER_ADDED broadcast for it. + processBroadcast(addedIntent); + + // Expect that the VPN UID ranges contain both |uid| and the UID range for the newly-added + // restricted user. + final UidRange rRange = UidRange.createForUser(UserHandle.of(RESTRICTED_USER)); + final Range<Integer> restrictUidRange = new Range<Integer>(rRange.start, rRange.stop); + final Range<Integer> singleUidRange = new Range<Integer>(uid, uid); + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.getUids().size() == 2 + && caps.getUids().contains(singleUidRange) + && caps.getUids().contains(restrictUidRange) + && caps.hasTransport(TRANSPORT_VPN) + && caps.hasTransport(TRANSPORT_WIFI)); + + // Change the VPN's capabilities somehow (specifically, disconnect wifi). + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.getUids().size() == 2 + && caps.getUids().contains(singleUidRange) + && caps.getUids().contains(restrictUidRange) + && caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_WIFI)); + + // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user. + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + processBroadcast(removedIntent); + + // Expect that the VPN gains the UID range for the restricted user, and that the capability + // change made just before that (i.e., loss of TRANSPORT_WIFI) is preserved. + callback.expectCapabilitiesThat(mMockVpn, (caps) + -> caps.getUids().size() == 1 + && caps.getUids().contains(singleUidRange) + && caps.hasTransport(TRANSPORT_VPN) + && !caps.hasTransport(TRANSPORT_WIFI)); + } + + @Test + public void testLockdownVpnWithRestrictedProfiles() throws Exception { + // For ConnectivityService#setAlwaysOnVpnPackage. + mServiceContext.setPermission( + Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED); + // For call Vpn#setAlwaysOnPackage. + mServiceContext.setPermission( + Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED); + // Necessary to see the UID ranges in NetworkCapabilities. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + final int uid = Process.myUid(); + + // Connect wifi and check that UIDs in the main and restricted profiles have network access. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true /* validated */); + final int restrictedUid = UserHandle.getUid(RESTRICTED_USER, 42 /* appId */); + assertNotNull(mCm.getActiveNetworkForUid(uid)); + assertNotNull(mCm.getActiveNetworkForUid(restrictedUid)); + + // Enable always-on VPN lockdown. The main user loses network access because no VPN is up. + final ArrayList<String> allowList = new ArrayList<>(); + mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE, + true /* lockdown */, allowList); + waitForIdle(); + assertNull(mCm.getActiveNetworkForUid(uid)); + // This is arguably overspecified: a UID that is not running doesn't have an active network. + // But it's useful to check that non-default users do not lose network access, and to prove + // that the loss of connectivity below is indeed due to the restricted profile coming up. + assertNotNull(mCm.getActiveNetworkForUid(restrictedUid)); + + // Start the restricted profile, and check that the UID within it loses network access. + when(mPackageManager.getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER)) + .thenReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)); + when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO, + RESTRICTED_USER_INFO)); + // TODO: check that VPN app within restricted profile still has access, etc. + final Intent addedIntent = new Intent(ACTION_USER_ADDED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + processBroadcast(addedIntent); + assertNull(mCm.getActiveNetworkForUid(uid)); + assertNull(mCm.getActiveNetworkForUid(restrictedUid)); + + // Stop the restricted profile, and check that the UID within it has network access again. + when(mUserManager.getAliveUsers()).thenReturn(Arrays.asList(PRIMARY_USER_INFO)); + + // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user. + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(RESTRICTED_USER)); + removedIntent.putExtra(Intent.EXTRA_USER_HANDLE, RESTRICTED_USER); + processBroadcast(removedIntent); + assertNull(mCm.getActiveNetworkForUid(uid)); + assertNotNull(mCm.getActiveNetworkForUid(restrictedUid)); + + mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */, + allowList); + waitForIdle(); + } + + @Test + public void testIsActiveNetworkMeteredOverWifi() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + + assertFalse(mCm.isActiveNetworkMetered()); + } + + @Test + public void testIsActiveNetworkMeteredOverCell() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + + assertTrue(mCm.isActiveNetworkMetered()); + } + + @Test + public void testIsActiveNetworkMeteredOverVpnTrackingPlatformDefault() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect VPN network. By default it is using current default network (Cell). + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + + // Ensure VPN is now the active network. + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect WiFi. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + // VPN should still be the active network. + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + // Expect VPN to be unmetered as it should now be using WiFi (new default). + assertFalse(mCm.isActiveNetworkMetered()); + + // Disconnecting Cell should not affect VPN's meteredness. + mCellNetworkAgent.disconnect(); + waitForIdle(); + + assertFalse(mCm.isActiveNetworkMetered()); + + // Disconnect WiFi; Now there is no platform default network. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + + // VPN without any underlying networks is treated as metered. + assertTrue(mCm.isActiveNetworkMetered()); + + mMockVpn.disconnect(); + } + + @Test + public void testIsActiveNetworkMeteredOverVpnSpecifyingUnderlyingNetworks() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + waitForIdle(); + assertTrue(mCm.isActiveNetworkMetered()); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertFalse(mCm.isActiveNetworkMetered()); + + // Connect VPN network. + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + + // Ensure VPN is now the active network. + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + // VPN is using Cell + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is now using WiFi + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be unmetered + assertFalse(mCm.isActiveNetworkMetered()); + + // VPN is using Cell | WiFi. + mMockVpn.setUnderlyingNetworks( + new Network[] { mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Expect VPN to be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is using WiFi | Cell. + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork(), mCellNetworkAgent.getNetwork() }); + waitForIdle(); + + // Order should not matter and VPN should still be metered. + assertTrue(mCm.isActiveNetworkMetered()); + + // VPN is not using any underlying networks. + mMockVpn.setUnderlyingNetworks(new Network[0]); + waitForIdle(); + + // VPN without underlying networks is treated as metered. + assertTrue(mCm.isActiveNetworkMetered()); + + mMockVpn.disconnect(); + } + + @Test + public void testIsActiveNetworkMeteredOverAlwaysMeteredVpn() throws Exception { + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertFalse(mCm.isActiveNetworkMetered()); + + // Connect VPN network. + mMockVpn.registerAgent(true /* isAlwaysMetered */, uidRangesForUids(Process.myUid()), + new LinkProperties()); + mMockVpn.connect(true); + waitForIdle(); + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + + // VPN is tracking current platform default (WiFi). + mMockVpn.setUnderlyingNetworks(null); + waitForIdle(); + + // Despite VPN using WiFi (which is unmetered), VPN itself is marked as always metered. + assertTrue(mCm.isActiveNetworkMetered()); + + + // VPN explicitly declares WiFi as its underlying network. + mMockVpn.setUnderlyingNetworks( + new Network[] { mWiFiNetworkAgent.getNetwork() }); + waitForIdle(); + + // Doesn't really matter whether VPN declares its underlying networks explicitly. + assertTrue(mCm.isActiveNetworkMetered()); + + // With WiFi lost, VPN is basically without any underlying networks. And in that case it is + // anyways suppose to be metered. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + + assertTrue(mCm.isActiveNetworkMetered()); + + mMockVpn.disconnect(); + } + + private class DetailedBlockedStatusCallback extends TestNetworkCallback { + public void expectAvailableThenValidatedCallbacks(HasNetwork n, int blockedStatus) { + super.expectAvailableThenValidatedCallbacks(n.getNetwork(), blockedStatus, TIMEOUT_MS); + } + public void expectBlockedStatusCallback(HasNetwork n, int blockedStatus) { + // This doesn't work: + // super.expectBlockedStatusCallback(blockedStatus, n.getNetwork()); + super.expectBlockedStatusCallback(blockedStatus, n.getNetwork(), TIMEOUT_MS); + } + public void onBlockedStatusChanged(Network network, int blockedReasons) { + getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons)); + } + } + + @Test + public void testNetworkBlockedStatus() throws Exception { + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .build(); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + final DetailedBlockedStatusCallback detailedCallback = new DetailedBlockedStatusCallback(); + mCm.registerNetworkCallback(cellRequest, detailedCallback); + + mockUidNetworkingBlocked(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + detailedCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent, + BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_BATTERY_SAVER); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_REASON_BATTERY_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + // If blocked state does not change but blocked reason does, the boolean callback is called. + // TODO: investigate de-duplicating. + setBlockedReasonChanged(BLOCKED_METERED_REASON_USER_RESTRICTED); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_USER_RESTRICTED); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_DATA_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + // Restrict the network based on UID rule and NOT_METERED capability change. + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + cellNetworkCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + cellNetworkCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, + mCellNetworkAgent); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectCapabilitiesWithout(NET_CAPABILITY_NOT_METERED, + mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_DATA_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.assertNoCallback(); + detailedCallback.assertNoCallback(); + + // Restrict background data. Networking is not blocked because the network is unmetered. + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + cellNetworkCallback.expectBlockedStatusCallback(true, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, + BLOCKED_METERED_REASON_DATA_SAVER); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + cellNetworkCallback.assertNoCallback(); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + detailedCallback.expectBlockedStatusCallback(mCellNetworkAgent, BLOCKED_REASON_NONE); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + setBlockedReasonChanged(BLOCKED_REASON_NONE); + cellNetworkCallback.assertNoCallback(); + detailedCallback.assertNoCallback(); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + + mCm.unregisterNetworkCallback(cellNetworkCallback); + } + + @Test + public void testNetworkBlockedStatusBeforeAndAfterConnect() throws Exception { + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + mockUidNetworkingBlocked(); + + // No Networkcallbacks invoked before any network is active. + setBlockedReasonChanged(BLOCKED_REASON_BATTERY_SAVER); + setBlockedReasonChanged(BLOCKED_REASON_NONE); + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + defaultCallback.assertNoCallback(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + defaultCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mCellNetworkAgent); + + // Allow to use the network after switching to NOT_METERED network. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + defaultCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Switch to METERED network. Restrict the use of the network. + mWiFiNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksValidatedAndBlocked(mCellNetworkAgent); + + // Network becomes NOT_METERED. + mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + defaultCallback.expectCapabilitiesWith(NET_CAPABILITY_NOT_METERED, mCellNetworkAgent); + defaultCallback.expectBlockedStatusCallback(false, mCellNetworkAgent); + + // Verify there's no Networkcallbacks invoked after data saver on/off. + setBlockedReasonChanged(BLOCKED_METERED_REASON_DATA_SAVER); + setBlockedReasonChanged(BLOCKED_REASON_NONE); + defaultCallback.assertNoCallback(); + + mCellNetworkAgent.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(defaultCallback); + } + + private void expectNetworkRejectNonSecureVpn(InOrder inOrder, boolean add, + UidRangeParcel... expected) throws Exception { + inOrder.verify(mMockNetd).networkRejectNonSecureVpn(eq(add), aryEq(expected)); + } + + private void checkNetworkInfo(NetworkInfo ni, int type, DetailedState state) { + assertNotNull(ni); + assertEquals(type, ni.getType()); + assertEquals(ConnectivityManager.getNetworkTypeName(type), state, ni.getDetailedState()); + if (state == DetailedState.CONNECTED || state == DetailedState.SUSPENDED) { + assertNotNull(ni.getExtraInfo()); + } else { + // Technically speaking, a network that's in CONNECTING state will generally have a + // non-null extraInfo. This doesn't actually happen in this test because it never calls + // a legacy API while a network is connecting. When a network is in CONNECTING state + // because of legacy lockdown VPN, its extraInfo is always null. + assertNull(ni.getExtraInfo()); + } + } + + private void assertActiveNetworkInfo(int type, DetailedState state) { + checkNetworkInfo(mCm.getActiveNetworkInfo(), type, state); + } + private void assertNetworkInfo(int type, DetailedState state) { + checkNetworkInfo(mCm.getNetworkInfo(type), type, state); + } + + private void assertExtraInfoFromCm(TestNetworkAgentWrapper network, boolean present) { + final NetworkInfo niForNetwork = mCm.getNetworkInfo(network.getNetwork()); + final NetworkInfo niForType = mCm.getNetworkInfo(network.getLegacyType()); + if (present) { + assertEquals(network.getExtraInfo(), niForNetwork.getExtraInfo()); + assertEquals(network.getExtraInfo(), niForType.getExtraInfo()); + } else { + assertNull(niForNetwork.getExtraInfo()); + assertNull(niForType.getExtraInfo()); + } + } + + private void assertExtraInfoFromCmBlocked(TestNetworkAgentWrapper network) { + assertExtraInfoFromCm(network, false); + } + + private void assertExtraInfoFromCmPresent(TestNetworkAgentWrapper network) { + assertExtraInfoFromCm(network, true); + } + + // Checks that each of the |agents| receive a blocked status change callback with the specified + // |blocked| value, in any order. This is needed because when an event affects multiple + // networks, ConnectivityService does not guarantee the order in which callbacks are fired. + private void assertBlockedCallbackInAnyOrder(TestNetworkCallback callback, boolean blocked, + TestNetworkAgentWrapper... agents) { + final List<Network> expectedNetworks = Arrays.asList(agents).stream() + .map((agent) -> agent.getNetwork()) + .collect(Collectors.toList()); + + // Expect exactly one blocked callback for each agent. + for (int i = 0; i < agents.length; i++) { + CallbackEntry e = callback.expectCallbackThat(TIMEOUT_MS, (c) -> + c instanceof CallbackEntry.BlockedStatus + && ((CallbackEntry.BlockedStatus) c).getBlocked() == blocked); + Network network = e.getNetwork(); + assertTrue("Received unexpected blocked callback for network " + network, + expectedNetworks.remove(network)); + } + } + + @Test + public void testNetworkBlockedStatusAlwaysOnVpn() throws Exception { + mServiceContext.setPermission( + Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED); + mServiceContext.setPermission( + Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TestNetworkCallback callback = new TestNetworkCallback(); + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + mCm.registerNetworkCallback(request, callback); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + final TestNetworkCallback vpnUidCallback = new TestNetworkCallback(); + final NetworkRequest vpnUidRequest = new NetworkRequest.Builder().build(); + registerNetworkCallbackAsUid(vpnUidRequest, vpnUidCallback, VPN_UID); + + final TestNetworkCallback vpnUidDefaultCallback = new TestNetworkCallback(); + registerDefaultNetworkCallbackAsUid(vpnUidDefaultCallback, VPN_UID); + + final TestNetworkCallback vpnDefaultCallbackAsUid = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallbackForUid(VPN_UID, vpnDefaultCallbackAsUid, + new Handler(ConnectivityThread.getInstanceLooper())); + + final int uid = Process.myUid(); + final int userId = UserHandle.getUserId(uid); + final ArrayList<String> allowList = new ArrayList<>(); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + waitForIdle(); + + UidRangeParcel firstHalf = new UidRangeParcel(1, VPN_UID - 1); + UidRangeParcel secondHalf = new UidRangeParcel(VPN_UID + 1, 99999); + InOrder inOrder = inOrder(mMockNetd); + expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf); + + // Connect a network when lockdown is active, expect to see it blocked. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + vpnUidCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + vpnUidDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + vpnDefaultCallbackAsUid.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + // Mobile is BLOCKED even though it's not actually connected. + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + + // Disable lockdown, expect to see the network unblocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + callback.expectBlockedStatusCallback(false, mWiFiNetworkAgent); + defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Add our UID to the allowlist and re-enable lockdown, expect network is not blocked. + allowList.add(TEST_PACKAGE_NAME); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + callback.assertNoCallback(); + defaultCallback.assertNoCallback(); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + + // The following requires that the UID of this test package is greater than VPN_UID. This + // is always true in practice because a plain AOSP build with no apps installed has almost + // 200 packages installed. + final UidRangeParcel piece1 = new UidRangeParcel(1, VPN_UID - 1); + final UidRangeParcel piece2 = new UidRangeParcel(VPN_UID + 1, uid - 1); + final UidRangeParcel piece3 = new UidRangeParcel(uid + 1, 99999); + expectNetworkRejectNonSecureVpn(inOrder, true, piece1, piece2, piece3); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Connect a new network, expect it to be unblocked. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + defaultCallback.assertNoCallback(); + vpnUidCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + // Cellular is DISCONNECTED because it's not the default and there are no requests for it. + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Disable lockdown, remove our UID from the allowlist, and re-enable lockdown. + // Everything should now be blocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + waitForIdle(); + expectNetworkRejectNonSecureVpn(inOrder, false, piece1, piece2, piece3); + allowList.clear(); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + waitForIdle(); + expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf); + defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent); + assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + + // Disable lockdown. Everything is unblocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent); + assertBlockedCallbackInAnyOrder(callback, false, mWiFiNetworkAgent, mCellNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Enable and disable an always-on VPN package without lockdown. Expect no changes. + reset(mMockNetd); + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, false /* lockdown */, + allowList); + inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any()); + callback.assertNoCallback(); + defaultCallback.assertNoCallback(); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + mVpnManagerService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList); + inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any()); + callback.assertNoCallback(); + defaultCallback.assertNoCallback(); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + // Enable lockdown and connect a VPN. The VPN is not blocked. + mVpnManagerService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, + allowList); + defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent); + assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertNull(mCm.getActiveNetwork()); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + vpnUidCallback.assertNoCallback(); // vpnUidCallback has NOT_VPN capability. + vpnUidDefaultCallback.assertNoCallback(); // VPN does not apply to VPN_UID + vpnDefaultCallbackAsUid.assertNoCallback(); + assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork()); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID)); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + + mMockVpn.disconnect(); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + vpnUidCallback.assertNoCallback(); + vpnUidDefaultCallback.assertNoCallback(); + vpnDefaultCallbackAsUid.assertNoCallback(); + assertNull(mCm.getActiveNetwork()); + + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(defaultCallback); + mCm.unregisterNetworkCallback(vpnUidCallback); + mCm.unregisterNetworkCallback(vpnUidDefaultCallback); + mCm.unregisterNetworkCallback(vpnDefaultCallbackAsUid); + } + + private void setupLegacyLockdownVpn() { + final String profileName = "testVpnProfile"; + final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8); + when(mVpnProfileStore.get(Credentials.LOCKDOWN_VPN)).thenReturn(profileTag); + + final VpnProfile profile = new VpnProfile(profileName); + profile.name = "My VPN"; + profile.server = "192.0.2.1"; + profile.dnsServers = "8.8.8.8"; + profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK; + final byte[] encodedProfile = profile.encode(); + when(mVpnProfileStore.get(Credentials.VPN + profileName)).thenReturn(encodedProfile); + } + + private void establishLegacyLockdownVpn(Network underlying) throws Exception { + // The legacy lockdown VPN only supports userId 0, and must have an underlying network. + assertNotNull(underlying); + mMockVpn.setVpnType(VpnManager.TYPE_VPN_LEGACY); + // The legacy lockdown VPN only supports userId 0. + final Set<UidRange> ranges = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.registerAgent(ranges); + mMockVpn.setUnderlyingNetworks(new Network[]{underlying}); + mMockVpn.connect(true); + } + + @Test + public void testLegacyLockdownVpn() throws Exception { + mServiceContext.setPermission( + Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED); + // For LockdownVpnTracker to call registerSystemDefaultNetworkCallback. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(defaultCallback); + + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + + // Pretend lockdown VPN was configured. + setupLegacyLockdownVpn(); + + // LockdownVpnTracker disables the Vpn teardown code and enables lockdown. + // Check the VPN's state before it does so. + assertTrue(mMockVpn.getEnableTeardown()); + assertFalse(mMockVpn.getLockdown()); + + // Send a USER_UNLOCKED broadcast so CS starts LockdownVpnTracker. + final int userId = UserHandle.getUserId(Process.myUid()); + final Intent addedIntent = new Intent(ACTION_USER_UNLOCKED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId)); + addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); + processBroadcast(addedIntent); + + // Lockdown VPN disables teardown and enables lockdown. + assertFalse(mMockVpn.getEnableTeardown()); + assertTrue(mMockVpn.getLockdown()); + + // Bring up a network. + // Expect nothing to happen because the network does not have an IPv4 default route: legacy + // VPN only supports IPv4. + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName("rmnet0"); + cellLp.addLinkAddress(new LinkAddress("2001:db8::1/64")); + cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, "rmnet0")); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + waitForIdle(); + assertNull(mMockVpn.getAgent()); + + // Add an IPv4 address. Ideally the VPN should start, but it doesn't because nothing calls + // LockdownVpnTracker#handleStateChangedLocked. This is a bug. + // TODO: consider fixing this. + cellLp.addLinkAddress(new LinkAddress("192.0.2.2/25")); + cellLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "rmnet0")); + mCellNetworkAgent.sendLinkProperties(cellLp); + callback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, + mCellNetworkAgent); + waitForIdle(); + assertNull(mMockVpn.getAgent()); + + // Disconnect, then try again with a network that supports IPv4 at connection time. + // Expect lockdown VPN to come up. + ExpectedBroadcast b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED); + mCellNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + b1.expectBroadcast(); + + // When lockdown VPN is active, the NetworkInfo state in CONNECTIVITY_ACTION is overwritten + // with the state of the VPN network. So expect a CONNECTING broadcast. + b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(false /* validated */); + callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellNetworkAgent); + b1.expectBroadcast(); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mCellNetworkAgent); + + // TODO: it would be nice if we could simply rely on the production code here, and have + // LockdownVpnTracker start the VPN, have the VPN code register its NetworkAgent with + // ConnectivityService, etc. That would require duplicating a fair bit of code from the + // Vpn tests around how to mock out LegacyVpnRunner. But even if we did that, this does not + // work for at least two reasons: + // 1. In this test, calling registerNetworkAgent does not actually result in an agent being + // registered. This is because nothing calls onNetworkMonitorCreated, which is what + // actually ends up causing handleRegisterNetworkAgent to be called. Code in this test + // that wants to register an agent must use TestNetworkAgentWrapper. + // 2. Even if we exposed Vpn#agentConnect to the test, and made MockVpn#agentConnect call + // the TestNetworkAgentWrapper code, this would deadlock because the + // TestNetworkAgentWrapper code cannot be called on the handler thread since it calls + // waitForIdle(). + mMockVpn.expectStartLegacyVpnRunner(); + b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED); + ExpectedBroadcast b2 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED); + establishLegacyLockdownVpn(mCellNetworkAgent.getNetwork()); + callback.expectAvailableThenValidatedCallbacks(mMockVpn); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + b1.expectBroadcast(); + b2.expectBroadcast(); + assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mCellNetworkAgent); + assertTrue(vpnNc.hasTransport(TRANSPORT_VPN)); + assertTrue(vpnNc.hasTransport(TRANSPORT_CELLULAR)); + assertFalse(vpnNc.hasTransport(TRANSPORT_WIFI)); + assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED)); + assertVpnTransportInfo(vpnNc, VpnManager.TYPE_VPN_LEGACY); + + // Switch default network from cell to wifi. Expect VPN to disconnect and reconnect. + final LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName("wlan0"); + wifiLp.addLinkAddress(new LinkAddress("192.0.2.163/25")); + wifiLp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, "wlan0")); + final NetworkCapabilities wifiNc = new NetworkCapabilities(); + wifiNc.addTransportType(TRANSPORT_WIFI); + wifiNc.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc); + + b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED); + // Wifi is CONNECTING because the VPN isn't up yet. + b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING); + ExpectedBroadcast b3 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.connect(false /* validated */); + b1.expectBroadcast(); + b2.expectBroadcast(); + b3.expectBroadcast(); + mMockVpn.expectStopVpnRunnerPrivileged(); + mMockVpn.expectStartLegacyVpnRunner(); + + // TODO: why is wifi not blocked? Is it because when this callback is sent, the VPN is still + // connected, so the network is not considered blocked by the lockdown UID ranges? But the + // fact that a VPN is connected should only result in the VPN itself being unblocked, not + // any other network. Bug in isUidBlockedByVpn? + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + callback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectCallback(CallbackEntry.LOST, mMockVpn); + defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiNetworkAgent); + systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // While the VPN is reconnecting on the new network, everything is blocked. + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED); + assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED); + assertExtraInfoFromCmBlocked(mWiFiNetworkAgent); + + // The VPN comes up again on wifi. + b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED); + b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED); + establishLegacyLockdownVpn(mWiFiNetworkAgent.getNetwork()); + callback.expectAvailableThenValidatedCallbacks(mMockVpn); + defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn); + systemDefaultCallback.assertNoCallback(); + b1.expectBroadcast(); + b2.expectBroadcast(); + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mWiFiNetworkAgent); + vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork()); + assertTrue(vpnNc.hasTransport(TRANSPORT_VPN)); + assertTrue(vpnNc.hasTransport(TRANSPORT_WIFI)); + assertFalse(vpnNc.hasTransport(TRANSPORT_CELLULAR)); + assertTrue(vpnNc.hasCapability(NET_CAPABILITY_NOT_METERED)); + + // Disconnect cell. Nothing much happens since it's not the default network. + mCellNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + defaultCallback.assertNoCallback(); + systemDefaultCallback.assertNoCallback(); + + assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED); + assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED); + assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED); + assertExtraInfoFromCmPresent(mWiFiNetworkAgent); + + b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED); + b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + systemDefaultCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + b1.expectBroadcast(); + callback.expectCapabilitiesThat(mMockVpn, nc -> !nc.hasTransport(TRANSPORT_WIFI)); + mMockVpn.expectStopVpnRunnerPrivileged(); + callback.expectCallback(CallbackEntry.LOST, mMockVpn); + b2.expectBroadcast(); + } + + /** + * Test mutable and requestable network capabilities such as + * {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and + * {@link NetworkCapabilities#NET_CAPABILITY_NOT_VCN_MANAGED}. Verify that the + * {@code ConnectivityService} re-assign the networks accordingly. + */ + @Test + public final void testLoseMutableAndRequestableCaps() throws Exception { + final int[] testCaps = new int [] { + NET_CAPABILITY_TRUSTED, + NET_CAPABILITY_NOT_VCN_MANAGED + }; + for (final int testCap : testCaps) { + // Create requests with and without the testing capability. + final TestNetworkCallback callbackWithCap = new TestNetworkCallback(); + final TestNetworkCallback callbackWithoutCap = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder().addCapability(testCap).build(), + callbackWithCap); + mCm.requestNetwork(new NetworkRequest.Builder().removeCapability(testCap).build(), + callbackWithoutCap); + + // Setup networks with testing capability and verify the default network changes. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.addCapability(testCap); + mCellNetworkAgent.connect(true); + callbackWithCap.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + callbackWithoutCap.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + verify(mMockNetd).networkSetDefault(eq(mCellNetworkAgent.getNetwork().netId)); + reset(mMockNetd); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(testCap); + mWiFiNetworkAgent.connect(true); + callbackWithCap.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + callbackWithoutCap.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + verify(mMockNetd).networkSetDefault(eq(mWiFiNetworkAgent.getNetwork().netId)); + reset(mMockNetd); + + // Remove the testing capability on wifi, verify the callback and default network + // changes back to cellular. + mWiFiNetworkAgent.removeCapability(testCap); + callbackWithCap.expectAvailableCallbacksValidated(mCellNetworkAgent); + callbackWithoutCap.expectCapabilitiesWithout(testCap, mWiFiNetworkAgent); + verify(mMockNetd).networkSetDefault(eq(mCellNetworkAgent.getNetwork().netId)); + reset(mMockNetd); + + mCellNetworkAgent.removeCapability(testCap); + callbackWithCap.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + callbackWithoutCap.assertNoCallback(); + verify(mMockNetd).networkClearDefault(); + + mCm.unregisterNetworkCallback(callbackWithCap); + mCm.unregisterNetworkCallback(callbackWithoutCap); + } + } + + @Test + public final void testBatteryStatsNetworkType() throws Exception { + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName("cell0"); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(true); + waitForIdle(); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + cellLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + + final LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName("wifi0"); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + wifiLp.getInterfaceName(), + new int[] { TRANSPORT_WIFI }); + + mCellNetworkAgent.disconnect(); + mWiFiNetworkAgent.disconnect(); + + cellLp.setInterfaceName("wifi0"); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + mCellNetworkAgent.connect(true); + waitForIdle(); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + cellLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + mCellNetworkAgent.disconnect(); + } + + /** + * Make simulated InterfaceConfigParcel for Nat464Xlat to query clat lower layer info. + */ + private InterfaceConfigurationParcel getClatInterfaceConfigParcel(LinkAddress la) { + final InterfaceConfigurationParcel cfg = new InterfaceConfigurationParcel(); + cfg.hwAddr = "11:22:33:44:55:66"; + cfg.ipv4Addr = la.getAddress().getHostAddress(); + cfg.prefixLength = la.getPrefixLength(); + return cfg; + } + + /** + * Make expected stack link properties, copied from Nat464Xlat. + */ + private LinkProperties makeClatLinkProperties(LinkAddress la) { + LinkAddress clatAddress = la; + LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName(CLAT_PREFIX + MOBILE_IFNAME); + RouteInfo ipv4Default = new RouteInfo( + new LinkAddress(Inet4Address.ANY, 0), + clatAddress.getAddress(), CLAT_PREFIX + MOBILE_IFNAME); + stacked.addRoute(ipv4Default); + stacked.addLinkAddress(clatAddress); + return stacked; + } + + private Nat64PrefixEventParcel makeNat64PrefixEvent(final int netId, final int prefixOperation, + final String prefixAddress, final int prefixLength) { + final Nat64PrefixEventParcel event = new Nat64PrefixEventParcel(); + event.netId = netId; + event.prefixOperation = prefixOperation; + event.prefixAddress = prefixAddress; + event.prefixLength = prefixLength; + return event; + } + + @Test + public void testStackedLinkProperties() throws Exception { + final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24"); + final LinkAddress myIpv6 = new LinkAddress("2001:db8:1::1/64"); + final String kNat64PrefixString = "2001:db8:64:64:64:64::"; + final IpPrefix kNat64Prefix = new IpPrefix(InetAddress.getByName(kNat64PrefixString), 96); + final String kOtherNat64PrefixString = "64:ff9b::"; + final IpPrefix kOtherNat64Prefix = new IpPrefix( + InetAddress.getByName(kOtherNat64PrefixString), 96); + final RouteInfo defaultRoute = new RouteInfo((IpPrefix) null, myIpv6.getAddress(), + MOBILE_IFNAME); + final RouteInfo ipv6Subnet = new RouteInfo(myIpv6, null, MOBILE_IFNAME); + final RouteInfo ipv4Subnet = new RouteInfo(myIpv4, null, MOBILE_IFNAME); + final RouteInfo stackedDefault = new RouteInfo((IpPrefix) null, myIpv4.getAddress(), + CLAT_PREFIX + MOBILE_IFNAME); + + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + // Prepare ipv6 only link properties. + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + cellLp.addLinkAddress(myIpv6); + cellLp.addRoute(defaultRoute); + cellLp.addRoute(ipv6Subnet); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp); + reset(mMockDnsResolver); + reset(mMockNetd); + + // Connect with ipv6 link properties. Expect prefix discovery to be started. + mCellNetworkAgent.connect(true); + final int cellNetId = mCellNetworkAgent.getNetwork().netId; + waitForIdle(); + + verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId, + INetd.PERMISSION_NONE)); + assertRoutesAdded(cellNetId, ipv6Subnet, defaultRoute); + verify(mMockDnsResolver, times(1)).createNetworkCache(eq(cellNetId)); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, MOBILE_IFNAME); + verify(mDeps).reportNetworkInterfaceForTransports(mServiceContext, + cellLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + + networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId); + + // Switching default network updates TCP buffer sizes. + verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES); + // Add an IPv4 address. Expect prefix discovery to be stopped. Netd doesn't tell us that + // the NAT64 prefix was removed because one was never discovered. + cellLp.addLinkAddress(myIpv4); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + assertRoutesAdded(cellNetId, ipv4Subnet); + verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId); + verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any()); + + // Make sure BatteryStats was not told about any v4- interfaces, as none should have + // come online yet. + waitForIdle(); + verify(mDeps, never()) + .reportNetworkInterfaceForTransports(eq(mServiceContext), startsWith("v4-"), any()); + + verifyNoMoreInteractions(mMockNetd); + verifyNoMoreInteractions(mMockDnsResolver); + reset(mMockNetd); + reset(mMockDnsResolver); + when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME)) + .thenReturn(getClatInterfaceConfigParcel(myIpv4)); + + // Remove IPv4 address. Expect prefix discovery to be started again. + cellLp.removeLinkAddress(myIpv4); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId); + assertRoutesRemoved(cellNetId, ipv4Subnet); + + // When NAT64 prefix discovery succeeds, LinkProperties are updated and clatd is started. + Nat464Xlat clat = getNat464Xlat(mCellNetworkAgent); + assertNull(mCm.getLinkProperties(mCellNetworkAgent.getNetwork()).getNat64Prefix()); + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96)); + LinkProperties lpBeforeClat = networkCallback.expectCallback( + CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent).getLp(); + assertEquals(0, lpBeforeClat.getStackedLinks().size()); + assertEquals(kNat64Prefix, lpBeforeClat.getNat64Prefix()); + verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString()); + + // Clat iface comes up. Expect stacked link to be added. + clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + List<LinkProperties> stackedLps = mCm.getLinkProperties(mCellNetworkAgent.getNetwork()) + .getStackedLinks(); + assertEquals(makeClatLinkProperties(myIpv4), stackedLps.get(0)); + assertRoutesAdded(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + // Change trivial linkproperties and see if stacked link is preserved. + cellLp.addDnsServer(InetAddress.getByName("8.8.8.8")); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + + List<LinkProperties> stackedLpsAfterChange = + mCm.getLinkProperties(mCellNetworkAgent.getNetwork()).getStackedLinks(); + assertNotEquals(stackedLpsAfterChange, Collections.EMPTY_LIST); + assertEquals(makeClatLinkProperties(myIpv4), stackedLpsAfterChange.get(0)); + + verify(mMockDnsResolver, times(1)).setResolverConfiguration( + mResolverParamsParcelCaptor.capture()); + ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue(); + assertEquals(1, resolvrParams.servers.length); + assertTrue(ArrayUtils.contains(resolvrParams.servers, "8.8.8.8")); + + for (final LinkProperties stackedLp : stackedLpsAfterChange) { + verify(mDeps).reportNetworkInterfaceForTransports( + mServiceContext, stackedLp.getInterfaceName(), + new int[] { TRANSPORT_CELLULAR }); + } + reset(mMockNetd); + when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME)) + .thenReturn(getClatInterfaceConfigParcel(myIpv4)); + // Change the NAT64 prefix without first removing it. + // Expect clatd to be stopped and started with the new prefix. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_ADDED, kOtherNat64PrefixString, 96)); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 0); + verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME); + assertRoutesRemoved(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + + verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kOtherNat64Prefix.toString()); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getNat64Prefix().equals(kOtherNat64Prefix)); + clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 1); + assertRoutesAdded(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + reset(mMockNetd); + + // Add ipv4 address, expect that clatd and prefix discovery are stopped and stacked + // linkproperties are cleaned up. + cellLp.addLinkAddress(myIpv4); + cellLp.addRoute(ipv4Subnet); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + assertRoutesAdded(cellNetId, ipv4Subnet); + verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME); + verify(mMockDnsResolver, times(1)).stopPrefix64Discovery(cellNetId); + + // As soon as stop is called, the linkproperties lose the stacked interface. + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + LinkProperties actualLpAfterIpv4 = mCm.getLinkProperties(mCellNetworkAgent.getNetwork()); + LinkProperties expected = new LinkProperties(cellLp); + expected.setNat64Prefix(kOtherNat64Prefix); + assertEquals(expected, actualLpAfterIpv4); + assertEquals(0, actualLpAfterIpv4.getStackedLinks().size()); + assertRoutesRemoved(cellNetId, stackedDefault); + + // The interface removed callback happens but has no effect after stop is called. + clat.interfaceRemoved(CLAT_PREFIX + MOBILE_IFNAME); + networkCallback.assertNoCallback(); + verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + verifyNoMoreInteractions(mMockNetd); + verifyNoMoreInteractions(mMockDnsResolver); + reset(mMockNetd); + reset(mMockDnsResolver); + when(mMockNetd.interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME)) + .thenReturn(getClatInterfaceConfigParcel(myIpv4)); + + // Stopping prefix discovery causes netd to tell us that the NAT64 prefix is gone. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_REMOVED, kOtherNat64PrefixString, 96)); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getNat64Prefix() == null); + + // Remove IPv4 address and expect prefix discovery and clatd to be started again. + cellLp.removeLinkAddress(myIpv4); + cellLp.removeRoute(new RouteInfo(myIpv4, null, MOBILE_IFNAME)); + cellLp.removeDnsServer(InetAddress.getByName("8.8.8.8")); + mCellNetworkAgent.sendLinkProperties(cellLp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + assertRoutesRemoved(cellNetId, ipv4Subnet); // Directly-connected routes auto-added. + verify(mMockDnsResolver, times(1)).startPrefix64Discovery(cellNetId); + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_ADDED, kNat64PrefixString, 96)); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + verify(mMockNetd, times(1)).clatdStart(MOBILE_IFNAME, kNat64Prefix.toString()); + + // Clat iface comes up. Expect stacked link to be added. + clat.interfaceLinkStateChanged(CLAT_PREFIX + MOBILE_IFNAME, true); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 1 && lp.getNat64Prefix() != null); + assertRoutesAdded(cellNetId, stackedDefault); + verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + + // NAT64 prefix is removed. Expect that clat is stopped. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent( + cellNetId, PREFIX_OPERATION_REMOVED, kNat64PrefixString, 96)); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 0 && lp.getNat64Prefix() == null); + assertRoutesRemoved(cellNetId, ipv4Subnet, stackedDefault); + + // Stop has no effect because clat is already stopped. + verify(mMockNetd, times(1)).clatdStop(MOBILE_IFNAME); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + (lp) -> lp.getStackedLinks().size() == 0); + verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_PREFIX + MOBILE_IFNAME); + verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_PREFIX + MOBILE_IFNAME); + verifyNoMoreInteractions(mMockNetd); + // Clean up. + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + networkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(networkCallback); + } + + private void expectNat64PrefixChange(TestableNetworkCallback callback, + TestNetworkAgentWrapper agent, IpPrefix prefix) { + callback.expectLinkPropertiesThat(agent, x -> Objects.equals(x.getNat64Prefix(), prefix)); + } + + @Test + public void testNat64PrefixMultipleSources() throws Exception { + final String iface = "wlan0"; + final String pref64FromRaStr = "64:ff9b::"; + final String pref64FromDnsStr = "2001:db8:64::"; + final IpPrefix pref64FromRa = new IpPrefix(InetAddress.getByName(pref64FromRaStr), 96); + final IpPrefix pref64FromDns = new IpPrefix(InetAddress.getByName(pref64FromDnsStr), 96); + final IpPrefix newPref64FromRa = new IpPrefix("2001:db8:64:64:64:64::/96"); + + final NetworkRequest request = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, callback); + + final LinkProperties baseLp = new LinkProperties(); + baseLp.setInterfaceName(iface); + baseLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + baseLp.addDnsServer(InetAddress.getByName("2001:4860:4860::6464")); + + reset(mMockNetd, mMockDnsResolver); + InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver); + + // If a network already has a NAT64 prefix on connect, clatd is started immediately and + // prefix discovery is never started. + LinkProperties lp = new LinkProperties(baseLp); + lp.setNat64Prefix(pref64FromRa); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp); + mWiFiNetworkAgent.connect(false); + final Network network = mWiFiNetworkAgent.getNetwork(); + int netId = network.getNetId(); + callback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + callback.assertNoCallback(); + assertEquals(pref64FromRa, mCm.getLinkProperties(network).getNat64Prefix()); + + // If the RA prefix is withdrawn, clatd is stopped and prefix discovery is started. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, null); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId); + + // If the RA prefix appears while DNS discovery is in progress, discovery is stopped and + // clatd is started with the prefix from the RA. + lp.setNat64Prefix(pref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString()); + + // Withdraw the RA prefix so we can test the case where an RA prefix appears after DNS + // discovery has succeeded. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, null); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId); + + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96)); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString()); + + // If an RA advertises the same prefix that was discovered by DNS, nothing happens: prefix + // discovery is not stopped, and there are no callbacks. + lp.setNat64Prefix(pref64FromDns); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // If the RA is later withdrawn, nothing happens again. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // If the RA prefix changes, clatd is restarted and prefix discovery is stopped. + lp.setNat64Prefix(pref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromRa); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId); + + // Stopping prefix discovery results in a prefix removed notification. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(netId, PREFIX_OPERATION_REMOVED, pref64FromDnsStr, 96)); + + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, pref64FromRa.toString()); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + + // If the RA prefix changes, clatd is restarted and prefix discovery is not started. + lp.setNat64Prefix(newPref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, newPref64FromRa); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockNetd).clatdStart(iface, newPref64FromRa.toString()); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, newPref64FromRa.toString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + + // If the RA prefix changes to the same value, nothing happens. + lp.setNat64Prefix(newPref64FromRa); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + assertEquals(newPref64FromRa, mCm.getLinkProperties(network).getNat64Prefix()); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // The transition between no prefix and DNS prefix is tested in testStackedLinkProperties. + + // If the same prefix is learned first by DNS and then by RA, and clat is later stopped, + // (e.g., because the network disconnects) setPrefix64(netid, "") is never called. + lp.setNat64Prefix(null); + mWiFiNetworkAgent.sendLinkProperties(lp); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, null); + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).setPrefix64(netId, ""); + inOrder.verify(mMockDnsResolver).startPrefix64Discovery(netId); + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(netId, PREFIX_OPERATION_ADDED, pref64FromDnsStr, 96)); + expectNat64PrefixChange(callback, mWiFiNetworkAgent, pref64FromDns); + inOrder.verify(mMockNetd).clatdStart(iface, pref64FromDns.toString()); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), any()); + + lp.setNat64Prefix(pref64FromDns); + mWiFiNetworkAgent.sendLinkProperties(lp); + callback.assertNoCallback(); + inOrder.verify(mMockNetd, never()).clatdStop(iface); + inOrder.verify(mMockNetd, never()).clatdStart(eq(iface), anyString()); + inOrder.verify(mMockDnsResolver, never()).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).startPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + // When tearing down a network, clat state is only updated after CALLBACK_LOST is fired, but + // before CONNECTIVITY_ACTION is sent. Wait for CONNECTIVITY_ACTION before verifying that + // clat has been stopped, or the test will be flaky. + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.disconnect(); + callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + b.expectBroadcast(); + + inOrder.verify(mMockNetd).clatdStop(iface); + inOrder.verify(mMockDnsResolver).stopPrefix64Discovery(netId); + inOrder.verify(mMockDnsResolver, never()).setPrefix64(eq(netId), anyString()); + + mCm.unregisterNetworkCallback(callback); + } + + @Test + public void testWith464XlatDisable() throws Exception { + doReturn(false).when(mDeps).getCellular464XlatEnabled(); + + final TestNetworkCallback callback = new TestNetworkCallback(); + final TestNetworkCallback defaultCallback = new TestNetworkCallback(); + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + mCm.registerNetworkCallback(networkRequest, callback); + mCm.registerDefaultNetworkCallback(defaultCallback); + + // Bring up validated cell. + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + cellLp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, MOBILE_IFNAME)); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + defaultCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + final int cellNetId = mCellNetworkAgent.getNetwork().netId; + waitForIdle(); + + verify(mMockDnsResolver, never()).startPrefix64Discovery(cellNetId); + Nat464Xlat clat = getNat464Xlat(mCellNetworkAgent); + assertTrue("Nat464Xlat was not IDLE", !clat.isStarted()); + + // This cannot happen because prefix discovery cannot succeed if it is never started. + mService.mResolverUnsolEventCallback.onNat64PrefixEvent( + makeNat64PrefixEvent(cellNetId, PREFIX_OPERATION_ADDED, "64:ff9b::", 96)); + + // ... but still, check that even if it did, clatd would not be started. + verify(mMockNetd, never()).clatdStart(anyString(), anyString()); + assertTrue("Nat464Xlat was not IDLE", !clat.isStarted()); + } + + @Test + public void testDataActivityTracking() throws Exception { + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + final LinkProperties cellLp = new LinkProperties(); + cellLp.setInterfaceName(MOBILE_IFNAME); + mCellNetworkAgent.sendLinkProperties(cellLp); + mCellNetworkAgent.connect(true); + networkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName(WIFI_IFNAME); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + + // Network switch + mWiFiNetworkAgent.connect(true); + networkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + networkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + // Disconnect wifi and switch back to cell + reset(mMockNetd); + mWiFiNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + assertNoCallbacks(networkCallback); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + // reconnect wifi + reset(mMockNetd); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + wifiLp.setInterfaceName(WIFI_IFNAME); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + mWiFiNetworkAgent.connect(true); + networkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + networkCallback.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + networkCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + verify(mMockNetd, times(1)).idletimerAddInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + + // Disconnect cell + reset(mMockNetd); + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + // LOST callback is triggered earlier than removing idle timer. Broadcast should also be + // sent as network being switched. Ensure rule removal for cell will not be triggered + // unexpectedly before network being removed. + waitForIdle(); + verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_CELLULAR))); + verify(mMockNetd, times(1)).networkDestroy(eq(mCellNetworkAgent.getNetwork().netId)); + verify(mMockDnsResolver, times(1)) + .destroyNetworkCache(eq(mCellNetworkAgent.getNetwork().netId)); + + // Disconnect wifi + ExpectedBroadcast b = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED); + mWiFiNetworkAgent.disconnect(); + b.expectBroadcast(); + verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(), + eq(Integer.toString(TRANSPORT_WIFI))); + + // Clean up + mCm.unregisterNetworkCallback(networkCallback); + } + + private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception { + String[] values = tcpBufferSizes.split(","); + String rmemValues = String.join(" ", values[0], values[1], values[2]); + String wmemValues = String.join(" ", values[3], values[4], values[5]); + verify(mMockNetd, atLeastOnce()).setTcpRWmemorySize(rmemValues, wmemValues); + reset(mMockNetd); + } + + @Test + public void testTcpBufferReset() throws Exception { + final String testTcpBufferSizes = "1,2,3,4,5,6"; + final NetworkRequest networkRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(networkRequest, networkCallback); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + reset(mMockNetd); + // Switching default network updates TCP buffer sizes. + mCellNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + verifyTcpBufferSizeChange(ConnectivityService.DEFAULT_TCP_BUFFER_SIZES); + // Change link Properties should have updated tcp buffer size. + LinkProperties lp = new LinkProperties(); + lp.setTcpBufferSizes(testTcpBufferSizes); + mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectCallback(CallbackEntry.LINK_PROPERTIES_CHANGED, mCellNetworkAgent); + verifyTcpBufferSizeChange(testTcpBufferSizes); + // Clean up. + mCellNetworkAgent.disconnect(); + networkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + networkCallback.assertNoCallback(); + mCm.unregisterNetworkCallback(networkCallback); + } + + @Test + public void testGetGlobalProxyForNetwork() throws Exception { + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + final Network wifiNetwork = mWiFiNetworkAgent.getNetwork(); + when(mService.mProxyTracker.getGlobalProxy()).thenReturn(testProxyInfo); + assertEquals(testProxyInfo, mService.getProxyForNetwork(wifiNetwork)); + } + + @Test + public void testGetProxyForActiveNetwork() throws Exception { + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertNull(mService.getProxyForNetwork(null)); + + final LinkProperties testLinkProperties = new LinkProperties(); + testLinkProperties.setHttpProxy(testProxyInfo); + + mWiFiNetworkAgent.sendLinkProperties(testLinkProperties); + waitForIdle(); + + assertEquals(testProxyInfo, mService.getProxyForNetwork(null)); + } + + @Test + public void testGetProxyForVPN() throws Exception { + final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888); + + // Set up a WiFi network with no proxy + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + assertNull(mService.getProxyForNetwork(null)); + + // Connect a VPN network with a proxy. + LinkProperties testLinkProperties = new LinkProperties(); + testLinkProperties.setHttpProxy(testProxyInfo); + mMockVpn.establishForMyUid(testLinkProperties); + assertUidRangesUpdatedForMyUid(true); + + // Test that the VPN network returns a proxy, and the WiFi does not. + assertEquals(testProxyInfo, mService.getProxyForNetwork(mMockVpn.getNetwork())); + assertEquals(testProxyInfo, mService.getProxyForNetwork(null)); + assertNull(mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork())); + + // Test that the VPN network returns no proxy when it is set to null. + testLinkProperties.setHttpProxy(null); + mMockVpn.sendLinkProperties(testLinkProperties); + waitForIdle(); + assertNull(mService.getProxyForNetwork(mMockVpn.getNetwork())); + assertNull(mService.getProxyForNetwork(null)); + + // Set WiFi proxy and check that the vpn proxy is still null. + testLinkProperties.setHttpProxy(testProxyInfo); + mWiFiNetworkAgent.sendLinkProperties(testLinkProperties); + waitForIdle(); + assertNull(mService.getProxyForNetwork(null)); + + // Disconnect from VPN and check that the active network, which is now the WiFi, has the + // correct proxy setting. + mMockVpn.disconnect(); + waitForIdle(); + assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertEquals(testProxyInfo, mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork())); + assertEquals(testProxyInfo, mService.getProxyForNetwork(null)); + } + + @Test + public void testFullyRoutedVpnResultsInInterfaceFilteringRules() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE)); + // The uid range needs to cover the test app so the network is visible to it. + final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, VPN_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, VPN_UID); + + // A connected VPN should have interface rules set up. There are two expected invocations, + // one during the VPN initial connection, one during the VPN LinkProperties update. + ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class); + verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID); + assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID); + assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange)); + + mMockVpn.disconnect(); + waitForIdle(); + + // Disconnected VPN should have interface rules removed + verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0")); + } + + @Test + public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + // The uid range needs to cover the test app so the network is visible to it. + final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID); + + // Legacy VPN should not have interface rules set up + verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any()); + } + + @Test + public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule() + throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0")); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE)); + // The uid range needs to cover the test app so the network is visible to it. + final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID); + + // IPv6 unreachable route should not be misinterpreted as a default route + verify(mMockNetd, never()).firewallAddUidInterfaceRules(any(), any()); + } + + @Test + public void testVpnHandoverChangesInterfaceFilteringRule() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + // The uid range needs to cover the test app so the network is visible to it. + final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.establish(lp, VPN_UID, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, VPN_UID); + + // Connected VPN should have interface rules set up. There are two expected invocations, + // one during VPN uid update, one during VPN LinkProperties update + ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class); + verify(mMockNetd, times(2)).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID); + assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID); + + reset(mMockNetd); + InOrder inOrder = inOrder(mMockNetd); + lp.setInterfaceName("tun1"); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // VPN handover (switch to a new interface) should result in rules being updated (old rules + // removed first, then new rules added) + inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + + reset(mMockNetd); + lp = new LinkProperties(); + lp.setInterfaceName("tun1"); + lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun1")); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // VPN not routing everything should no longer have interface filtering rules + verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + + reset(mMockNetd); + lp = new LinkProperties(); + lp.setInterfaceName("tun1"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + mMockVpn.sendLinkProperties(lp); + waitForIdle(); + // Back to routing all IPv6 traffic should have filtering rules + verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun1"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + } + + @Test + public void testUidUpdateChangesInterfaceFilteringRule() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), RTN_UNREACHABLE)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + // The uid range needs to cover the test app so the network is visible to it. + final UidRange vpnRange = PRIMARY_UIDRANGE; + final Set<UidRange> vpnRanges = Collections.singleton(vpnRange); + mMockVpn.establish(lp, VPN_UID, vpnRanges); + assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); + + reset(mMockNetd); + InOrder inOrder = inOrder(mMockNetd); + + // Update to new range which is old range minus APP1, i.e. only APP2 + final Set<UidRange> newRanges = new HashSet<>(Arrays.asList( + new UidRange(vpnRange.start, APP1_UID - 1), + new UidRange(APP1_UID + 1, vpnRange.stop))); + mMockVpn.setUids(newRanges); + waitForIdle(); + + ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class); + // Verify old rules are removed before new rules are added + inOrder.verify(mMockNetd).firewallRemoveUidInterfaceRules(uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID); + inOrder.verify(mMockNetd).firewallAddUidInterfaceRules(eq("tun0"), uidCaptor.capture()); + assertContainsExactly(uidCaptor.getValue(), APP2_UID); + } + + @Test + public void testLinkPropertiesWithWakeOnLanForActiveNetwork() throws Exception { + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + + LinkProperties wifiLp = new LinkProperties(); + wifiLp.setInterfaceName(WIFI_WOL_IFNAME); + wifiLp.setWakeOnLanSupported(false); + + // Default network switch should update ifaces. + mWiFiNetworkAgent.connect(false); + mWiFiNetworkAgent.sendLinkProperties(wifiLp); + waitForIdle(); + + // ConnectivityService should have changed the WakeOnLanSupported to true + wifiLp.setWakeOnLanSupported(true); + assertEquals(wifiLp, mService.getActiveLinkProperties()); + } + + @Test + public void testLegacyExtraInfoSentToNetworkMonitor() throws Exception { + class TestNetworkAgent extends NetworkAgent { + TestNetworkAgent(Context context, Looper looper, NetworkAgentConfig config) { + super(context, looper, "MockAgent", new NetworkCapabilities(), + new LinkProperties(), 40 , config, null /* provider */); + } + } + final NetworkAgent naNoExtraInfo = new TestNetworkAgent( + mServiceContext, mCsHandlerThread.getLooper(), new NetworkAgentConfig()); + naNoExtraInfo.register(); + verify(mNetworkStack).makeNetworkMonitor(any(), isNull(String.class), any()); + naNoExtraInfo.unregister(); + + reset(mNetworkStack); + final NetworkAgentConfig config = + new NetworkAgentConfig.Builder().setLegacyExtraInfo("legacyinfo").build(); + final NetworkAgent naExtraInfo = new TestNetworkAgent( + mServiceContext, mCsHandlerThread.getLooper(), config); + naExtraInfo.register(); + verify(mNetworkStack).makeNetworkMonitor(any(), eq("legacyinfo"), any()); + naExtraInfo.unregister(); + } + + // To avoid granting location permission bypass. + private void denyAllLocationPrivilegedPermissions() { + mServiceContext.setPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + mServiceContext.setPermission(Manifest.permission.NETWORK_STACK, + PERMISSION_DENIED); + mServiceContext.setPermission(Manifest.permission.NETWORK_SETUP_WIZARD, + PERMISSION_DENIED); + } + + private void setupLocationPermissions( + int targetSdk, boolean locationToggle, String op, String perm) throws Exception { + denyAllLocationPrivilegedPermissions(); + + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = targetSdk; + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), any())) + .thenReturn(applicationInfo); + when(mPackageManager.getTargetSdkVersion(any())).thenReturn(targetSdk); + + when(mLocationManager.isLocationEnabledForUser(any())).thenReturn(locationToggle); + + if (op != null) { + when(mAppOpsManager.noteOp(eq(op), eq(Process.myUid()), + eq(mContext.getPackageName()), eq(getAttributionTag()), anyString())) + .thenReturn(AppOpsManager.MODE_ALLOWED); + } + + if (perm != null) { + mServiceContext.setPermission(perm, PERMISSION_GRANTED); + } + } + + private int getOwnerUidNetCapsPermission(int ownerUid, int callerUid, + boolean includeLocationSensitiveInfo) { + final NetworkCapabilities netCap = new NetworkCapabilities().setOwnerUid(ownerUid); + + return mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, includeLocationSensitiveInfo, Process.myUid(), callerUid, + mContext.getPackageName(), getAttributionTag()) + .getOwnerUid(); + } + + private void verifyTransportInfoCopyNetCapsPermission( + int callerUid, boolean includeLocationSensitiveInfo, + boolean shouldMakeCopyWithLocationSensitiveFieldsParcelable) { + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()).thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, includeLocationSensitiveInfo, Process.myPid(), callerUid, + mContext.getPackageName(), getAttributionTag()); + if (shouldMakeCopyWithLocationSensitiveFieldsParcelable) { + verify(transportInfo).makeCopy(REDACT_NONE); + } else { + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION); + } + } + + private void verifyOwnerUidAndTransportInfoNetCapsPermission( + boolean shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag, + boolean shouldInclLocationSensitiveOwnerUidWithIncludeFlag, + boolean shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag, + boolean shouldInclLocationSensitiveTransportInfoWithIncludeFlag) { + final int myUid = Process.myUid(); + + final int expectedOwnerUidWithoutIncludeFlag = + shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag + ? myUid : INVALID_UID; + assertEquals(expectedOwnerUidWithoutIncludeFlag, getOwnerUidNetCapsPermission( + myUid, myUid, false /* includeLocationSensitiveInfo */)); + + final int expectedOwnerUidWithIncludeFlag = + shouldInclLocationSensitiveOwnerUidWithIncludeFlag ? myUid : INVALID_UID; + assertEquals(expectedOwnerUidWithIncludeFlag, getOwnerUidNetCapsPermission( + myUid, myUid, true /* includeLocationSensitiveInfo */)); + + verifyTransportInfoCopyNetCapsPermission(myUid, + false, /* includeLocationSensitiveInfo */ + shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag); + + verifyTransportInfoCopyNetCapsPermission(myUid, + true, /* includeLocationSensitiveInfo */ + shouldInclLocationSensitiveTransportInfoWithIncludeFlag); + + } + + private void verifyOwnerUidAndTransportInfoNetCapsPermissionPreS() { + verifyOwnerUidAndTransportInfoNetCapsPermission( + // Ensure that owner uid is included even if the request asks to remove it (which is + // the default) since the app has necessary permissions and targetSdk < S. + true, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */ + true, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */ + // Ensure that location info is removed if the request asks to remove it even if the + // app has necessary permissions. + false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */ + true /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */ + ); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithFineLocationAfterQPreS() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermissionPreS(); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithFineLocationPreSWithAndWithoutCallbackFlag() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.R, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermissionPreS(); + } + + @Test + public void + testCreateWithLocationInfoSanitizedWithFineLocationAfterSWithAndWithoutCallbackFlag() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermission( + // Ensure that the owner UID is removed if the request asks us to remove it even + // if the app has necessary permissions since targetSdk >= S. + false, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */ + true, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */ + // Ensure that location info is removed if the request asks to remove it even if the + // app has necessary permissions. + false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */ + true /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */ + ); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithCoarseLocationPreQ() + throws Exception { + setupLocationPermissions(Build.VERSION_CODES.P, true, AppOpsManager.OPSTR_COARSE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsPermissionPreS(); + } + + private void verifyOwnerUidAndTransportInfoNetCapsNotIncluded() { + verifyOwnerUidAndTransportInfoNetCapsPermission( + false, /* shouldInclLocationSensitiveOwnerUidWithoutIncludeFlag */ + false, /* shouldInclLocationSensitiveOwnerUidWithIncludeFlag */ + false, /* shouldInclLocationSensitiveTransportInfoWithoutIncludeFlag */ + false /* shouldInclLocationSensitiveTransportInfoWithIncludeFlag */ + ); + } + + @Test + public void testCreateWithLocationInfoSanitizedLocationOff() throws Exception { + // Test that even with fine location permission, and UIDs matching, the UID is sanitized. + setupLocationPermissions(Build.VERSION_CODES.Q, false, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsNotIncluded(); + } + + @Test + public void testCreateWithLocationInfoSanitizedWrongUid() throws Exception { + // Test that even with fine location permission, not being the owner leads to sanitization. + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + final int myUid = Process.myUid(); + assertEquals(Process.INVALID_UID, + getOwnerUidNetCapsPermission(myUid + 1, myUid, + true /* includeLocationSensitiveInfo */)); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithCoarseLocationAfterQ() + throws Exception { + // Test that not having fine location permission leads to sanitization. + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_COARSE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsNotIncluded(); + } + + @Test + public void testCreateWithLocationInfoSanitizedWithCoarseLocationAfterS() + throws Exception { + // Test that not having fine location permission leads to sanitization. + setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_COARSE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION); + + verifyOwnerUidAndTransportInfoNetCapsNotIncluded(); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithLocalMacAddressPermission() + throws Exception { + mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_GRANTED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // don't redact MAC_ADDRESS fields, only location sensitive fields. + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithoutLocalMacAddressPermission() + throws Exception { + mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_DENIED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // redact both MAC_ADDRESS & location sensitive fields. + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION + | REDACT_FOR_LOCAL_MAC_ADDRESS); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithSettingsPermission() + throws Exception { + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // don't redact NETWORK_SETTINGS fields, only location sensitive fields. + verify(transportInfo).makeCopy(REDACT_FOR_ACCESS_FINE_LOCATION); + } + + @Test + public void testCreateForCallerWithLocalMacAddressSanitizedWithoutSettingsPermission() + throws Exception { + mServiceContext.setPermission(Manifest.permission.LOCAL_MAC_ADDRESS, PERMISSION_DENIED); + + final TransportInfo transportInfo = mock(TransportInfo.class); + when(transportInfo.getApplicableRedactions()) + .thenReturn(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS); + final NetworkCapabilities netCap = + new NetworkCapabilities().setTransportInfo(transportInfo); + + mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + netCap, false /* includeLocationSensitiveInfoInTransportInfo */, + Process.myPid(), Process.myUid(), + mContext.getPackageName(), getAttributionTag()); + // redact both NETWORK_SETTINGS & location sensitive fields. + verify(transportInfo).makeCopy( + REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_NETWORK_SETTINGS); + } + + /** + * Test TransportInfo to verify redaction mechanism. + */ + private static class TestTransportInfo implements TransportInfo { + public final boolean locationRedacted; + public final boolean localMacAddressRedacted; + public final boolean settingsRedacted; + + TestTransportInfo() { + locationRedacted = false; + localMacAddressRedacted = false; + settingsRedacted = false; + } + + TestTransportInfo(boolean locationRedacted, boolean localMacAddressRedacted, + boolean settingsRedacted) { + this.locationRedacted = locationRedacted; + this.localMacAddressRedacted = + localMacAddressRedacted; + this.settingsRedacted = settingsRedacted; + } + + @Override + public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) { + return new TestTransportInfo( + locationRedacted | (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0, + localMacAddressRedacted | (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0, + settingsRedacted | (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0 + ); + } + + @Override + public @NetworkCapabilities.RedactionType long getApplicableRedactions() { + return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS + | REDACT_FOR_NETWORK_SETTINGS; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof TestTransportInfo)) return false; + TestTransportInfo that = (TestTransportInfo) other; + return that.locationRedacted == this.locationRedacted + && that.localMacAddressRedacted == this.localMacAddressRedacted + && that.settingsRedacted == this.settingsRedacted; + } + + @Override + public int hashCode() { + return Objects.hash(locationRedacted, localMacAddressRedacted, settingsRedacted); + } + + @Override + public String toString() { + return String.format( + "TestTransportInfo{locationRedacted=%s macRedacted=%s settingsRedacted=%s}", + locationRedacted, localMacAddressRedacted, settingsRedacted); + } + } + + private TestTransportInfo getTestTransportInfo(NetworkCapabilities nc) { + return (TestTransportInfo) nc.getTransportInfo(); + } + + private TestTransportInfo getTestTransportInfo(TestNetworkAgentWrapper n) { + final NetworkCapabilities nc = mCm.getNetworkCapabilities(n.getNetwork()); + assertNotNull(nc); + return getTestTransportInfo(nc); + } + + + private void verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps( + @NonNull TestNetworkCallback wifiNetworkCallback, int actualOwnerUid, + @NonNull TransportInfo actualTransportInfo, int expectedOwnerUid, + @NonNull TransportInfo expectedTransportInfo) throws Exception { + when(mPackageManager.getTargetSdkVersion(anyString())).thenReturn(Build.VERSION_CODES.S); + final NetworkCapabilities ncTemplate = + new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .setOwnerUid(actualOwnerUid); + + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), + ncTemplate); + mWiFiNetworkAgent.connect(false); + + wifiNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + + // Send network capabilities update with TransportInfo to trigger capabilities changed + // callback. + mWiFiNetworkAgent.setNetworkCapabilities( + ncTemplate.setTransportInfo(actualTransportInfo), true); + + wifiNetworkCallback.expectCapabilitiesThat(mWiFiNetworkAgent, + nc -> Objects.equals(expectedOwnerUid, nc.getOwnerUid()) + && Objects.equals(expectedTransportInfo, nc.getTransportInfo())); + } + + @Test + public void testVerifyLocationDataIsNotIncludedWhenInclFlagNotSet() throws Exception { + final TestNetworkCallback wifiNetworkCallack = new TestNetworkCallback(); + final int ownerUid = Process.myUid(); + final TransportInfo transportInfo = new TestTransportInfo(); + // Even though the test uid holds privileged permissions, mask location fields since + // the callback did not explicitly opt-in to get location data. + final TransportInfo sanitizedTransportInfo = new TestTransportInfo( + true, /* locationRedacted */ + true, /* localMacAddressRedacted */ + true /* settingsRedacted */ + ); + // Should not expect location data since the callback does not set the flag for including + // location data. + verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps( + wifiNetworkCallack, ownerUid, transportInfo, INVALID_UID, sanitizedTransportInfo); + } + + @Test + public void testTransportInfoRedactionInSynchronousCalls() throws Exception { + final NetworkCapabilities ncTemplate = new NetworkCapabilities() + .addTransportType(TRANSPORT_WIFI) + .setTransportInfo(new TestTransportInfo()); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), + ncTemplate); + mWiFiNetworkAgent.connect(true /* validated; waits for callback */); + + // NETWORK_SETTINGS redaction is controlled by the NETWORK_SETTINGS permission + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted); + withPermission(NETWORK_SETTINGS, () -> { + assertFalse(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted); + }); + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted); + + // LOCAL_MAC_ADDRESS redaction is controlled by the LOCAL_MAC_ADDRESS permission + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted); + withPermission(LOCAL_MAC_ADDRESS, () -> { + assertFalse(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted); + }); + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted); + + // Synchronous getNetworkCapabilities calls never return unredacted location-sensitive + // information. + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted); + setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted); + denyAllLocationPrivilegedPermissions(); + assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted); + } + + private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType) + throws Exception { + final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE); + mMockVpn.setVpnType(vpnType); + mMockVpn.establish(new LinkProperties(), vpnOwnerUid, vpnRange); + assertVpnUidRangesUpdated(true, vpnRange, vpnOwnerUid); + + final UnderlyingNetworkInfo underlyingNetworkInfo = + new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList<String>()); + mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo); + when(mDeps.getConnectionOwnerUid(anyInt(), any(), any())).thenReturn(42); + } + + private void setupConnectionOwnerUidAsVpnApp(int vpnOwnerUid, @VpnManager.VpnType int vpnType) + throws Exception { + setupConnectionOwnerUid(vpnOwnerUid, vpnType); + + // Test as VPN app + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + mServiceContext.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_DENIED); + } + + private ConnectionInfo getTestConnectionInfo() throws Exception { + return new ConnectionInfo( + IPPROTO_TCP, + new InetSocketAddress(InetAddresses.parseNumericAddress("1.2.3.4"), 1234), + new InetSocketAddress(InetAddresses.parseNumericAddress("2.3.4.5"), 2345)); + } + + @Test + public void testGetConnectionOwnerUidPlatformVpn() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_PLATFORM); + + assertEquals(INVALID_UID, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceWrongUser() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid + 1, VpnManager.TYPE_VPN_SERVICE); + + assertEquals(INVALID_UID, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceDoesNotThrow() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUidAsVpnApp(myUid, VpnManager.TYPE_VPN_SERVICE); + + assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceNetworkStackDoesNotThrow() throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE); + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + @Test + public void testGetConnectionOwnerUidVpnServiceMainlineNetworkStackDoesNotThrow() + throws Exception { + final int myUid = Process.myUid(); + setupConnectionOwnerUid(myUid, VpnManager.TYPE_VPN_SERVICE); + mServiceContext.setPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, PERMISSION_GRANTED); + + assertEquals(42, mService.getConnectionOwnerUid(getTestConnectionInfo())); + } + + private static PackageInfo buildPackageInfo(boolean hasSystemPermission, int uid) { + final PackageInfo packageInfo = new PackageInfo(); + if (hasSystemPermission) { + packageInfo.requestedPermissions = new String[] { + CHANGE_NETWORK_STATE, CONNECTIVITY_USE_RESTRICTED_NETWORKS }; + packageInfo.requestedPermissionsFlags = new int[] { + REQUESTED_PERMISSION_GRANTED, REQUESTED_PERMISSION_GRANTED }; + } else { + packageInfo.requestedPermissions = new String[0]; + } + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.applicationInfo.privateFlags = 0; + packageInfo.applicationInfo.uid = UserHandle.getUid(UserHandle.USER_SYSTEM, + UserHandle.getAppId(uid)); + return packageInfo; + } + + @Test + public void testRegisterConnectivityDiagnosticsCallbackInvalidRequest() throws Exception { + final NetworkRequest request = + new NetworkRequest( + new NetworkCapabilities(), TYPE_ETHERNET, 0, NetworkRequest.Type.NONE); + try { + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, request, mContext.getPackageName()); + fail("registerConnectivityDiagnosticsCallback should throw on invalid NetworkRequest"); + } catch (IllegalArgumentException expected) { + } + } + + private void assertRouteInfoParcelMatches(RouteInfo route, RouteInfoParcel parcel) { + assertEquals(route.getDestination().toString(), parcel.destination); + assertEquals(route.getInterface(), parcel.ifName); + assertEquals(route.getMtu(), parcel.mtu); + + switch (route.getType()) { + case RouteInfo.RTN_UNICAST: + if (route.hasGateway()) { + assertEquals(route.getGateway().getHostAddress(), parcel.nextHop); + } else { + assertEquals(INetd.NEXTHOP_NONE, parcel.nextHop); + } + break; + case RouteInfo.RTN_UNREACHABLE: + assertEquals(INetd.NEXTHOP_UNREACHABLE, parcel.nextHop); + break; + case RouteInfo.RTN_THROW: + assertEquals(INetd.NEXTHOP_THROW, parcel.nextHop); + break; + default: + assertEquals(INetd.NEXTHOP_NONE, parcel.nextHop); + break; + } + } + + private void assertRoutesAdded(int netId, RouteInfo... routes) throws Exception { + ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class); + verify(mMockNetd, times(routes.length)).networkAddRouteParcel(eq(netId), captor.capture()); + for (int i = 0; i < routes.length; i++) { + assertRouteInfoParcelMatches(routes[i], captor.getAllValues().get(i)); + } + } + + private void assertRoutesRemoved(int netId, RouteInfo... routes) throws Exception { + ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class); + verify(mMockNetd, times(routes.length)).networkRemoveRouteParcel(eq(netId), + captor.capture()); + for (int i = 0; i < routes.length; i++) { + assertRouteInfoParcelMatches(routes[i], captor.getAllValues().get(i)); + } + } + + @Test + public void testRegisterUnregisterConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest wifiRequest = + new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + verify(mIBinder).linkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt()); + verify(mConnectivityDiagnosticsCallback).asBinder(); + assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + + mService.unregisterConnectivityDiagnosticsCallback(mConnectivityDiagnosticsCallback); + verify(mIBinder, timeout(TIMEOUT_MS)) + .unlinkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt()); + assertFalse(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + verify(mConnectivityDiagnosticsCallback, atLeastOnce()).asBinder(); + } + + @Test + public void testRegisterDuplicateConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest wifiRequest = + new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + verify(mIBinder).linkToDeath(any(ConnectivityDiagnosticsCallbackInfo.class), anyInt()); + verify(mConnectivityDiagnosticsCallback).asBinder(); + assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + + // Register the same callback again + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, wifiRequest, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + assertTrue(mService.mConnectivityDiagnosticsCallbacks.containsKey(mIBinder)); + } + + public NetworkAgentInfo fakeMobileNai(NetworkCapabilities nc) { + final NetworkInfo info = new NetworkInfo(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE, + ConnectivityManager.getNetworkTypeName(TYPE_MOBILE), + TelephonyManager.getNetworkTypeName(TelephonyManager.NETWORK_TYPE_LTE)); + return new NetworkAgentInfo(null, new Network(NET_ID), info, new LinkProperties(), + nc, new NetworkScore.Builder().setLegacyInt(0).build(), + mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0, + INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker, + new ConnectivityService.Dependencies()); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsNetworkStack() throws Exception { + final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities()); + + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + assertTrue( + "NetworkStack permission not applied", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception { + final int wrongUid = Process.myUid() + 1; + + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setAdministratorUids(new int[] {wrongUid}); + final NetworkAgentInfo naiWithUid = fakeMobileNai(nc); + + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + assertFalse( + "Mismatched uid/package name should not pass the location permission check", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsNoLocationPermission() throws Exception { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setAdministratorUids(new int[] {Process.myUid()}); + final NetworkAgentInfo naiWithUid = fakeMobileNai(nc); + + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + assertFalse( + "ACCESS_FINE_LOCATION permission necessary for Connectivity Diagnostics", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsActiveVpn() throws Exception { + final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities()); + + mMockVpn.establishForMyUid(); + assertUidRangesUpdatedForMyUid(true); + + // Wait for networks to connect and broadcasts to be sent before removing permissions. + waitForIdle(); + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + + assertTrue(mMockVpn.setUnderlyingNetworks(new Network[] {naiWithoutUid.network})); + waitForIdle(); + assertTrue( + "Active VPN permission not applied", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + + assertTrue(mMockVpn.setUnderlyingNetworks(null)); + waitForIdle(); + assertFalse( + "VPN shouldn't receive callback on non-underlying network", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithoutUid, + mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsNetworkAdministrator() throws Exception { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setAdministratorUids(new int[] {Process.myUid()}); + final NetworkAgentInfo naiWithUid = fakeMobileNai(nc); + + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + assertTrue( + "NetworkCapabilities administrator uid permission not applied", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName())); + } + + @Test + public void testCheckConnectivityDiagnosticsPermissionsFails() throws Exception { + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setOwnerUid(Process.myUid()); + nc.setAdministratorUids(new int[] {Process.myUid()}); + final NetworkAgentInfo naiWithUid = fakeMobileNai(nc); + + setupLocationPermissions(Build.VERSION_CODES.Q, true, AppOpsManager.OPSTR_FINE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION); + mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED); + + // Use wrong pid and uid + assertFalse( + "Permissions allowed when they shouldn't be granted", + mService.checkConnectivityDiagnosticsPermissions( + Process.myPid() + 1, Process.myUid() + 1, naiWithUid, + mContext.getOpPackageName())); + } + + @Test + public void testRegisterConnectivityDiagnosticsCallbackCallsOnConnectivityReport() + throws Exception { + // Set up the Network, which leads to a ConnectivityReport being cached for the network. + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + final LinkProperties linkProperties = new LinkProperties(); + linkProperties.setInterfaceName(INTERFACE_NAME); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, linkProperties); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + callback.assertNoCallback(); + + final NetworkRequest request = new NetworkRequest.Builder().build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, request, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + verify(mConnectivityDiagnosticsCallback) + .onConnectivityReportAvailable(argThat(report -> { + return INTERFACE_NAME.equals(report.getLinkProperties().getInterfaceName()) + && report.getNetworkCapabilities().hasTransport(TRANSPORT_CELLULAR); + })); + } + + private void setUpConnectivityDiagnosticsCallback() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + when(mConnectivityDiagnosticsCallback.asBinder()).thenReturn(mIBinder); + + mServiceContext.setPermission( + android.Manifest.permission.NETWORK_STACK, PERMISSION_GRANTED); + + mService.registerConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback, request, mContext.getPackageName()); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Connect the cell agent verify that it notifies TestNetworkCallback that it is available + final TestNetworkCallback callback = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + + final NetworkCapabilities ncTemplate = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .setTransportInfo(new TestTransportInfo()); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), + ncTemplate); + mCellNetworkAgent.connect(true); + callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + callback.assertNoCallback(); + } + + private boolean areConnDiagCapsRedacted(NetworkCapabilities nc) { + TestTransportInfo ti = (TestTransportInfo) nc.getTransportInfo(); + return nc.getUids() == null + && nc.getAdministratorUids().length == 0 + && nc.getOwnerUid() == Process.INVALID_UID + && getTestTransportInfo(nc).locationRedacted + && getTestTransportInfo(nc).localMacAddressRedacted + && getTestTransportInfo(nc).settingsRedacted; + } + + @Test + public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable() + throws Exception { + setUpConnectivityDiagnosticsCallback(); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Verify onConnectivityReport fired + verify(mConnectivityDiagnosticsCallback).onConnectivityReportAvailable( + argThat(report -> areConnDiagCapsRedacted(report.getNetworkCapabilities()))); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnDataStallSuspected() throws Exception { + setUpConnectivityDiagnosticsCallback(); + + // Trigger notifyDataStallSuspected() on the INetworkMonitorCallbacks instance in the + // cellular network agent + mCellNetworkAgent.notifyDataStallSuspected(); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Verify onDataStallSuspected fired + verify(mConnectivityDiagnosticsCallback).onDataStallSuspected( + argThat(report -> areConnDiagCapsRedacted(report.getNetworkCapabilities()))); + } + + @Test + public void testConnectivityDiagnosticsCallbackOnConnectivityReported() throws Exception { + setUpConnectivityDiagnosticsCallback(); + + final Network n = mCellNetworkAgent.getNetwork(); + final boolean hasConnectivity = true; + mService.reportNetworkConnectivity(n, hasConnectivity); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Verify onNetworkConnectivityReported fired + verify(mConnectivityDiagnosticsCallback) + .onNetworkConnectivityReported(eq(n), eq(hasConnectivity)); + + final boolean noConnectivity = false; + mService.reportNetworkConnectivity(n, noConnectivity); + + // Block until all other events are done processing. + HandlerUtils.waitForIdle(mCsHandlerThread, TIMEOUT_MS); + + // Wait for onNetworkConnectivityReported to fire + verify(mConnectivityDiagnosticsCallback) + .onNetworkConnectivityReported(eq(n), eq(noConnectivity)); + } + + @Test + public void testRouteAddDeleteUpdate() throws Exception { + final NetworkRequest request = new NetworkRequest.Builder().build(); + final TestNetworkCallback networkCallback = new TestNetworkCallback(); + mCm.registerNetworkCallback(request, networkCallback); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + reset(mMockNetd); + mCellNetworkAgent.connect(false); + networkCallback.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + final int netId = mCellNetworkAgent.getNetwork().netId; + + final String iface = "rmnet_data0"; + final InetAddress gateway = InetAddress.getByName("fe80::5678"); + RouteInfo direct = RouteInfo.makeHostRoute(gateway, iface); + RouteInfo rio1 = new RouteInfo(new IpPrefix("2001:db8:1::/48"), gateway, iface); + RouteInfo rio2 = new RouteInfo(new IpPrefix("2001:db8:2::/48"), gateway, iface); + RouteInfo defaultRoute = new RouteInfo((IpPrefix) null, gateway, iface); + RouteInfo defaultWithMtu = new RouteInfo(null, gateway, iface, RouteInfo.RTN_UNICAST, + 1280 /* mtu */); + + // Send LinkProperties and check that we ask netd to add routes. + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(iface); + lp.addRoute(direct); + lp.addRoute(rio1); + lp.addRoute(defaultRoute); + mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, x -> x.getRoutes().size() == 3); + + assertRoutesAdded(netId, direct, rio1, defaultRoute); + reset(mMockNetd); + + // Send updated LinkProperties and check that we ask netd to add, remove, update routes. + assertTrue(lp.getRoutes().contains(defaultRoute)); + lp.removeRoute(rio1); + lp.addRoute(rio2); + lp.addRoute(defaultWithMtu); + // Ensure adding the same route with a different MTU replaces the previous route. + assertFalse(lp.getRoutes().contains(defaultRoute)); + assertTrue(lp.getRoutes().contains(defaultWithMtu)); + + mCellNetworkAgent.sendLinkProperties(lp); + networkCallback.expectLinkPropertiesThat(mCellNetworkAgent, + x -> x.getRoutes().contains(rio2)); + + assertRoutesRemoved(netId, rio1); + assertRoutesAdded(netId, rio2); + + ArgumentCaptor<RouteInfoParcel> captor = ArgumentCaptor.forClass(RouteInfoParcel.class); + verify(mMockNetd).networkUpdateRouteParcel(eq(netId), captor.capture()); + assertRouteInfoParcelMatches(defaultWithMtu, captor.getValue()); + + + mCm.unregisterNetworkCallback(networkCallback); + } + + @Test + public void testDumpDoesNotCrash() { + mServiceContext.setPermission(DUMP, PERMISSION_GRANTED); + // Filing a couple requests prior to testing the dump. + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + final StringWriter stringWriter = new StringWriter(); + + mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]); + + assertFalse(stringWriter.toString().isEmpty()); + } + + @Test + public void testRequestsSortedByIdSortsCorrectly() { + final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback(); + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest genericRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + final NetworkRequest wifiRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).build(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(genericRequest, genericNetworkCallback); + mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + waitForIdle(); + + final ConnectivityService.NetworkRequestInfo[] nriOutput = mService.requestsSortedById(); + + assertTrue(nriOutput.length > 1); + for (int i = 0; i < nriOutput.length - 1; i++) { + final boolean isRequestIdInOrder = + nriOutput[i].mRequests.get(0).requestId + < nriOutput[i + 1].mRequests.get(0).requestId; + assertTrue(isRequestIdInOrder); + } + } + + private void assertUidRangesUpdatedForMyUid(boolean add) throws Exception { + final int uid = Process.myUid(); + assertVpnUidRangesUpdated(add, uidRangesForUids(uid), uid); + } + + private void assertVpnUidRangesUpdated(boolean add, Set<UidRange> vpnRanges, int exemptUid) + throws Exception { + InOrder inOrder = inOrder(mMockNetd); + ArgumentCaptor<int[]> exemptUidCaptor = ArgumentCaptor.forClass(int[].class); + + inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)), + exemptUidCaptor.capture()); + assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid); + + if (add) { + inOrder.verify(mMockNetd, times(1)) + .networkAddUidRanges(eq(mMockVpn.getNetwork().getNetId()), + eq(toUidRangeStableParcels(vpnRanges))); + } else { + inOrder.verify(mMockNetd, times(1)) + .networkRemoveUidRanges(eq(mMockVpn.getNetwork().getNetId()), + eq(toUidRangeStableParcels(vpnRanges))); + } + + inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)), + exemptUidCaptor.capture()); + assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid); + } + + @Test + public void testVpnUidRangesUpdate() throws Exception { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName("tun0"); + lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null)); + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null)); + final UidRange vpnRange = PRIMARY_UIDRANGE; + Set<UidRange> vpnRanges = Collections.singleton(vpnRange); + mMockVpn.establish(lp, VPN_UID, vpnRanges); + assertVpnUidRangesUpdated(true, vpnRanges, VPN_UID); + + reset(mMockNetd); + // Update to new range which is old range minus APP1, i.e. only APP2 + final Set<UidRange> newRanges = new HashSet<>(Arrays.asList( + new UidRange(vpnRange.start, APP1_UID - 1), + new UidRange(APP1_UID + 1, vpnRange.stop))); + mMockVpn.setUids(newRanges); + waitForIdle(); + + assertVpnUidRangesUpdated(true, newRanges, VPN_UID); + assertVpnUidRangesUpdated(false, vpnRanges, VPN_UID); + } + + @Test + public void testInvalidRequestTypes() { + final int[] invalidReqTypeInts = new int[]{-1, NetworkRequest.Type.NONE.ordinal(), + NetworkRequest.Type.LISTEN.ordinal(), NetworkRequest.Type.values().length}; + final NetworkCapabilities nc = new NetworkCapabilities().addTransportType(TRANSPORT_WIFI); + + for (int reqTypeInt : invalidReqTypeInts) { + assertThrows("Expect throws for invalid request type " + reqTypeInt, + IllegalArgumentException.class, + () -> mService.requestNetwork(Process.INVALID_UID, nc, reqTypeInt, null, 0, + null, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE, + mContext.getPackageName(), getAttributionTag()) + ); + } + } + + @Test + public void testKeepConnected() throws Exception { + setAlwaysOnNetworks(false); + registerDefaultNetworkCallbacks(); + final TestNetworkCallback allNetworksCb = new TestNetworkCallback(); + final NetworkRequest allNetworksRequest = new NetworkRequest.Builder().clearCapabilities() + .build(); + mCm.registerNetworkCallback(allNetworksRequest, allNetworksCb); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true /* validated */); + + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true /* validated */); + + mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + // While the default callback doesn't see the network before it's validated, the listen + // sees the network come up and validate later + allNetworksCb.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent); + allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent); + allNetworksCb.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent); + allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, + TEST_LINGER_DELAY_MS * 2); + + // The cell network has disconnected (see LOST above) because it was outscored and + // had no requests (see setAlwaysOnNetworks(false) above) + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + final NetworkScore score = new NetworkScore.Builder().setLegacyInt(30).build(); + mCellNetworkAgent.setScore(score); + mCellNetworkAgent.connect(false /* validated */); + + // The cell network gets torn down right away. + allNetworksCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, + TEST_NASCENT_DELAY_MS * 2); + allNetworksCb.assertNoCallback(); + + // Now create a cell network with KEEP_CONNECTED_FOR_HANDOVER and make sure it's + // not disconnected immediately when outscored. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + final NetworkScore scoreKeepup = new NetworkScore.Builder().setLegacyInt(30) + .setKeepConnectedReason(KEEP_CONNECTED_FOR_HANDOVER).build(); + mCellNetworkAgent.setScore(scoreKeepup); + mCellNetworkAgent.connect(true /* validated */); + + allNetworksCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.assertNoCallback(); + + mWiFiNetworkAgent.disconnect(); + + allNetworksCb.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent); + mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + + // Reconnect a WiFi network and make sure the cell network is still not torn down. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true /* validated */); + + allNetworksCb.expectAvailableThenValidatedCallbacks(mWiFiNetworkAgent); + mDefaultNetworkCallback.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Now remove the reason to keep connected and make sure the network lingers and is + // torn down. + mCellNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).build()); + allNetworksCb.expectCallback(CallbackEntry.LOSING, mCellNetworkAgent, + TEST_NASCENT_DELAY_MS * 2); + allNetworksCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent, + TEST_LINGER_DELAY_MS * 2); + mDefaultNetworkCallback.assertNoCallback(); + + mCm.unregisterNetworkCallback(allNetworksCb); + // mDefaultNetworkCallback will be unregistered by tearDown() + } + + private class QosCallbackMockHelper { + @NonNull public final QosFilter mFilter; + @NonNull public final IQosCallback mCallback; + @NonNull public final TestNetworkAgentWrapper mAgentWrapper; + @NonNull private final List<IQosCallback> mCallbacks = new ArrayList(); + + QosCallbackMockHelper() throws Exception { + Log.d(TAG, "QosCallbackMockHelper: "); + mFilter = mock(QosFilter.class); + + // Ensure the network is disconnected before anything else occurs + assertNull(mCellNetworkAgent); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + verifyActiveNetwork(TRANSPORT_CELLULAR); + waitForIdle(); + final Network network = mCellNetworkAgent.getNetwork(); + + final Pair<IQosCallback, IBinder> pair = createQosCallback(); + mCallback = pair.first; + + when(mFilter.getNetwork()).thenReturn(network); + when(mFilter.validate()).thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mAgentWrapper = mCellNetworkAgent; + } + + void registerQosCallback(@NonNull final QosFilter filter, + @NonNull final IQosCallback callback) { + mCallbacks.add(callback); + final NetworkAgentInfo nai = + mService.getNetworkAgentInfoForNetwork(filter.getNetwork()); + mService.registerQosCallbackInternal(filter, callback, nai); + } + + void tearDown() { + for (int i = 0; i < mCallbacks.size(); i++) { + mService.unregisterQosCallback(mCallbacks.get(i)); + } + } + } + + private Pair<IQosCallback, IBinder> createQosCallback() { + final IQosCallback callback = mock(IQosCallback.class); + final IBinder binder = mock(Binder.class); + when(callback.asBinder()).thenReturn(binder); + when(binder.isBinderAlive()).thenReturn(true); + return new Pair<>(callback, binder); + } + + + @Test + public void testQosCallbackRegistration() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + final NetworkAgentWrapper wrapper = mQosCallbackMockHelper.mAgentWrapper; + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + + final NetworkAgentWrapper.CallbackType.OnQosCallbackRegister cbRegister1 = + (NetworkAgentWrapper.CallbackType.OnQosCallbackRegister) + wrapper.getCallbackHistory().poll(1000, x -> true); + assertNotNull(cbRegister1); + + final int registerCallbackId = cbRegister1.mQosCallbackId; + mService.unregisterQosCallback(mQosCallbackMockHelper.mCallback); + final NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister cbUnregister; + cbUnregister = (NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister) + wrapper.getCallbackHistory().poll(1000, x -> true); + assertNotNull(cbUnregister); + assertEquals(registerCallbackId, cbUnregister.mQosCallbackId); + assertNull(wrapper.getCallbackHistory().poll(200, x -> true)); + } + + @Test + public void testQosCallbackNoRegistrationOnValidationError() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + waitForIdle(); + verify(mQosCallbackMockHelper.mCallback) + .onError(eq(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED)); + } + + @Test + public void testQosCallbackAvailableAndLost() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + final int sessionId = 10; + final int qosCallbackId = 1; + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + waitForIdle(); + + final EpsBearerQosSessionAttributes attributes = new EpsBearerQosSessionAttributes( + 1, 2, 3, 4, 5, new ArrayList<>()); + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionAvailable(qosCallbackId, sessionId, attributes); + waitForIdle(); + + verify(mQosCallbackMockHelper.mCallback).onQosEpsBearerSessionAvailable(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_EPS_BEARER), eq(attributes)); + + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_EPS_BEARER); + waitForIdle(); + verify(mQosCallbackMockHelper.mCallback).onQosSessionLost(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_EPS_BEARER)); + } + + @Test + public void testNrQosCallbackAvailableAndLost() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + final int sessionId = 10; + final int qosCallbackId = 1; + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, mQosCallbackMockHelper.mCallback); + waitForIdle(); + + final NrQosSessionAttributes attributes = new NrQosSessionAttributes( + 1, 2, 3, 4, 5, 6, 7, new ArrayList<>()); + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionAvailable(qosCallbackId, sessionId, attributes); + waitForIdle(); + + verify(mQosCallbackMockHelper.mCallback).onNrQosSessionAvailable(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_NR_BEARER), eq(attributes)); + + mQosCallbackMockHelper.mAgentWrapper.getNetworkAgent() + .sendQosSessionLost(qosCallbackId, sessionId, QosSession.TYPE_NR_BEARER); + waitForIdle(); + verify(mQosCallbackMockHelper.mCallback).onQosSessionLost(argThat(session -> + session.getSessionId() == sessionId + && session.getSessionType() == QosSession.TYPE_NR_BEARER)); + } + + @Test + public void testQosCallbackTooManyRequests() throws Exception { + mQosCallbackMockHelper = new QosCallbackMockHelper(); + + when(mQosCallbackMockHelper.mFilter.validate()) + .thenReturn(QosCallbackException.EX_TYPE_FILTER_NONE); + for (int i = 0; i < 100; i++) { + final Pair<IQosCallback, IBinder> pair = createQosCallback(); + + try { + mQosCallbackMockHelper.registerQosCallback( + mQosCallbackMockHelper.mFilter, pair.first); + } catch (ServiceSpecificException e) { + assertEquals(e.errorCode, ConnectivityManager.Errors.TOO_MANY_REQUESTS); + if (i < 50) { + fail("TOO_MANY_REQUESTS thrown too early, the count is " + i); + } + + // As long as there is at least 50 requests, it is safe to assume it works. + // Note: The count isn't being tested precisely against 100 because the counter + // is shared with request network. + return; + } + } + fail("TOO_MANY_REQUESTS never thrown"); + } + + private UidRange createUidRange(int userId) { + return UidRange.createForUser(UserHandle.of(userId)); + } + + private void mockGetApplicationInfo(@NonNull final String packageName, @NonNull final int uid) { + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = uid; + try { + when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())) + .thenReturn(applicationInfo); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + private void mockGetApplicationInfoThrowsNameNotFound(@NonNull final String packageName) + throws Exception { + when(mPackageManager.getApplicationInfo(eq(packageName), anyInt())) + .thenThrow(new PackageManager.NameNotFoundException(packageName)); + } + + private void mockHasSystemFeature(@NonNull final String featureName, + @NonNull final boolean hasFeature) { + when(mPackageManager.hasSystemFeature(eq(featureName))) + .thenReturn(hasFeature); + } + + private Range<Integer> getNriFirstUidRange( + @NonNull final ConnectivityService.NetworkRequestInfo nri) { + return nri.mRequests.get(0).networkCapabilities.getUids().iterator().next(); + } + + private OemNetworkPreferences createDefaultOemNetworkPreferences( + @OemNetworkPreferences.OemNetworkPreference final int preference) { + // Arrange PackageManager mocks + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + + // Build OemNetworkPreferences object + return new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, preference) + .build(); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceUninitializedThrowsError() + throws PackageManager.NameNotFoundException { + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_UNINITIALIZED; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + assertThrows(IllegalArgumentException.class, + () -> mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest))); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPaid() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 3; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet<ConnectivityService.NetworkRequestInfo> nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List<NetworkRequest> mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isListen()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_NOT_METERED)); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_VALIDATED)); + assertTrue(mRequests.get(1).isRequest()); + assertTrue(mRequests.get(1).hasCapability(NET_CAPABILITY_OEM_PAID)); + assertEquals(NetworkRequest.Type.TRACK_DEFAULT, mRequests.get(2).type); + assertTrue(mService.getDefaultRequest().networkCapabilities.equalsNetCapabilities( + mRequests.get(2).networkCapabilities)); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPaidNoFallback() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 2; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet<ConnectivityService.NetworkRequestInfo> nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List<NetworkRequest> mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isListen()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_NOT_METERED)); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_VALIDATED)); + assertTrue(mRequests.get(1).isRequest()); + assertTrue(mRequests.get(1).hasCapability(NET_CAPABILITY_OEM_PAID)); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPaidOnly() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 1; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet<ConnectivityService.NetworkRequestInfo> nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List<NetworkRequest> mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isRequest()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PAID)); + } + + @Test + public void testOemNetworkRequestFactoryPreferenceOemPrivateOnly() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfRequests = 1; + + @OemNetworkPreferences.OemNetworkPreference final int prefToTest = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet<ConnectivityService.NetworkRequestInfo> nris = + mService.new OemNetworkRequestFactory() + .createNrisFromOemNetworkPreferences( + createDefaultOemNetworkPreferences(prefToTest)); + + final List<NetworkRequest> mRequests = nris.iterator().next().mRequests; + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfRequests, mRequests.size()); + assertTrue(mRequests.get(0).isRequest()); + assertTrue(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PRIVATE)); + assertFalse(mRequests.get(0).hasCapability(NET_CAPABILITY_OEM_PAID)); + } + + @Test + public void testOemNetworkRequestFactoryCreatesCorrectNumOfNris() + throws Exception { + // Expectations + final int expectedNumOfNris = 2; + + // Arrange PackageManager mocks + final String testPackageName2 = "com.google.apps.dialer"; + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfo(testPackageName2, TEST_PACKAGE_UID); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final int testOemPref2 = OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .addNetworkPreference(testPackageName2, testOemPref2) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet<ConnectivityService.NetworkRequestInfo> nris = + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref); + + assertNotNull(nris); + assertEquals(expectedNumOfNris, nris.size()); + } + + @Test + public void testOemNetworkRequestFactoryMultiplePrefsCorrectlySetsUids() + throws Exception { + // Arrange PackageManager mocks + final String testPackageName2 = "com.google.apps.dialer"; + final int testPackageNameUid2 = 456; + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfo(testPackageName2, testPackageNameUid2); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final int testOemPref2 = OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .addNetworkPreference(testPackageName2, testOemPref2) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final List<ConnectivityService.NetworkRequestInfo> nris = + new ArrayList<>( + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences( + pref)); + + // Sort by uid to access nris by index + nris.sort(Comparator.comparingInt(nri -> getNriFirstUidRange(nri).getLower())); + assertEquals(TEST_PACKAGE_UID, (int) getNriFirstUidRange(nris.get(0)).getLower()); + assertEquals(TEST_PACKAGE_UID, (int) getNriFirstUidRange(nris.get(0)).getUpper()); + assertEquals(testPackageNameUid2, (int) getNriFirstUidRange(nris.get(1)).getLower()); + assertEquals(testPackageNameUid2, (int) getNriFirstUidRange(nris.get(1)).getUpper()); + } + + @Test + public void testOemNetworkRequestFactoryMultipleUsersCorrectlySetsUids() + throws Exception { + // Arrange users + final int secondUser = 10; + final UserHandle secondUserHandle = new UserHandle(secondUser); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle)); + + // Arrange PackageManager mocks + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final List<ConnectivityService.NetworkRequestInfo> nris = + new ArrayList<>( + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences( + pref)); + + // UIDs for all users and all managed packages should be present. + // Two users each with two packages. + final int expectedUidSize = 2; + final List<Range<Integer>> uids = + new ArrayList<>(nris.get(0).mRequests.get(0).networkCapabilities.getUids()); + assertEquals(expectedUidSize, uids.size()); + + // Sort by uid to access nris by index + uids.sort(Comparator.comparingInt(uid -> uid.getLower())); + final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID); + assertEquals(TEST_PACKAGE_UID, (int) uids.get(0).getLower()); + assertEquals(TEST_PACKAGE_UID, (int) uids.get(0).getUpper()); + assertEquals(secondUserTestPackageUid, (int) uids.get(1).getLower()); + assertEquals(secondUserTestPackageUid, (int) uids.get(1).getUpper()); + } + + @Test + public void testOemNetworkRequestFactoryAddsPackagesToCorrectPreference() + throws Exception { + // Expectations + final int expectedNumOfNris = 1; + final int expectedNumOfAppUids = 2; + + // Arrange PackageManager mocks + final String testPackageName2 = "com.google.apps.dialer"; + final int testPackageNameUid2 = 456; + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfo(testPackageName2, testPackageNameUid2); + + // Build OemNetworkPreferences object + final int testOemPref = OEM_NETWORK_PREFERENCE_OEM_PAID; + final OemNetworkPreferences pref = new OemNetworkPreferences.Builder() + .addNetworkPreference(TEST_PACKAGE_NAME, testOemPref) + .addNetworkPreference(testPackageName2, testOemPref) + .build(); + + // Act on OemNetworkRequestFactory.createNrisFromOemNetworkPreferences() + final ArraySet<ConnectivityService.NetworkRequestInfo> nris = + mService.new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(pref); + + assertEquals(expectedNumOfNris, nris.size()); + assertEquals(expectedNumOfAppUids, + nris.iterator().next().mRequests.get(0).networkCapabilities.getUids().size()); + } + + @Test + public void testSetOemNetworkPreferenceNullListenerAndPrefParamThrowsNpe() { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true); + + // Act on ConnectivityService.setOemNetworkPreference() + assertThrows(NullPointerException.class, + () -> mService.setOemNetworkPreference( + null, + null)); + } + + @Test + public void testSetOemNetworkPreferenceFailsForNonAutomotive() + throws Exception { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, false); + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + // Act on ConnectivityService.setOemNetworkPreference() + assertThrows(UnsupportedOperationException.class, + () -> mService.setOemNetworkPreference( + createDefaultOemNetworkPreferences(networkPref), + new TestOemListenerCallback())); + } + + private void setOemNetworkPreferenceAgentConnected(final int transportType, + final boolean connectAgent) throws Exception { + switch(transportType) { + // Corresponds to a metered cellular network. Will be used for the default network. + case TRANSPORT_CELLULAR: + if (!connectAgent) { + mCellNetworkAgent.disconnect(); + break; + } + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED); + mCellNetworkAgent.connect(true); + break; + // Corresponds to a restricted ethernet network with OEM_PAID/OEM_PRIVATE. + case TRANSPORT_ETHERNET: + if (!connectAgent) { + stopOemManagedNetwork(); + break; + } + startOemManagedNetwork(true); + break; + // Corresponds to unmetered Wi-Fi. + case TRANSPORT_WIFI: + if (!connectAgent) { + mWiFiNetworkAgent.disconnect(); + break; + } + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.connect(true); + break; + default: + throw new AssertionError("Unsupported transport type passed in."); + + } + waitForIdle(); + } + + private void startOemManagedNetwork(final boolean isOemPaid) throws Exception { + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.addCapability( + isOemPaid ? NET_CAPABILITY_OEM_PAID : NET_CAPABILITY_OEM_PRIVATE); + mEthernetNetworkAgent.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + mEthernetNetworkAgent.connect(true); + } + + private void stopOemManagedNetwork() { + mEthernetNetworkAgent.disconnect(); + waitForIdle(); + } + + private void verifyMultipleDefaultNetworksTracksCorrectly( + final int expectedOemRequestsSize, + @NonNull final Network expectedDefaultNetwork, + @NonNull final Network expectedPerAppNetwork) { + // The current test setup assumes two tracked default network requests; one for the default + // network and the other for the OEM network preference being tested. This will be validated + // each time to confirm it doesn't change under test. + final int expectedDefaultNetworkRequestsSize = 2; + assertEquals(expectedDefaultNetworkRequestsSize, mService.mDefaultNetworkRequests.size()); + for (final ConnectivityService.NetworkRequestInfo defaultRequest + : mService.mDefaultNetworkRequests) { + final Network defaultNetwork = defaultRequest.getSatisfier() == null + ? null : defaultRequest.getSatisfier().network(); + // If this is the default request. + if (defaultRequest == mService.mDefaultRequest) { + assertEquals( + expectedDefaultNetwork, + defaultNetwork); + // Make sure this value doesn't change. + assertEquals(1, defaultRequest.mRequests.size()); + continue; + } + assertEquals(expectedPerAppNetwork, defaultNetwork); + assertEquals(expectedOemRequestsSize, defaultRequest.mRequests.size()); + } + verifyMultipleDefaultCallbacks(expectedDefaultNetwork, expectedPerAppNetwork); + } + + /** + * Verify default callbacks for 'available' fire as expected. This will only run if + * registerDefaultNetworkCallbacks() was executed prior and will only be different if the + * setOemNetworkPreference() per-app API was used for the current process. + * @param expectedSystemDefault the expected network for the system default. + * @param expectedPerAppDefault the expected network for the current process's default. + */ + private void verifyMultipleDefaultCallbacks( + @NonNull final Network expectedSystemDefault, + @NonNull final Network expectedPerAppDefault) { + if (null != mSystemDefaultNetworkCallback && null != expectedSystemDefault + && mService.mNoServiceNetwork.network() != expectedSystemDefault) { + // getLastAvailableNetwork() is used as this method can be called successively with + // the same network to validate therefore expectAvailableThenValidatedCallbacks + // can't be used. + assertEquals(mSystemDefaultNetworkCallback.getLastAvailableNetwork(), + expectedSystemDefault); + } + if (null != mDefaultNetworkCallback && null != expectedPerAppDefault + && mService.mNoServiceNetwork.network() != expectedPerAppDefault) { + assertEquals(mDefaultNetworkCallback.getLastAvailableNetwork(), + expectedPerAppDefault); + } + } + + private void registerDefaultNetworkCallbacks() { + // Using Manifest.permission.NETWORK_SETTINGS for registerSystemDefaultNetworkCallback() + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED); + mSystemDefaultNetworkCallback = new TestNetworkCallback(); + mDefaultNetworkCallback = new TestNetworkCallback(); + mProfileDefaultNetworkCallback = new TestNetworkCallback(); + mCm.registerSystemDefaultNetworkCallback(mSystemDefaultNetworkCallback, + new Handler(ConnectivityThread.getInstanceLooper())); + mCm.registerDefaultNetworkCallback(mDefaultNetworkCallback); + registerDefaultNetworkCallbackAsUid(mProfileDefaultNetworkCallback, + TEST_WORK_PROFILE_APP_UID); + // TODO: test using ConnectivityManager#registerDefaultNetworkCallbackAsUid as well. + mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_DENIED); + } + + private void unregisterDefaultNetworkCallbacks() { + if (null != mDefaultNetworkCallback) { + mCm.unregisterNetworkCallback(mDefaultNetworkCallback); + } + if (null != mSystemDefaultNetworkCallback) { + mCm.unregisterNetworkCallback(mSystemDefaultNetworkCallback); + } + if (null != mProfileDefaultNetworkCallback) { + mCm.unregisterNetworkCallback(mProfileDefaultNetworkCallback); + } + } + + private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup) + throws Exception { + final int testPackageNameUid = TEST_PACKAGE_UID; + final String testPackageName = "per.app.defaults.package"; + setupMultipleDefaultNetworksForOemNetworkPreferenceTest( + networkPrefToSetup, testPackageNameUid, testPackageName); + } + + private void setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup) + throws Exception { + final int testPackageNameUid = Process.myUid(); + final String testPackageName = "per.app.defaults.package"; + setupMultipleDefaultNetworksForOemNetworkPreferenceTest( + networkPrefToSetup, testPackageNameUid, testPackageName); + } + + private void setupMultipleDefaultNetworksForOemNetworkPreferenceTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup, + final int testPackageUid, @NonNull final String testPackageName) throws Exception { + // Only the default request should be included at start. + assertEquals(1, mService.mDefaultNetworkRequests.size()); + + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(testPackageUid)); + setupSetOemNetworkPreferenceForPreferenceTest( + networkPrefToSetup, uidRanges, testPackageName); + } + + private void setupSetOemNetworkPreferenceForPreferenceTest( + @OemNetworkPreferences.OemNetworkPreference final int networkPrefToSetup, + @NonNull final UidRangeParcel[] uidRanges, + @NonNull final String testPackageName) + throws Exception { + // These tests work off a single UID therefore using 'start' is valid. + mockGetApplicationInfo(testPackageName, uidRanges[0].start); + + setOemNetworkPreference(networkPrefToSetup, testPackageName); + } + + private void setOemNetworkPreference(final int networkPrefToSetup, + @NonNull final String... testPackageNames) + throws Exception { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true); + + // Build OemNetworkPreferences object + final OemNetworkPreferences.Builder builder = new OemNetworkPreferences.Builder(); + for (final String packageName : testPackageNames) { + builder.addNetworkPreference(packageName, networkPrefToSetup); + } + final OemNetworkPreferences pref = builder.build(); + + // Act on ConnectivityService.setOemNetworkPreference() + final TestOemListenerCallback oemPrefListener = new TestOemListenerCallback(); + mService.setOemNetworkPreference(pref, oemPrefListener); + + // Verify call returned successfully + oemPrefListener.expectOnComplete(); + } + + private static class TestOemListenerCallback implements IOnCompleteListener { + final CompletableFuture<Object> mDone = new CompletableFuture<>(); + + @Override + public void onComplete() { + mDone.complete(new Object()); + } + + void expectOnComplete() { + try { + mDone.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + fail("Expected onComplete() not received after " + TIMEOUT_MS + " ms"); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Override + public IBinder asBinder() { + return null; + } + } + + @Test + public void testMultiDefaultGetActiveNetworkIsCorrect() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active network for the default should be null at this point as this is a retricted + // network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // Verify that the active network is correct + verifyActiveNetwork(TRANSPORT_ETHERNET); + // default NCs will be unregistered in tearDown + } + + @Test + public void testMultiDefaultIsActiveNetworkMeteredIsCorrect() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Returns true by default when no network is available. + assertTrue(mCm.isActiveNetworkMetered()); + + // Connect to an unmetered restricted network that will only be available to the OEM pref. + mEthernetNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET); + mEthernetNetworkAgent.addCapability(NET_CAPABILITY_OEM_PAID); + mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mEthernetNetworkAgent.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + mEthernetNetworkAgent.connect(true); + waitForIdle(); + + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + assertFalse(mCm.isActiveNetworkMetered()); + // default NCs will be unregistered in tearDown + } + + @Test + public void testPerAppDefaultRegisterDefaultNetworkCallback() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + + // Register the default network callback before the pref is already set. This means that + // the policy will be applied to the callback on setOemNetworkPreference(). + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback(); + withPermission(NETWORK_SETTINGS, () -> + mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper()))); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active nai for the default is null at this point as this is a restricted network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // At this point with a restricted network used, the available callback should trigger. + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), + mEthernetNetworkAgent.getNetwork()); + otherUidDefaultCallback.assertNoCallback(); + + // Now bring down the default network which should trigger a LOST callback. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + + // At this point, with no network is available, the lost callback should trigger + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Confirm we can unregister without issues. + mCm.unregisterNetworkCallback(defaultNetworkCallback); + mCm.unregisterNetworkCallback(otherUidDefaultCallback); + } + + @Test + public void testPerAppDefaultRegisterDefaultNetworkCallbackAfterPrefSet() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + + // Setup the test process to use networkPref for their default network. + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + // Register the default network callback after the pref is already set. This means that + // the policy will be applied to the callback on requestNetwork(). + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback(); + withPermission(NETWORK_SETTINGS, () -> + mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper()))); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active nai for the default is null at this point as this is a restricted network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // At this point with a restricted network used, the available callback should trigger + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), + mEthernetNetworkAgent.getNetwork()); + otherUidDefaultCallback.assertNoCallback(); + + // Now bring down the default network which should trigger a LOST callback. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + otherUidDefaultCallback.assertNoCallback(); + + // At this point, with no network is available, the lost callback should trigger + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Confirm we can unregister without issues. + mCm.unregisterNetworkCallback(defaultNetworkCallback); + mCm.unregisterNetworkCallback(otherUidDefaultCallback); + } + + @Test + public void testPerAppDefaultRegisterDefaultNetworkCallbackDoesNotFire() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + final int expectedOemPrefRequestSize = 1; + final TestNetworkCallback defaultNetworkCallback = new TestNetworkCallback(); + final int userId = UserHandle.getUserId(Process.myUid()); + + mCm.registerDefaultNetworkCallback(defaultNetworkCallback); + defaultNetworkCallback.assertNoCallback(); + + final TestNetworkCallback otherUidDefaultCallback = new TestNetworkCallback(); + withPermission(NETWORK_SETTINGS, () -> + mCm.registerDefaultNetworkCallbackForUid(TEST_PACKAGE_UID, otherUidDefaultCallback, + new Handler(ConnectivityThread.getInstanceLooper()))); + + // Setup a process different than the test process to use the default network. This means + // that the defaultNetworkCallback won't be tracked by the per-app policy. + setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(networkPref); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + // The active nai for the default is null at this point as this is a restricted network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // As this callback does not have access to the OEM_PAID network, it will not fire. + defaultNetworkCallback.assertNoCallback(); + assertDefaultNetworkCapabilities(userId /* no networks */); + + // The other UID does have access, and gets a callback. + otherUidDefaultCallback.expectAvailableThenValidatedCallbacks(mEthernetNetworkAgent); + + // Bring up unrestricted cellular. This should now satisfy the default network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // At this point with an unrestricted network used, the available callback should trigger + // The other UID is unaffected and remains on the paid network. + defaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + assertEquals(defaultNetworkCallback.getLastAvailableNetwork(), + mCellNetworkAgent.getNetwork()); + assertDefaultNetworkCapabilities(userId, mCellNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Now bring down the per-app network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + + // Since the callback didn't use the per-app network, only the other UID gets a callback. + // Because the preference specifies no fallback, it does not switch to cellular. + defaultNetworkCallback.assertNoCallback(); + otherUidDefaultCallback.expectCallback(CallbackEntry.LOST, mEthernetNetworkAgent); + + // Now bring down the default network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + + // As this callback was tracking the default, this should now trigger. + defaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + otherUidDefaultCallback.assertNoCallback(); + + // Confirm we can unregister without issues. + mCm.unregisterNetworkCallback(defaultNetworkCallback); + mCm.unregisterNetworkCallback(otherUidDefaultCallback); + } + + /** + * This method assumes that the same uidRanges input will be used to verify that dependencies + * are called as expected. + */ + private void verifySetOemNetworkPreferenceForPreference( + @NonNull final UidRangeParcel[] uidRanges, + final int addUidRangesNetId, + final int addUidRangesTimes, + final int removeUidRangesNetId, + final int removeUidRangesTimes, + final boolean shouldDestroyNetwork) throws RemoteException { + verifySetOemNetworkPreferenceForPreference(uidRanges, uidRanges, + addUidRangesNetId, addUidRangesTimes, removeUidRangesNetId, removeUidRangesTimes, + shouldDestroyNetwork); + } + + private void verifySetOemNetworkPreferenceForPreference( + @NonNull final UidRangeParcel[] addedUidRanges, + @NonNull final UidRangeParcel[] removedUidRanges, + final int addUidRangesNetId, + final int addUidRangesTimes, + final int removeUidRangesNetId, + final int removeUidRangesTimes, + final boolean shouldDestroyNetwork) throws RemoteException { + final boolean useAnyIdForAdd = OEM_PREF_ANY_NET_ID == addUidRangesNetId; + final boolean useAnyIdForRemove = OEM_PREF_ANY_NET_ID == removeUidRangesNetId; + + // Validate netd. + verify(mMockNetd, times(addUidRangesTimes)) + .networkAddUidRanges( + (useAnyIdForAdd ? anyInt() : eq(addUidRangesNetId)), eq(addedUidRanges)); + verify(mMockNetd, times(removeUidRangesTimes)) + .networkRemoveUidRanges( + (useAnyIdForRemove ? anyInt() : eq(removeUidRangesNetId)), + eq(removedUidRanges)); + if (shouldDestroyNetwork) { + verify(mMockNetd, times(1)) + .networkDestroy((useAnyIdForRemove ? anyInt() : eq(removeUidRangesNetId))); + } + reset(mMockNetd); + } + + /** + * Test the tracked default requests clear previous OEM requests on setOemNetworkPreference(). + */ + @Test + public void testSetOemNetworkPreferenceClearPreviousOemValues() throws Exception { + @OemNetworkPreferences.OemNetworkPreference int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + final int testPackageUid = 123; + final String testPackageName = "com.google.apps.contacts"; + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(testPackageUid)); + + // Validate the starting requests only includes the fallback request. + assertEquals(1, mService.mDefaultNetworkRequests.size()); + + // Add an OEM default network request to track. + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, testPackageName); + + // Two requests should exist, one for the fallback and one for the pref. + assertEquals(2, mService.mDefaultNetworkRequests.size()); + + networkPref = OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, testPackageName); + + // Two requests should still exist validating the previous per-app request was replaced. + assertEquals(2, mService.mDefaultNetworkRequests.size()); + } + + /** + * Test network priority for preference OEM_NETWORK_PREFERENCE_OEM_PAID in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID -> fallback + */ + @Test + public void testMultilayerForPreferenceOemPaidEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mWiFiNetworkAgent.getNetwork().netId, 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting OEM_PAID should have no effect as it is lower in priority then unmetered. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + // netd should not be called as default networks haven't changed. + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting unmetered should put PANS on lowest priority fallback request. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mWiFiNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + + // Disconnecting the fallback network should result in no connectivity. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + mCellNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID + */ + @Test + public void testMultilayerForPreferenceOemPaidNoFallbackEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. This preference doesn't support using the fallback network + // therefore should be on the disconnected network as it has no networks to connect to. + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + // This preference should not use this network as it doesn't support fallback usage. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mWiFiNetworkAgent.getNetwork().netId, 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting unmetered should put PANS on OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mWiFiNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + + // Disconnecting OEM_PAID should result in no connectivity. + // OEM_PAID_NO_FALLBACK not supporting a fallback now uses the disconnected network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY in the following order: + * NET_CAPABILITY_OEM_PAID + * This preference should only apply to OEM_PAID networks. + */ + @Test + public void testMultilayerForPreferenceOemPaidOnlyEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. This preference doesn't support using the fallback network + // therefore should be on the disconnected network as it has no networks to connect to. + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up metered cellular. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting OEM_PAID should result in no connectivity. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY in the following order: + * NET_CAPABILITY_OEM_PRIVATE + * This preference should only apply to OEM_PRIVATE networks. + */ + @Test + public void testMultilayerForPreferenceOemPrivateOnlyEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + + // Arrange PackageManager mocks + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. This preference doesn't support using the fallback network + // therefore should be on the disconnected network as it has no networks to connect to. + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up metered cellular. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up unmetered Wi-Fi. This should not apply to this preference. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Bring up ethernet with OEM_PRIVATE. This will satisfy NET_CAPABILITY_OEM_PRIVATE. + startOemManagedNetwork(false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mEthernetNetworkAgent.getNetwork().netId, 1 /* times */, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + false /* shouldDestroyNetwork */); + + // Disconnecting OEM_PRIVATE should result in no connectivity. + stopOemManagedNetwork(); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mService.mNoServiceNetwork.network.getNetId(), 1 /* times */, + mEthernetNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + @Test + public void testMultilayerForMultipleUsersEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Arrange users + final int secondUser = 10; + final UserHandle secondUserHandle = new UserHandle(secondUser); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle)); + + // Arrange PackageManager mocks + final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID); + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels( + uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid)); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly add the expected values for multiple users. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRanges, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly remove the expected values for multiple users. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifySetOemNetworkPreferenceForPreference(uidRanges, + OEM_PREF_ANY_NET_ID, 0 /* times */, + mCellNetworkAgent.getNetwork().netId, 0 /* times */, + true /* shouldDestroyNetwork */); + } + + @Test + public void testMultilayerForBroadcastedUsersEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + + // Arrange users + final int secondUser = 10; + final UserHandle secondUserHandle = new UserHandle(secondUser); + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE)); + + // Arrange PackageManager mocks + final int secondUserTestPackageUid = UserHandle.getUid(secondUser, TEST_PACKAGE_UID); + final UidRangeParcel[] uidRangesSingleUser = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + final UidRangeParcel[] uidRangesBothUsers = + toUidRangeStableParcels( + uidRangesForUids(TEST_PACKAGE_UID, secondUserTestPackageUid)); + setupSetOemNetworkPreferenceForPreferenceTest( + networkPref, uidRangesSingleUser, TEST_PACKAGE_NAME); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly add the expected values for multiple users. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Send a broadcast indicating a user was added. + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE, secondUserHandle)); + final Intent addedIntent = new Intent(ACTION_USER_ADDED); + addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(secondUser)); + processBroadcast(addedIntent); + + // Test that we correctly add values for all users and remove for the single user. + verifySetOemNetworkPreferenceForPreference(uidRangesBothUsers, uidRangesSingleUser, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Send a broadcast indicating a user was removed. + when(mUserManager.getUserHandles(anyBoolean())).thenReturn( + Arrays.asList(PRIMARY_USER_HANDLE)); + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(secondUser)); + processBroadcast(removedIntent); + + // Test that we correctly add values for the single user and remove for the all users. + verifySetOemNetworkPreferenceForPreference(uidRangesSingleUser, uidRangesBothUsers, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + } + + @Test + public void testMultilayerForPackageChangesEvaluatesCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + final String packageScheme = "package:"; + + // Arrange PackageManager mocks + final String packageToInstall = "package.to.install"; + final int packageToInstallUid = 81387; + final UidRangeParcel[] uidRangesSinglePackage = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + mockGetApplicationInfo(TEST_PACKAGE_NAME, TEST_PACKAGE_UID); + mockGetApplicationInfoThrowsNameNotFound(packageToInstall); + setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME, packageToInstall); + grantUsingBackgroundNetworksPermissionForUid(Binder.getCallingUid(), packageToInstall); + + // Verify the starting state. No networks should be connected. + verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, + OEM_PREF_ANY_NET_ID, 0 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Test that we correctly add the expected values for installed packages. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + OEM_PREF_ANY_NET_ID, 0 /* times */, + false /* shouldDestroyNetwork */); + + // Set the system to recognize the package to be installed + mockGetApplicationInfo(packageToInstall, packageToInstallUid); + final UidRangeParcel[] uidRangesAllPackages = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID, packageToInstallUid)); + + // Send a broadcast indicating a package was installed. + final Intent addedIntent = new Intent(ACTION_PACKAGE_ADDED); + addedIntent.setData(Uri.parse(packageScheme + packageToInstall)); + processBroadcast(addedIntent); + + // Test the single package is removed and the combined packages are added. + verifySetOemNetworkPreferenceForPreference(uidRangesAllPackages, uidRangesSinglePackage, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Set the system to no longer recognize the package to be installed + mockGetApplicationInfoThrowsNameNotFound(packageToInstall); + + // Send a broadcast indicating a package was removed. + final Intent removedIntent = new Intent(ACTION_PACKAGE_REMOVED); + removedIntent.setData(Uri.parse(packageScheme + packageToInstall)); + processBroadcast(removedIntent); + + // Test the combined packages are removed and the single package is added. + verifySetOemNetworkPreferenceForPreference(uidRangesSinglePackage, uidRangesAllPackages, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + + // Set the system to change the installed package's uid + final int replacedTestPackageUid = TEST_PACKAGE_UID + 1; + mockGetApplicationInfo(TEST_PACKAGE_NAME, replacedTestPackageUid); + final UidRangeParcel[] uidRangesReplacedPackage = + toUidRangeStableParcels(uidRangesForUids(replacedTestPackageUid)); + + // Send a broadcast indicating a package was replaced. + final Intent replacedIntent = new Intent(ACTION_PACKAGE_REPLACED); + replacedIntent.setData(Uri.parse(packageScheme + TEST_PACKAGE_NAME)); + processBroadcast(replacedIntent); + + // Test the original uid is removed and is replaced with the new uid. + verifySetOemNetworkPreferenceForPreference(uidRangesReplacedPackage, uidRangesSinglePackage, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + mCellNetworkAgent.getNetwork().netId, 1 /* times */, + false /* shouldDestroyNetwork */); + } + + /** + * Test network priority for preference OEM_NETWORK_PREFERENCE_OEM_PAID in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID -> fallback + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 3; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mCellNetworkAgent.getNetwork()); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mWiFiNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi will put the pref on OEM_PAID and fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting cellular should keep OEM network on OEM_PAID and fallback will be null. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PAID will put both on null as it is the last network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + null); + + // default callbacks will be unregistered in tearDown + } + + @Test + public void testNetworkFactoryRequestsWithMultilayerRequest() + throws Exception { + // First use OEM_PAID preference to create a multi-layer request : 1. listen for + // unmetered, 2. request network with cap OEM_PAID, 3, request the default network for + // fallback. + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + + final HandlerThread handlerThread = new HandlerThread("MockFactory"); + handlerThread.start(); + NetworkCapabilities internetFilter = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + final MockNetworkFactory internetFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "internetFactory", internetFilter, mCsHandlerThread); + internetFactory.setScoreFilter(40); + internetFactory.register(); + // Default internet request only. The unmetered request is never sent to factories (it's a + // LISTEN, not requestable). The 3rd (fallback) request in OEM_PAID NRI is TRACK_DEFAULT + // which is also not sent to factories. Finally, the OEM_PAID request doesn't match the + // internetFactory filter. + internetFactory.expectRequestAdds(1); + internetFactory.assertRequestCountEquals(1); + + NetworkCapabilities oemPaidFilter = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addCapability(NET_CAPABILITY_OEM_PAID) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + final MockNetworkFactory oemPaidFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "oemPaidFactory", oemPaidFilter, mCsHandlerThread); + oemPaidFactory.setScoreFilter(40); + oemPaidFactory.register(); + oemPaidFactory.expectRequestAdd(); // Because nobody satisfies the request + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + // A network connected that satisfies the default internet request. For the OEM_PAID + // preference, this is not as good as an OEM_PAID network, so even if the score of + // the network is better than the factory announced, it still should try to bring up + // the network. + expectNoRequestChanged(oemPaidFactory); + oemPaidFactory.assertRequestCountEquals(1); + // The internet factory however is outscored, and should lose its requests. + internetFactory.expectRequestRemove(); + internetFactory.assertRequestCountEquals(0); + + final NetworkCapabilities oemPaidNc = new NetworkCapabilities(); + oemPaidNc.addCapability(NET_CAPABILITY_OEM_PAID); + oemPaidNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + final TestNetworkAgentWrapper oemPaidAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, + new LinkProperties(), oemPaidNc); + oemPaidAgent.connect(true); + + // The oemPaidAgent has score 50/cell transport, so it beats what the oemPaidFactory can + // provide, therefore it loses the request. + oemPaidFactory.expectRequestRemove(); + oemPaidFactory.assertRequestCountEquals(0); + expectNoRequestChanged(internetFactory); + internetFactory.assertRequestCountEquals(0); + + oemPaidAgent.setScore(new NetworkScore.Builder().setLegacyInt(20).setExiting(true).build()); + // Now the that the agent is weak, the oemPaidFactory can beat the existing network for the + // OEM_PAID request. The internet factory however can't beat a network that has OEM_PAID + // for the preference request, so it doesn't see the request. + oemPaidFactory.expectRequestAdd(); + oemPaidFactory.assertRequestCountEquals(1); + expectNoRequestChanged(internetFactory); + internetFactory.assertRequestCountEquals(0); + + mCellNetworkAgent.disconnect(); + // The network satisfying the default internet request has disconnected, so the + // internetFactory sees the default request again. However there is a network with OEM_PAID + // connected, so the 2nd OEM_PAID req is already satisfied, so the oemPaidFactory doesn't + // care about networks that don't have OEM_PAID. + expectNoRequestChanged(oemPaidFactory); + oemPaidFactory.assertRequestCountEquals(1); + internetFactory.expectRequestAdd(); + internetFactory.assertRequestCountEquals(1); + + // Cell connects again, still with score 50. Back to the previous state. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + expectNoRequestChanged(oemPaidFactory); + oemPaidFactory.assertRequestCountEquals(1); + internetFactory.expectRequestRemove(); + internetFactory.assertRequestCountEquals(0); + + // Create a request that holds the upcoming wifi network. + final TestNetworkCallback wifiCallback = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), + wifiCallback); + + // Now WiFi connects and it's unmetered, but it's weaker than cell. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED); + mWiFiNetworkAgent.setScore(new NetworkScore.Builder().setLegacyInt(30).setExiting(true) + .build()); // Not the best Internet network, but unmetered + mWiFiNetworkAgent.connect(true); + + // The OEM_PAID preference prefers an unmetered network to an OEM_PAID network, so + // the oemPaidFactory can't beat wifi no matter how high its score. + oemPaidFactory.expectRequestRemove(); + expectNoRequestChanged(internetFactory); + + mCellNetworkAgent.disconnect(); + // Now that the best internet network (cell, with its 50 score compared to 30 for WiFi + // at this point), the default internet request is satisfied by a network worse than + // the internetFactory announced, so it gets the request. However, there is still an + // unmetered network, so the oemPaidNetworkFactory still can't beat this. + expectNoRequestChanged(oemPaidFactory); + internetFactory.expectRequestAdd(); + mCm.unregisterNetworkCallback(wifiCallback); + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK in the following order: + * NET_CAPABILITY_NOT_METERED -> NET_CAPABILITY_OEM_PAID + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidNoFallbackCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 2; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network but not the pref. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. This will satisfy NET_CAPABILITY_NOT_METERED. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mWiFiNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi will put the OEM pref on OEM_PAID and fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting cellular should keep OEM network on OEM_PAID and fallback will be null. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PAID puts the fallback on null and the pref on the disconnected net. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mService.mNoServiceNetwork.network()); + + // default callbacks will be unregistered in tearDown + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY in the following order: + * NET_CAPABILITY_OEM_PAID + * This preference should only apply to OEM_PAID networks. + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPaidOnlyCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Bring up ethernet with OEM_PAID. This will satisfy NET_CAPABILITY_OEM_PAID. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. The OEM network shouldn't change, the fallback will take Wi-Fi. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi shouldn't change the OEM network with fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PAID will keep the fallback on cellular and nothing for OEM_PAID. + // OEM_PAID_ONLY not supporting a fallback now uses the disconnected network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_ETHERNET, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Disconnecting cellular will put the fallback on null and the pref on disconnected. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mService.mNoServiceNetwork.network()); + + // default callbacks will be unregistered in tearDown + } + + /** + * Test network priority for OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY in the following order: + * NET_CAPABILITY_OEM_PRIVATE + * This preference should only apply to OEM_PRIVATE networks. + */ + @Test + public void testMultipleDefaultNetworksTracksOemNetworkPreferenceOemPrivateOnlyCorrectly() + throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + setupMultipleDefaultNetworksForOemNetworkPreferenceCurrentUidTest(networkPref); + final int expectedDefaultRequestSize = 2; + final int expectedOemPrefRequestSize = 1; + registerDefaultNetworkCallbacks(); + + // The fallback as well as the OEM preference should now be tracked. + assertEquals(expectedDefaultRequestSize, mService.mDefaultNetworkRequests.size()); + + // Test lowest to highest priority requests. + // Bring up metered cellular. This will satisfy the fallback network. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Bring up ethernet with OEM_PRIVATE. This will satisfy NET_CAPABILITY_OEM_PRIVATE. + startOemManagedNetwork(false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Bring up unmetered Wi-Fi. The OEM network shouldn't change, the fallback will take Wi-Fi. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, true); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mWiFiNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting unmetered Wi-Fi shouldn't change the OEM network with fallback on cellular. + setOemNetworkPreferenceAgentConnected(TRANSPORT_WIFI, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mEthernetNetworkAgent.getNetwork()); + + // Disconnecting OEM_PRIVATE will keep the fallback on cellular. + // OEM_PRIVATE_ONLY not supporting a fallback now uses to the disconnected network. + stopOemManagedNetwork(); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + mCellNetworkAgent.getNetwork(), + mService.mNoServiceNetwork.network()); + + // Disconnecting cellular will put the fallback on null and pref on disconnected. + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, false); + verifyMultipleDefaultNetworksTracksCorrectly(expectedOemPrefRequestSize, + null, + mService.mNoServiceNetwork.network()); + + // default callbacks will be unregistered in tearDown + } + + @Test + public void testCapabilityWithOemNetworkPreference() throws Exception { + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(networkPref); + registerDefaultNetworkCallbacks(); + + setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true); + + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + mSystemDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + + // default callbacks will be unregistered in tearDown + } + + @Test + public void testSetOemNetworkPreferenceLogsRequest() throws Exception { + mServiceContext.setPermission(DUMP, PERMISSION_GRANTED); + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PAID; + final StringWriter stringWriter = new StringWriter(); + final String logIdentifier = "UPDATE INITIATED: OemNetworkPreferences"; + final Pattern pattern = Pattern.compile(logIdentifier); + + final int expectedNumLogs = 2; + final UidRangeParcel[] uidRanges = + toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID)); + + // Call twice to generate two logs. + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME); + mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]); + + final String dumpOutput = stringWriter.toString(); + final Matcher matcher = pattern.matcher(dumpOutput); + int count = 0; + while (matcher.find()) { + count++; + } + assertEquals(expectedNumLogs, count); + } + + @Test + public void testGetAllNetworkStateSnapshots() throws Exception { + verifyNoNetwork(); + + // Setup test cellular network with specified LinkProperties and NetworkCapabilities, + // verify the content of the snapshot matches. + final LinkProperties cellLp = new LinkProperties(); + final LinkAddress myIpv4Addr = new LinkAddress(InetAddress.getByName("192.0.2.129"), 25); + final LinkAddress myIpv6Addr = new LinkAddress(InetAddress.getByName("2001:db8::1"), 64); + cellLp.setInterfaceName("test01"); + cellLp.addLinkAddress(myIpv4Addr); + cellLp.addLinkAddress(myIpv6Addr); + cellLp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234"))); + cellLp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + cellLp.addRoute(new RouteInfo(myIpv4Addr, null)); + cellLp.addRoute(new RouteInfo(myIpv6Addr, null)); + final NetworkCapabilities cellNcTemplate = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_MMS).build(); + + final TestNetworkCallback cellCb = new TestNetworkCallback(); + mCm.requestNetwork(new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(), + cellCb); + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp, cellNcTemplate); + mCellNetworkAgent.connect(true); + cellCb.expectAvailableCallbacksUnvalidated(mCellNetworkAgent); + List<NetworkStateSnapshot> snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(1, snapshots); + + // Compose the expected cellular snapshot for verification. + final NetworkCapabilities cellNc = + mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()); + final NetworkStateSnapshot cellSnapshot = new NetworkStateSnapshot( + mCellNetworkAgent.getNetwork(), cellNc, cellLp, + null, ConnectivityManager.TYPE_MOBILE); + assertEquals(cellSnapshot, snapshots.get(0)); + + // Connect wifi and verify the snapshots. + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + waitForIdle(); + // Compose the expected wifi snapshot for verification. + final NetworkCapabilities wifiNc = + mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()); + final NetworkStateSnapshot wifiSnapshot = new NetworkStateSnapshot( + mWiFiNetworkAgent.getNetwork(), wifiNc, new LinkProperties(), null, + ConnectivityManager.TYPE_WIFI); + + snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(2, snapshots); + assertContainsAll(snapshots, cellSnapshot, wifiSnapshot); + + // Set cellular as suspended, verify the snapshots will not contain suspended networks. + // TODO: Consider include SUSPENDED networks, which should be considered as + // temporary shortage of connectivity of a connected network. + mCellNetworkAgent.suspend(); + waitForIdle(); + snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(1, snapshots); + assertEquals(wifiSnapshot, snapshots.get(0)); + + // Disconnect wifi, verify the snapshots contain nothing. + mWiFiNetworkAgent.disconnect(); + waitForIdle(); + snapshots = mCm.getAllNetworkStateSnapshots(); + assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork()); + assertLength(0, snapshots); + + mCellNetworkAgent.resume(); + waitForIdle(); + snapshots = mCm.getAllNetworkStateSnapshots(); + assertLength(1, snapshots); + assertEquals(cellSnapshot, snapshots.get(0)); + + mCellNetworkAgent.disconnect(); + waitForIdle(); + verifyNoNetwork(); + mCm.unregisterNetworkCallback(cellCb); + } + + // Cannot be part of MockNetworkFactory since it requires method of the test. + private void expectNoRequestChanged(@NonNull MockNetworkFactory factory) { + waitForIdle(); + factory.assertNoRequestChanged(); + } + + @Test + public void testRegisterBestMatchingNetworkCallback_noIssueToFactory() throws Exception { + // Prepare mock mms factory. + final HandlerThread handlerThread = new HandlerThread("MockCellularFactory"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_MMS); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter, mCsHandlerThread); + testFactory.setScoreFilter(40); + + try { + // Register the factory. It doesn't see the default request because its filter does + // not include INTERNET. + testFactory.register(); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(0); + // The factory won't try to start the network since the default request doesn't + // match the filter (no INTERNET capability). + assertFalse(testFactory.getMyStartRequested()); + + // Register callback for listening best matching network. Verify that the request won't + // be sent to factory. + final TestNetworkCallback bestMatchingCb = new TestNetworkCallback(); + mCm.registerBestMatchingNetworkCallback( + new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(), + bestMatchingCb, mCsHandlerThread.getThreadHandler()); + bestMatchingCb.assertNoCallback(); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(0); + assertFalse(testFactory.getMyStartRequested()); + + // Fire a normal mms request, verify the factory will only see the request. + final TestNetworkCallback mmsNetworkCallback = new TestNetworkCallback(); + final NetworkRequest mmsRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_MMS).build(); + mCm.requestNetwork(mmsRequest, mmsNetworkCallback); + testFactory.expectRequestAdd(); + testFactory.assertRequestCountEquals(1); + assertTrue(testFactory.getMyStartRequested()); + + // Unregister best matching callback, verify factory see no change. + mCm.unregisterNetworkCallback(bestMatchingCb); + expectNoRequestChanged(testFactory); + testFactory.assertRequestCountEquals(1); + assertTrue(testFactory.getMyStartRequested()); + } finally { + testFactory.terminate(); + } + } + + @Test + public void testRegisterBestMatchingNetworkCallback_trackBestNetwork() throws Exception { + final TestNetworkCallback bestMatchingCb = new TestNetworkCallback(); + mCm.registerBestMatchingNetworkCallback( + new NetworkRequest.Builder().addCapability(NET_CAPABILITY_TRUSTED).build(), + bestMatchingCb, mCsHandlerThread.getThreadHandler()); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + bestMatchingCb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + + mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI); + mWiFiNetworkAgent.connect(true); + bestMatchingCb.expectAvailableDoubleValidatedCallbacks(mWiFiNetworkAgent); + + // Change something on cellular to trigger capabilities changed, since the callback + // only cares about the best network, verify it received nothing from cellular. + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + bestMatchingCb.assertNoCallback(); + + // Make cellular the best network again, verify the callback now tracks cellular. + mWiFiNetworkAgent.adjustScore(-50); + bestMatchingCb.expectAvailableCallbacksValidated(mCellNetworkAgent); + + // Make cellular temporary non-trusted, which will not satisfying the request. + // Verify the callback switch from/to the other network accordingly. + mCellNetworkAgent.removeCapability(NET_CAPABILITY_TRUSTED); + bestMatchingCb.expectAvailableCallbacksValidated(mWiFiNetworkAgent); + mCellNetworkAgent.addCapability(NET_CAPABILITY_TRUSTED); + bestMatchingCb.expectAvailableDoubleValidatedCallbacks(mCellNetworkAgent); + + // Verify the callback doesn't care about wifi disconnect. + mWiFiNetworkAgent.disconnect(); + bestMatchingCb.assertNoCallback(); + mCellNetworkAgent.disconnect(); + bestMatchingCb.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + } + + private UidRangeParcel[] uidRangeFor(final UserHandle handle) { + UidRange range = UidRange.createForUser(handle); + return new UidRangeParcel[] { new UidRangeParcel(range.start, range.stop) }; + } + + private static class TestOnCompleteListener implements Runnable { + final class OnComplete {} + final ArrayTrackRecord<OnComplete>.ReadHead mHistory = + new ArrayTrackRecord<OnComplete>().newReadHead(); + + @Override + public void run() { + mHistory.add(new OnComplete()); + } + + public void expectOnComplete() { + assertNotNull(mHistory.poll(TIMEOUT_MS, it -> true)); + } + } + + private TestNetworkAgentWrapper makeEnterpriseNetworkAgent() throws Exception { + final NetworkCapabilities workNc = new NetworkCapabilities(); + workNc.addCapability(NET_CAPABILITY_ENTERPRISE); + workNc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED); + return new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(), workNc); + } + + private TestNetworkCallback mEnterpriseCallback; + private UserHandle setupEnterpriseNetwork() { + final UserHandle userHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(userHandle, true); + + // File a request to avoid the enterprise network being disconnected as soon as the default + // request goes away – it would make impossible to test that networkRemoveUidRanges + // is called, as the network would disconnect first for lack of a request. + mEnterpriseCallback = new TestNetworkCallback(); + final NetworkRequest keepUpRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_ENTERPRISE) + .build(); + mCm.requestNetwork(keepUpRequest, mEnterpriseCallback); + return userHandle; + } + + private void maybeTearDownEnterpriseNetwork() { + if (null != mEnterpriseCallback) { + mCm.unregisterNetworkCallback(mEnterpriseCallback); + } + } + + /** + * Make sure per-profile networking preference behaves as expected when the enterprise network + * goes up and down while the preference is active. Make sure they behave as expected whether + * there is a general default network or not. + */ + @Test + public void testPreferenceForUserNetworkUpDown() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle = setupEnterpriseNetwork(); + registerDefaultNetworkCallbacks(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + + // Setting a network preference for this user will create a new set of routing rules for + // the UID range that corresponds to this user, so as to define the default network + // for these apps separately. This is true because the multi-layer request relevant to + // this UID range contains a TRACK_DEFAULT, so the range will be moved through UID-specific + // rules to the correct network – in this case the system default network. The case where + // the default network for the profile happens to be the same as the system default + // is not handled specially, the rules are always active as long as a preference is set. + inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + // The enterprise network is not ready yet. + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + mProfileDefaultNetworkCallback); + + final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); + workAgent.connect(false); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent); + mSystemDefaultNetworkCallback.assertNoCallback(); + mDefaultNetworkCallback.assertNoCallback(); + inOrder.verify(mMockNetd).networkCreate( + nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM)); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle)); + inOrder.verify(mMockNetd).networkRemoveUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + // Make sure changes to the work agent send callbacks to the app in the work profile, but + // not to the other apps. + workAgent.setNetworkValid(true /* isStrictMode */); + workAgent.mNetworkMonitor.forceReevaluation(Process.myUid()); + mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, + nc -> nc.hasCapability(NET_CAPABILITY_VALIDATED) + && nc.hasCapability(NET_CAPABILITY_ENTERPRISE)); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + + workAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + + // Conversely, change a capability on the system-wide default network and make sure + // that only the apps outside of the work profile receive the callbacks. + mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED); + mSystemDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + mDefaultNetworkCallback.expectCapabilitiesThat(mCellNetworkAgent, nc -> + nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)); + mProfileDefaultNetworkCallback.assertNoCallback(); + + // Disconnect and reconnect the system-wide default network and make sure that the + // apps on this network see the appropriate callbacks, and the app on the work profile + // doesn't because it continues to use the enterprise network. + mCellNetworkAgent.disconnect(); + mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mProfileDefaultNetworkCallback.assertNoCallback(); + inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mProfileDefaultNetworkCallback.assertNoCallback(); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + + // When the agent disconnects, test that the app on the work profile falls back to the + // default network. + workAgent.disconnect(); + mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent); + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + inOrder.verify(mMockNetd).networkDestroy(workAgent.getNetwork().netId); + + mCellNetworkAgent.disconnect(); + mSystemDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent); + + // Waiting for the handler to be idle before checking for networkDestroy is necessary + // here because ConnectivityService calls onLost before the network is fully torn down. + waitForIdle(); + inOrder.verify(mMockNetd).networkDestroy(mCellNetworkAgent.getNetwork().netId); + + // If the control comes here, callbacks seem to behave correctly in the presence of + // a default network when the enterprise network goes up and down. Now, make sure they + // also behave correctly in the absence of a system-wide default network. + final TestNetworkAgentWrapper workAgent2 = makeEnterpriseNetworkAgent(); + workAgent2.connect(false); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent2); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + workAgent2.getNetwork().netId, INetd.PERMISSION_SYSTEM)); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent2.getNetwork().netId, + uidRangeFor(testHandle)); + + workAgent2.setNetworkValid(true /* isStrictMode */); + workAgent2.mNetworkMonitor.forceReevaluation(Process.myUid()); + mProfileDefaultNetworkCallback.expectCapabilitiesThat(workAgent2, + nc -> nc.hasCapability(NET_CAPABILITY_ENTERPRISE) + && !nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd, never()).networkAddUidRanges(anyInt(), any()); + + // When the agent disconnects, test that the app on the work profile falls back to the + // default network. + workAgent2.disconnect(); + mProfileDefaultNetworkCallback.expectCallback(CallbackEntry.LOST, workAgent2); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkDestroy(workAgent2.getNetwork().netId); + + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + mProfileDefaultNetworkCallback); + + // Callbacks will be unregistered by tearDown() + } + + /** + * Test that, in a given networking context, calling setPreferenceForUser to set per-profile + * defaults on then off works as expected. + */ + @Test + public void testSetPreferenceForUserOnOff() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle = setupEnterpriseNetwork(); + + // Connect both a regular cell agent and an enterprise network first. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); + workAgent.connect(true); + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + registerDefaultNetworkCallbacks(); + + mSystemDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(workAgent); + + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_DEFAULT, + r -> r.run(), listener); + listener.expectOnComplete(); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback); + inOrder.verify(mMockNetd).networkRemoveUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + workAgent.disconnect(); + mCellNetworkAgent.disconnect(); + + // Callbacks will be unregistered by tearDown() + } + + /** + * Test per-profile default networks for two different profiles concurrently. + */ + @Test + public void testSetPreferenceForTwoProfiles() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle2 = setupEnterpriseNetwork(); + final UserHandle testHandle4 = UserHandle.of(TEST_WORK_PROFILE_USER_ID + 2); + mServiceContext.setWorkProfile(testHandle4, true); + registerDefaultNetworkCallbacks(); + + final TestNetworkCallback app4Cb = new TestNetworkCallback(); + final int testWorkProfileAppUid4 = + UserHandle.getUid(testHandle4.getIdentifier(), TEST_APP_ID); + registerDefaultNetworkCallbackAsUid(app4Cb, testWorkProfileAppUid4); + + // Connect both a regular cell agent and an enterprise network first. + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent(); + workAgent.connect(true); + + mSystemDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + mProfileDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + app4Cb.expectAvailableThenValidatedCallbacks(mCellNetworkAgent); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM)); + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle2, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle2)); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(workAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + app4Cb); + + mCm.setProfileNetworkPreference(testHandle4, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkAddUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle4)); + + app4Cb.expectAvailableCallbacksValidated(workAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + mProfileDefaultNetworkCallback); + + mCm.setProfileNetworkPreference(testHandle2, PROFILE_NETWORK_PREFERENCE_DEFAULT, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkRemoveUidRanges(workAgent.getNetwork().netId, + uidRangeFor(testHandle2)); + + mProfileDefaultNetworkCallback.expectAvailableCallbacksValidated(mCellNetworkAgent); + assertNoCallbacks(mSystemDefaultNetworkCallback, mDefaultNetworkCallback, + app4Cb); + + workAgent.disconnect(); + mCellNetworkAgent.disconnect(); + + mCm.unregisterNetworkCallback(app4Cb); + // Other callbacks will be unregistered by tearDown() + } + + @Test + public void testProfilePreferenceRemovedUponUserRemoved() throws Exception { + final InOrder inOrder = inOrder(mMockNetd); + final UserHandle testHandle = setupEnterpriseNetwork(); + + mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR); + mCellNetworkAgent.connect(true); + + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + inOrder.verify(mMockNetd).networkCreate(nativeNetworkConfigPhysical( + mCellNetworkAgent.getNetwork().netId, INetd.PERMISSION_NONE)); + inOrder.verify(mMockNetd).networkAddUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + + final Intent removedIntent = new Intent(ACTION_USER_REMOVED); + removedIntent.putExtra(Intent.EXTRA_USER, testHandle); + processBroadcast(removedIntent); + + inOrder.verify(mMockNetd).networkRemoveUidRanges(mCellNetworkAgent.getNetwork().netId, + uidRangeFor(testHandle)); + } + + /** + * Make sure that OEM preference and per-profile preference can't be used at the same + * time and throw ISE if tried + */ + @Test + public void testOemPreferenceAndProfilePreferenceExclusive() throws Exception { + final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(testHandle, true); + final TestOnCompleteListener listener = new TestOnCompleteListener(); + + setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest( + OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY); + assertThrows("Should not be able to set per-profile pref while OEM prefs present", + IllegalStateException.class, () -> + mCm.setProfileNetworkPreference(testHandle, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener)); + + // Empty the OEM prefs + final TestOemListenerCallback oemPrefListener = new TestOemListenerCallback(); + final OemNetworkPreferences emptyOemPref = new OemNetworkPreferences.Builder().build(); + mService.setOemNetworkPreference(emptyOemPref, oemPrefListener); + oemPrefListener.expectOnComplete(); + + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + r -> r.run(), listener); + listener.expectOnComplete(); + assertThrows("Should not be able to set OEM prefs while per-profile pref is on", + IllegalStateException.class , () -> + mService.setOemNetworkPreference(emptyOemPref, oemPrefListener)); + } + + /** + * Make sure wrong preferences for per-profile default networking are rejected. + */ + @Test + public void testProfileNetworkPrefWrongPreference() throws Exception { + final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(testHandle, true); + assertThrows("Should not be able to set an illegal preference", + IllegalArgumentException.class, + () -> mCm.setProfileNetworkPreference(testHandle, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE + 1, null, null)); + } + + /** + * Make sure requests for per-profile default networking for a non-work profile are + * rejected + */ + @Test + public void testProfileNetworkPrefWrongProfile() throws Exception { + final UserHandle testHandle = UserHandle.of(TEST_WORK_PROFILE_USER_ID); + mServiceContext.setWorkProfile(testHandle, false); + assertThrows("Should not be able to set a user pref for a non-work profile", + IllegalArgumentException.class , () -> + mCm.setProfileNetworkPreference(testHandle, + PROFILE_NETWORK_PREFERENCE_ENTERPRISE, null, null)); + } + + @Test + public void testSubIdsClearedWithoutNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED); + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setSubscriptionIds(Collections.singleton(Process.myUid())); + + final NetworkCapabilities result = + mService.networkCapabilitiesRestrictedForCallerPermissions( + nc, Process.myPid(), Process.myUid()); + assertTrue(result.getSubscriptionIds().isEmpty()); + } + + @Test + public void testSubIdsExistWithNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED); + + final Set<Integer> subIds = Collections.singleton(Process.myUid()); + final NetworkCapabilities nc = new NetworkCapabilities(); + nc.setSubscriptionIds(subIds); + + final NetworkCapabilities result = + mService.networkCapabilitiesRestrictedForCallerPermissions( + nc, Process.myPid(), Process.myUid()); + assertEquals(subIds, result.getSubscriptionIds()); + } + + private NetworkRequest getRequestWithSubIds() { + return new NetworkRequest.Builder() + .setSubscriptionIds(Collections.singleton(Process.myUid())) + .build(); + } + + @Test + public void testNetworkRequestWithSubIdsWithNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_GRANTED); + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + final NetworkCallback networkCallback1 = new NetworkCallback(); + final NetworkCallback networkCallback2 = new NetworkCallback(); + + mCm.requestNetwork(getRequestWithSubIds(), networkCallback1); + mCm.requestNetwork(getRequestWithSubIds(), pendingIntent); + mCm.registerNetworkCallback(getRequestWithSubIds(), networkCallback2); + + mCm.unregisterNetworkCallback(networkCallback1); + mCm.releaseNetworkRequest(pendingIntent); + mCm.unregisterNetworkCallback(networkCallback2); + } + + @Test + public void testNetworkRequestWithSubIdsWithoutNetworkFactoryPermission() throws Exception { + mServiceContext.setPermission(NETWORK_FACTORY, PERMISSION_DENIED); + final PendingIntent pendingIntent = PendingIntent.getBroadcast( + mContext, 0 /* requestCode */, new Intent("a"), FLAG_IMMUTABLE); + + final Class<SecurityException> expected = SecurityException.class; + assertThrows( + expected, () -> mCm.requestNetwork(getRequestWithSubIds(), new NetworkCallback())); + assertThrows(expected, () -> mCm.requestNetwork(getRequestWithSubIds(), pendingIntent)); + assertThrows( + expected, + () -> mCm.registerNetworkCallback(getRequestWithSubIds(), new NetworkCallback())); + } + + /** + * Validate request counts are counted accurately on setProfileNetworkPreference on set/replace. + */ + @Test + public void testProfileNetworkPrefCountsRequestsCorrectlyOnSet() throws Exception { + final UserHandle testHandle = setupEnterpriseNetwork(); + testRequestCountLimits(() -> { + // Set initially to test the limit prior to having existing requests. + final TestOnCompleteListener listener = new TestOnCompleteListener(); + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + Runnable::run, listener); + listener.expectOnComplete(); + + // re-set so as to test the limit as part of replacing existing requests. + mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE, + Runnable::run, listener); + listener.expectOnComplete(); + }); + } + + /** + * Validate request counts are counted accurately on setOemNetworkPreference on set/replace. + */ + @Test + public void testSetOemNetworkPreferenceCountsRequestsCorrectlyOnSet() throws Exception { + mockHasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, true); + @OemNetworkPreferences.OemNetworkPreference final int networkPref = + OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY; + testRequestCountLimits(() -> { + // Set initially to test the limit prior to having existing requests. + final TestOemListenerCallback listener = new TestOemListenerCallback(); + mService.setOemNetworkPreference( + createDefaultOemNetworkPreferences(networkPref), listener); + listener.expectOnComplete(); + + // re-set so as to test the limit as part of replacing existing requests. + mService.setOemNetworkPreference( + createDefaultOemNetworkPreferences(networkPref), listener); + listener.expectOnComplete(); + }); + } + + private void testRequestCountLimits(@NonNull final Runnable r) throws Exception { + final ArraySet<TestNetworkCallback> callbacks = new ArraySet<>(); + try { + final int requestCount = mService.mSystemNetworkRequestCounter + .mUidToNetworkRequestCount.get(Process.myUid()); + // The limit is hit when total requests <= limit. + final int maxCount = + ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - requestCount; + // Need permission so registerDefaultNetworkCallback uses mSystemNetworkRequestCounter + withPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, () -> { + for (int i = 1; i < maxCount - 1; i++) { + final TestNetworkCallback cb = new TestNetworkCallback(); + mCm.registerDefaultNetworkCallback(cb); + callbacks.add(cb); + } + + // Code to run to check if it triggers a max request count limit error. + r.run(); + }); + } finally { + for (final TestNetworkCallback cb : callbacks) { + mCm.unregisterNetworkCallback(cb); + } + } + } +} diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cf2c9c783ac7fe8292bd3e338d4398aa7f178164 --- /dev/null +++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java @@ -0,0 +1,1004 @@ +/* + * 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.server; + +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.INetd.IF_STATE_DOWN; +import static android.net.INetd.IF_STATE_UP; +import static android.net.IpSecManager.DIRECTION_FWD; +import static android.net.IpSecManager.DIRECTION_IN; +import static android.net.IpSecManager.DIRECTION_OUT; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpSecAlgorithm; +import android.net.IpSecConfig; +import android.net.IpSecManager; +import android.net.IpSecSpiResponse; +import android.net.IpSecTransform; +import android.net.IpSecTransformResponse; +import android.net.IpSecTunnelInterfaceResponse; +import android.net.IpSecUdpEncapResponse; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.system.Os; +import android.test.mock.MockContext; +import android.util.ArraySet; + +import androidx.test.filters.SmallTest; + +import com.android.server.IpSecService.TunnelInterfaceRecord; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.net.Inet4Address; +import java.net.Socket; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; + +/** Unit tests for {@link IpSecService}. */ +@SmallTest +@RunWith(Parameterized.class) +public class IpSecServiceParameterizedTest { + + private static final int TEST_SPI = 0xD1201D; + + private final String mSourceAddr; + private final String mDestinationAddr; + private final LinkAddress mLocalInnerAddress; + private final int mFamily; + + private static final int[] ADDRESS_FAMILIES = + new int[] {AF_INET, AF_INET6}; + + @Parameterized.Parameters + public static Collection ipSecConfigs() { + return Arrays.asList( + new Object[][] { + {"1.2.3.4", "8.8.4.4", "10.0.1.1/24", AF_INET}, + {"2601::2", "2601::10", "2001:db8::1/64", AF_INET6} + }); + } + + private static final byte[] AEAD_KEY = { + 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, + 0x73, 0x61, 0x6C, 0x74 + }; + private static final byte[] CRYPT_KEY = { + 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 + }; + private static final byte[] AUTH_KEY = { + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F + }; + + AppOpsManager mMockAppOps = mock(AppOpsManager.class); + ConnectivityManager mMockConnectivityMgr = mock(ConnectivityManager.class); + + TestContext mTestContext = new TestContext(); + + private class TestContext extends MockContext { + private Set<String> mAllowedPermissions = new ArraySet<>(Arrays.asList( + android.Manifest.permission.MANAGE_IPSEC_TUNNELS, + android.Manifest.permission.NETWORK_STACK, + PERMISSION_MAINLINE_NETWORK_STACK)); + + private void setAllowedPermissions(String... permissions) { + mAllowedPermissions = new ArraySet<>(permissions); + } + + @Override + public Object getSystemService(String name) { + switch(name) { + case Context.APP_OPS_SERVICE: + return mMockAppOps; + case Context.CONNECTIVITY_SERVICE: + return mMockConnectivityMgr; + default: + return null; + } + } + + @Override + public String getSystemServiceName(Class<?> serviceClass) { + if (ConnectivityManager.class == serviceClass) { + return Context.CONNECTIVITY_SERVICE; + } + return null; + } + + @Override + public PackageManager getPackageManager() { + return mMockPkgMgr; + } + + @Override + public void enforceCallingOrSelfPermission(String permission, String message) { + if (mAllowedPermissions.contains(permission)) { + return; + } else { + throw new SecurityException("Unavailable permission requested"); + } + } + + @Override + public int checkCallingOrSelfPermission(String permission) { + if (mAllowedPermissions.contains(permission)) { + return PERMISSION_GRANTED; + } else { + return PERMISSION_DENIED; + } + } + } + + INetd mMockNetd; + PackageManager mMockPkgMgr; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + Network fakeNetwork = new Network(0xAB); + int mUid = Os.getuid(); + + private static final IpSecAlgorithm AUTH_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4); + private static final IpSecAlgorithm CRYPT_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + private static final IpSecAlgorithm AEAD_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + private static final int REMOTE_ENCAP_PORT = 4500; + + private static final String BLESSED_PACKAGE = "blessedPackage"; + private static final String SYSTEM_PACKAGE = "systemPackage"; + private static final String BAD_PACKAGE = "badPackage"; + + public IpSecServiceParameterizedTest( + String sourceAddr, String destAddr, String localInnerAddr, int family) { + mSourceAddr = sourceAddr; + mDestinationAddr = destAddr; + mLocalInnerAddress = new LinkAddress(localInnerAddr); + mFamily = family; + } + + @Before + public void setUp() throws Exception { + mMockNetd = mock(INetd.class); + mMockPkgMgr = mock(PackageManager.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mTestContext, mMockIpSecSrvConfig); + + // Injecting mock netd + when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd); + + // PackageManager should always return true (feature flag tests in IpSecServiceTest) + when(mMockPkgMgr.hasSystemFeature(anyString())).thenReturn(true); + + // A package granted the AppOp for MANAGE_IPSEC_TUNNELS will be MODE_ALLOWED. + when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(BLESSED_PACKAGE))) + .thenReturn(AppOpsManager.MODE_ALLOWED); + // A system package will not be granted the app op, so this should fall back to + // a permissions check, which should pass. + when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(SYSTEM_PACKAGE))) + .thenReturn(AppOpsManager.MODE_DEFAULT); + // A mismatch between the package name and the UID will return MODE_IGNORED. + when(mMockAppOps.noteOp(anyInt(), anyInt(), eq(BAD_PACKAGE))) + .thenReturn(AppOpsManager.MODE_IGNORED); + } + + //TODO: Add a test to verify SPI. + + @Test + public void testIpSecServiceReserveSpi() throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI))) + .thenReturn(TEST_SPI); + + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + mDestinationAddr, TEST_SPI, new Binder()); + assertEquals(IpSecManager.Status.OK, spiResp.status); + assertEquals(TEST_SPI, spiResp.spi); + } + + @Test + public void testReleaseSecurityParameterIndex() throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI))) + .thenReturn(TEST_SPI); + + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + mDestinationAddr, TEST_SPI, new Binder()); + + mIpSecService.releaseSecurityParameterIndex(spiResp.resourceId); + + verify(mMockNetd) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent); + try { + userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testSecurityParameterIndexBinderDeath() throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), eq(mDestinationAddr), eq(TEST_SPI))) + .thenReturn(TEST_SPI); + + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + mDestinationAddr, TEST_SPI, new Binder()); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId); + + refcountedRecord.binderDied(); + + verify(mMockNetd) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent); + try { + userRecord.mSpiRecords.getRefcountedResourceOrThrow(spiResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + private int getNewSpiResourceId(String remoteAddress, int returnSpi) throws Exception { + when(mMockNetd.ipSecAllocateSpi(anyInt(), anyString(), anyString(), anyInt())) + .thenReturn(returnSpi); + + IpSecSpiResponse spi = + mIpSecService.allocateSecurityParameterIndex( + InetAddresses.parseNumericAddress(remoteAddress).getHostAddress(), + IpSecManager.INVALID_SECURITY_PARAMETER_INDEX, + new Binder()); + return spi.resourceId; + } + + private void addDefaultSpisAndRemoteAddrToIpSecConfig(IpSecConfig config) throws Exception { + config.setSpiResourceId(getNewSpiResourceId(mDestinationAddr, TEST_SPI)); + config.setSourceAddress(mSourceAddr); + config.setDestinationAddress(mDestinationAddr); + } + + private void addAuthAndCryptToIpSecConfig(IpSecConfig config) throws Exception { + config.setEncryption(CRYPT_ALGO); + config.setAuthentication(AUTH_ALGO); + } + + private void addEncapSocketToIpSecConfig(int resourceId, IpSecConfig config) throws Exception { + config.setEncapType(IpSecTransform.ENCAP_ESPINUDP); + config.setEncapSocketResourceId(resourceId); + config.setEncapRemotePort(REMOTE_ENCAP_PORT); + } + + private void verifyTransformNetdCalledForCreatingSA( + IpSecConfig config, IpSecTransformResponse resp) throws Exception { + verifyTransformNetdCalledForCreatingSA(config, resp, 0); + } + + private void verifyTransformNetdCalledForCreatingSA( + IpSecConfig config, IpSecTransformResponse resp, int encapSocketPort) throws Exception { + IpSecAlgorithm auth = config.getAuthentication(); + IpSecAlgorithm crypt = config.getEncryption(); + IpSecAlgorithm authCrypt = config.getAuthenticatedEncryption(); + + verify(mMockNetd, times(1)) + .ipSecAddSecurityAssociation( + eq(mUid), + eq(config.getMode()), + eq(config.getSourceAddress()), + eq(config.getDestinationAddress()), + eq((config.getNetwork() != null) ? config.getNetwork().netId : 0), + eq(TEST_SPI), + eq(0), + eq(0), + eq((auth != null) ? auth.getName() : ""), + eq((auth != null) ? auth.getKey() : new byte[] {}), + eq((auth != null) ? auth.getTruncationLengthBits() : 0), + eq((crypt != null) ? crypt.getName() : ""), + eq((crypt != null) ? crypt.getKey() : new byte[] {}), + eq((crypt != null) ? crypt.getTruncationLengthBits() : 0), + eq((authCrypt != null) ? authCrypt.getName() : ""), + eq((authCrypt != null) ? authCrypt.getKey() : new byte[] {}), + eq((authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0), + eq(config.getEncapType()), + eq(encapSocketPort), + eq(config.getEncapRemotePort()), + eq(config.getXfrmInterfaceId())); + } + + @Test + public void testCreateTransform() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + @Test + public void testCreateTransformAead() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + + ipSecConfig.setAuthenticatedEncryption(AEAD_ALGO); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + @Test + public void testCreateTransportModeTransformWithEncap() throws Exception { + IpSecUdpEncapResponse udpSock = mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TRANSPORT); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + addEncapSocketToIpSecConfig(udpSock.resourceId, ipSecConfig); + + if (mFamily == AF_INET) { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp, udpSock.port); + } else { + try { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("Expected IllegalArgumentException on attempt to use UDP Encap in IPv6"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testCreateTunnelModeTransformWithEncap() throws Exception { + IpSecUdpEncapResponse udpSock = mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + addEncapSocketToIpSecConfig(udpSock.resourceId, ipSecConfig); + + if (mFamily == AF_INET) { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp, udpSock.port); + } else { + try { + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("Expected IllegalArgumentException on attempt to use UDP Encap in IPv6"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testCreateTwoTransformsWithSameSpis() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + assertEquals(IpSecManager.Status.OK, createTransformResp.status); + + // Attempting to create transform a second time with the same SPIs should throw an error... + try { + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("IpSecService should have thrown an error for reuse of SPI"); + } catch (IllegalStateException expected) { + } + + // ... even if the transform is deleted + mIpSecService.deleteTransform(createTransformResp.resourceId); + try { + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + fail("IpSecService should have thrown an error for reuse of SPI"); + } catch (IllegalStateException expected) { + } + } + + @Test + public void testReleaseOwnedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent); + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + verify(mMockNetd, times(0)) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + // quota is not released until the SPI is released by the Transform + assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent); + } + + @Test + public void testDeleteTransform() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + mIpSecService.deleteTransform(createTransformResp.resourceId); + + verify(mMockNetd, times(1)) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(0, userRecord.mTransformQuotaTracker.mCurrent); + assertEquals(1, userRecord.mSpiQuotaTracker.mCurrent); + + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + // Verify that ipSecDeleteSa was not called when the SPI was released because the + // ownedByTransform property should prevent it; (note, the called count is cumulative). + verify(mMockNetd, times(1)) + .ipSecDeleteSecurityAssociation( + anyInt(), + anyString(), + anyString(), + anyInt(), + anyInt(), + anyInt(), + anyInt()); + assertEquals(0, userRecord.mSpiQuotaTracker.mCurrent); + + try { + userRecord.mTransformRecords.getRefcountedResourceOrThrow( + createTransformResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testTransportModeTransformBinderDeath() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mTransformRecords.getRefcountedResourceOrThrow( + createTransformResp.resourceId); + + refcountedRecord.binderDied(); + + verify(mMockNetd) + .ipSecDeleteSecurityAssociation( + eq(mUid), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), + anyInt(), + anyInt()); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mTransformQuotaTracker.mCurrent); + try { + userRecord.mTransformRecords.getRefcountedResourceOrThrow( + createTransformResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testApplyTransportModeTransform() throws Exception { + verifyApplyTransportModeTransformCommon(false); + } + + @Test + public void testApplyTransportModeTransformReleasedSpi() throws Exception { + verifyApplyTransportModeTransformCommon(true); + } + + public void verifyApplyTransportModeTransformCommon( + boolean closeSpiBeforeApply) throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + + if (closeSpiBeforeApply) { + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + } + + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + + int resourceId = createTransformResp.resourceId; + mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId); + + verify(mMockNetd) + .ipSecApplyTransportModeTransform( + eq(pfd), + eq(mUid), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI)); + } + + @Test + public void testApplyTransportModeTransformWithClosedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + + // Close SPI record + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + + int resourceId = createTransformResp.resourceId; + mIpSecService.applyTransportModeTransform(pfd, IpSecManager.DIRECTION_OUT, resourceId); + + verify(mMockNetd) + .ipSecApplyTransportModeTransform( + eq(pfd), + eq(mUid), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI)); + } + + @Test + public void testRemoveTransportModeTransform() throws Exception { + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + mIpSecService.removeTransportModeTransforms(pfd); + + verify(mMockNetd).ipSecRemoveTransportModeTransform(pfd); + } + + private IpSecTunnelInterfaceResponse createAndValidateTunnel( + String localAddr, String remoteAddr, String pkgName) throws Exception { + final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel(); + config.flags = new String[] {IF_STATE_DOWN}; + when(mMockNetd.interfaceGetCfg(anyString())).thenReturn(config); + IpSecTunnelInterfaceResponse createTunnelResp = + mIpSecService.createTunnelInterface( + mSourceAddr, mDestinationAddr, fakeNetwork, new Binder(), pkgName); + + assertNotNull(createTunnelResp); + assertEquals(IpSecManager.Status.OK, createTunnelResp.status); + for (int direction : new int[] {DIRECTION_IN, DIRECTION_OUT, DIRECTION_FWD}) { + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd).ipSecAddSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(direction), + anyString(), + anyString(), + eq(0), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(createTunnelResp.resourceId)); + } + } + + return createTunnelResp; + } + + @Test + public void testCreateTunnelInterface() throws Exception { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + // Check that we have stored the tracking object, and retrieve it + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + + assertEquals(1, userRecord.mTunnelQuotaTracker.mCurrent); + verify(mMockNetd) + .ipSecAddTunnelInterface( + eq(createTunnelResp.interfaceName), + eq(mSourceAddr), + eq(mDestinationAddr), + anyInt(), + anyInt(), + anyInt()); + verify(mMockNetd).interfaceSetCfg(argThat( + config -> Arrays.asList(config.flags).contains(IF_STATE_UP))); + } + + @Test + public void testDeleteTunnelInterface() throws Exception { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + + mIpSecService.deleteTunnelInterface(createTunnelResp.resourceId, BLESSED_PACKAGE); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mTunnelQuotaTracker.mCurrent); + verify(mMockNetd).ipSecRemoveTunnelInterface(eq(createTunnelResp.interfaceName)); + try { + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + } + } + + private Network createFakeUnderlyingNetwork(String interfaceName) { + final Network fakeNetwork = new Network(1000); + final LinkProperties fakeLp = new LinkProperties(); + fakeLp.setInterfaceName(interfaceName); + when(mMockConnectivityMgr.getLinkProperties(eq(fakeNetwork))).thenReturn(fakeLp); + return fakeNetwork; + } + + @Test + public void testSetNetworkForTunnelInterface() throws Exception { + final IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + final Network newFakeNetwork = createFakeUnderlyingNetwork("newFakeNetworkInterface"); + final int tunnelIfaceResourceId = createTunnelResp.resourceId; + mIpSecService.setNetworkForTunnelInterface( + tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE); + + final IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(mUid); + assertEquals(1, userRecord.mTunnelQuotaTracker.mCurrent); + + final TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelIfaceResourceId); + assertEquals(newFakeNetwork, tunnelInterfaceInfo.getUnderlyingNetwork()); + } + + @Test + public void testSetNetworkForTunnelInterfaceFailsForInvalidResourceId() throws Exception { + final IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + final Network newFakeNetwork = new Network(1000); + + try { + mIpSecService.setNetworkForTunnelInterface( + IpSecManager.INVALID_RESOURCE_ID, newFakeNetwork, BLESSED_PACKAGE); + fail("Expected an IllegalArgumentException for invalid resource ID."); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testSetNetworkForTunnelInterfaceFailsWhenSettingTunnelNetwork() throws Exception { + final IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + final int tunnelIfaceResourceId = createTunnelResp.resourceId; + final IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(mUid); + final TunnelInterfaceRecord tunnelInterfaceInfo = + userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelIfaceResourceId); + + final Network newFakeNetwork = + createFakeUnderlyingNetwork(tunnelInterfaceInfo.getInterfaceName()); + + try { + mIpSecService.setNetworkForTunnelInterface( + tunnelIfaceResourceId, newFakeNetwork, BLESSED_PACKAGE); + fail( + "Expected an IllegalArgumentException because the underlying network is the" + + " network being exposed by this tunnel."); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testTunnelInterfaceBinderDeath() throws Exception { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + IpSecService.UserRecord userRecord = mIpSecService.mUserResourceTracker.getUserRecord(mUid); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + + refcountedRecord.binderDied(); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mTunnelQuotaTracker.mCurrent); + verify(mMockNetd).ipSecRemoveTunnelInterface(eq(createTunnelResp.interfaceName)); + try { + userRecord.mTunnelInterfaceRecords.getRefcountedResourceOrThrow( + createTunnelResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testApplyTunnelModeTransformOutbound() throws Exception { + verifyApplyTunnelModeTransformCommon(false /* closeSpiBeforeApply */, DIRECTION_OUT); + } + + @Test + public void testApplyTunnelModeTransformOutboundNonNetworkStack() throws Exception { + mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS); + verifyApplyTunnelModeTransformCommon(false /* closeSpiBeforeApply */, DIRECTION_OUT); + } + + @Test + public void testApplyTunnelModeTransformOutboundReleasedSpi() throws Exception { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_OUT); + } + + @Test + public void testApplyTunnelModeTransformInbound() throws Exception { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_IN); + } + + @Test + public void testApplyTunnelModeTransformInboundNonNetworkStack() throws Exception { + mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS); + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_IN); + } + + @Test + public void testApplyTunnelModeTransformForward() throws Exception { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_FWD); + } + + @Test + public void testApplyTunnelModeTransformForwardNonNetworkStack() throws Exception { + mTestContext.setAllowedPermissions(android.Manifest.permission.MANAGE_IPSEC_TUNNELS); + + try { + verifyApplyTunnelModeTransformCommon(true /* closeSpiBeforeApply */, DIRECTION_FWD); + fail("Expected security exception due to use of forward policies without NETWORK_STACK" + + " or MAINLINE_NETWORK_STACK permission"); + } catch (SecurityException expected) { + } + } + + public void verifyApplyTunnelModeTransformCommon(boolean closeSpiBeforeApply, int direction) + throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + if (closeSpiBeforeApply) { + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + } + + int transformResourceId = createTransformResp.resourceId; + int tunnelResourceId = createTunnelResp.resourceId; + mIpSecService.applyTunnelModeTransform( + tunnelResourceId, direction, transformResourceId, BLESSED_PACKAGE); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd) + .ipSecUpdateSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(direction), + anyString(), + anyString(), + eq(direction == DIRECTION_OUT ? TEST_SPI : 0), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(tunnelResourceId)); + } + + ipSecConfig.setXfrmInterfaceId(tunnelResourceId); + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + + @Test + public void testApplyTunnelModeTransformWithClosedSpi() throws Exception { + IpSecConfig ipSecConfig = new IpSecConfig(); + ipSecConfig.setMode(IpSecTransform.MODE_TUNNEL); + addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig); + addAuthAndCryptToIpSecConfig(ipSecConfig); + + IpSecTransformResponse createTransformResp = + mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE); + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BLESSED_PACKAGE); + + // Close SPI record + mIpSecService.releaseSecurityParameterIndex(ipSecConfig.getSpiResourceId()); + + int transformResourceId = createTransformResp.resourceId; + int tunnelResourceId = createTunnelResp.resourceId; + mIpSecService.applyTunnelModeTransform( + tunnelResourceId, IpSecManager.DIRECTION_OUT, transformResourceId, BLESSED_PACKAGE); + + for (int selAddrFamily : ADDRESS_FAMILIES) { + verify(mMockNetd) + .ipSecUpdateSecurityPolicy( + eq(mUid), + eq(selAddrFamily), + eq(IpSecManager.DIRECTION_OUT), + anyString(), + anyString(), + eq(TEST_SPI), + anyInt(), // iKey/oKey + anyInt(), // mask + eq(tunnelResourceId)); + } + + ipSecConfig.setXfrmInterfaceId(tunnelResourceId); + verifyTransformNetdCalledForCreatingSA(ipSecConfig, createTransformResp); + } + + @Test + public void testAddRemoveAddressFromTunnelInterface() throws Exception { + for (String pkgName : new String[] {BLESSED_PACKAGE, SYSTEM_PACKAGE}) { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, pkgName); + mIpSecService.addAddressToTunnelInterface( + createTunnelResp.resourceId, mLocalInnerAddress, pkgName); + verify(mMockNetd, times(1)) + .interfaceAddAddress( + eq(createTunnelResp.interfaceName), + eq(mLocalInnerAddress.getAddress().getHostAddress()), + eq(mLocalInnerAddress.getPrefixLength())); + mIpSecService.removeAddressFromTunnelInterface( + createTunnelResp.resourceId, mLocalInnerAddress, pkgName); + verify(mMockNetd, times(1)) + .interfaceDelAddress( + eq(createTunnelResp.interfaceName), + eq(mLocalInnerAddress.getAddress().getHostAddress()), + eq(mLocalInnerAddress.getPrefixLength())); + mIpSecService.deleteTunnelInterface(createTunnelResp.resourceId, pkgName); + } + } + + @Ignore + @Test + public void testAddTunnelFailsForBadPackageName() throws Exception { + try { + IpSecTunnelInterfaceResponse createTunnelResp = + createAndValidateTunnel(mSourceAddr, mDestinationAddr, BAD_PACKAGE); + fail("Expected a SecurityException for badPackage."); + } catch (SecurityException expected) { + } + } + + @Test + public void testFeatureFlagVerification() throws Exception { + when(mMockPkgMgr.hasSystemFeature(eq(PackageManager.FEATURE_IPSEC_TUNNELS))) + .thenReturn(false); + + try { + String addr = Inet4Address.getLoopbackAddress().getHostAddress(); + mIpSecService.createTunnelInterface( + addr, addr, new Network(0), new Binder(), BLESSED_PACKAGE); + fail("Expected UnsupportedOperationException for disabled feature"); + } catch (UnsupportedOperationException expected) { + } + } +} diff --git a/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..22a2c94fc194e818d3c86b45175205a53f6cbbfd --- /dev/null +++ b/tests/unit/java/com/android/server/IpSecServiceRefcountedResourceTest.java @@ -0,0 +1,358 @@ +/* + * 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.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.IpSecService.IResource; +import com.android.server.IpSecService.RefcountedResource; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +/** Unit tests for {@link IpSecService.RefcountedResource}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecServiceRefcountedResourceTest { + Context mMockContext; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + + @Before + public void setUp() throws Exception { + mMockContext = mock(Context.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); + } + + private void assertResourceState( + RefcountedResource<IResource> resource, + int refCount, + int userReleaseCallCount, + int releaseReferenceCallCount, + int invalidateCallCount, + int freeUnderlyingResourcesCallCount) + throws RemoteException { + // Check refcount on RefcountedResource + assertEquals(refCount, resource.mRefCount); + + // Check call count of RefcountedResource + verify(resource, times(userReleaseCallCount)).userRelease(); + verify(resource, times(releaseReferenceCallCount)).releaseReference(); + + // Check call count of IResource + verify(resource.getResource(), times(invalidateCallCount)).invalidate(); + verify(resource.getResource(), times(freeUnderlyingResourcesCallCount)) + .freeUnderlyingResources(); + } + + /** Adds mockito instrumentation */ + private RefcountedResource<IResource> getTestRefcountedResource( + RefcountedResource... children) { + return getTestRefcountedResource(new Binder(), children); + } + + /** Adds mockito instrumentation with provided binder */ + private RefcountedResource<IResource> getTestRefcountedResource( + IBinder binder, RefcountedResource... children) { + return spy( + mIpSecService + .new RefcountedResource<IResource>(mock(IResource.class), binder, children)); + } + + @Test + public void testConstructor() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + RefcountedResource<IResource> resource = getTestRefcountedResource(binderMock); + + // Verify resource's refcount starts at 1 (for user-reference) + assertResourceState(resource, 1, 0, 0, 0, 0); + + // Verify linking to binder death + verify(binderMock).linkToDeath(anyObject(), anyInt()); + } + + @Test + public void testConstructorWithChildren() throws RemoteException { + IBinder binderMockChild = mock(IBinder.class); + IBinder binderMockParent = mock(IBinder.class); + RefcountedResource<IResource> childResource = getTestRefcountedResource(binderMockChild); + RefcountedResource<IResource> parentResource = + getTestRefcountedResource(binderMockParent, childResource); + + // Verify parent's refcount starts at 1 (for user-reference) + assertResourceState(parentResource, 1, 0, 0, 0, 0); + + // Verify child's refcounts were incremented + assertResourceState(childResource, 2, 0, 0, 0, 0); + + // Verify linking to binder death + verify(binderMockChild).linkToDeath(anyObject(), anyInt()); + verify(binderMockParent).linkToDeath(anyObject(), anyInt()); + } + + @Test + public void testFailLinkToDeath() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + doThrow(new RemoteException()).when(binderMock).linkToDeath(anyObject(), anyInt()); + + try { + getTestRefcountedResource(binderMock); + fail("Expected exception to propogate when binder fails to link to death"); + } catch (RuntimeException expected) { + } + } + + @Test + public void testCleanupAndRelease() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(binderMock); + + // Verify user-initiated cleanup path decrements refcount and calls full cleanup flow + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + // Verify user-initated cleanup path unlinks from binder + verify(binderMock).unlinkToDeath(eq(refcountedResource), eq(0)); + assertNull(refcountedResource.mBinder); + } + + @Test + public void testMultipleCallsToCleanupAndRelease() throws RemoteException { + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(); + + // Verify calling userRelease multiple times does not trigger any other cleanup + // methods + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + refcountedResource.userRelease(); + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 3, 1, 1, 1); + } + + @Test + public void testBinderDeathAfterCleanupAndReleaseDoesNothing() throws RemoteException { + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(); + + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + // Verify binder death call does not trigger any other cleanup methods if called after + // userRelease() + refcountedResource.binderDied(); + assertResourceState(refcountedResource, -1, 2, 1, 1, 1); + } + + @Test + public void testBinderDeath() throws RemoteException { + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(); + + // Verify binder death caused cleanup + refcountedResource.binderDied(); + verify(refcountedResource, times(1)).binderDied(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + assertNull(refcountedResource.mBinder); + } + + @Test + public void testCleanupParentDecrementsChildRefcount() throws RemoteException { + RefcountedResource<IResource> childResource = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource = getTestRefcountedResource(childResource); + + parentResource.userRelease(); + + // Verify parent gets cleaned up properly, and triggers releaseReference on + // child + assertResourceState(childResource, 1, 0, 1, 0, 0); + assertResourceState(parentResource, -1, 1, 1, 1, 1); + } + + @Test + public void testCleanupReferencedChildDoesNotTriggerRelease() throws RemoteException { + RefcountedResource<IResource> childResource = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource = getTestRefcountedResource(childResource); + + childResource.userRelease(); + + // Verify that child does not clean up kernel resources and quota. + assertResourceState(childResource, 1, 1, 1, 1, 0); + assertResourceState(parentResource, 1, 0, 0, 0, 0); + } + + @Test + public void testTwoParents() throws RemoteException { + RefcountedResource<IResource> childResource = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource1 = getTestRefcountedResource(childResource); + RefcountedResource<IResource> parentResource2 = getTestRefcountedResource(childResource); + + // Verify that child does not cleanup kernel resources and quota until all references + // have been released. Assumption: parents release correctly based on + // testCleanupParentDecrementsChildRefcount() + childResource.userRelease(); + assertResourceState(childResource, 2, 1, 1, 1, 0); + + parentResource1.userRelease(); + assertResourceState(childResource, 1, 1, 2, 1, 0); + + parentResource2.userRelease(); + assertResourceState(childResource, -1, 1, 3, 1, 1); + } + + @Test + public void testTwoChildren() throws RemoteException { + RefcountedResource<IResource> childResource1 = getTestRefcountedResource(); + RefcountedResource<IResource> childResource2 = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource = + getTestRefcountedResource(childResource1, childResource2); + + childResource1.userRelease(); + assertResourceState(childResource1, 1, 1, 1, 1, 0); + assertResourceState(childResource2, 2, 0, 0, 0, 0); + + parentResource.userRelease(); + assertResourceState(childResource1, -1, 1, 2, 1, 1); + assertResourceState(childResource2, 1, 0, 1, 0, 0); + + childResource2.userRelease(); + assertResourceState(childResource1, -1, 1, 2, 1, 1); + assertResourceState(childResource2, -1, 1, 2, 1, 1); + } + + @Test + public void testSampleUdpEncapTranform() throws RemoteException { + RefcountedResource<IResource> spi1 = getTestRefcountedResource(); + RefcountedResource<IResource> spi2 = getTestRefcountedResource(); + RefcountedResource<IResource> udpEncapSocket = getTestRefcountedResource(); + RefcountedResource<IResource> transform = + getTestRefcountedResource(spi1, spi2, udpEncapSocket); + + // Pretend one SPI goes out of reference (releaseManagedResource -> userRelease) + spi1.userRelease(); + + // User called releaseManagedResource on udpEncap socket + udpEncapSocket.userRelease(); + + // User dies, and binder kills the rest + spi2.binderDied(); + transform.binderDied(); + + // Check resource states + assertResourceState(spi1, -1, 1, 2, 1, 1); + assertResourceState(spi2, -1, 1, 2, 1, 1); + assertResourceState(udpEncapSocket, -1, 1, 2, 1, 1); + assertResourceState(transform, -1, 1, 1, 1, 1); + } + + @Test + public void testSampleDualTransformEncapSocket() throws RemoteException { + RefcountedResource<IResource> spi1 = getTestRefcountedResource(); + RefcountedResource<IResource> spi2 = getTestRefcountedResource(); + RefcountedResource<IResource> spi3 = getTestRefcountedResource(); + RefcountedResource<IResource> spi4 = getTestRefcountedResource(); + RefcountedResource<IResource> udpEncapSocket = getTestRefcountedResource(); + RefcountedResource<IResource> transform1 = + getTestRefcountedResource(spi1, spi2, udpEncapSocket); + RefcountedResource<IResource> transform2 = + getTestRefcountedResource(spi3, spi4, udpEncapSocket); + + // Pretend one SPIs goes out of reference (releaseManagedResource -> userRelease) + spi1.userRelease(); + + // User called releaseManagedResource on udpEncap socket and spi4 + udpEncapSocket.userRelease(); + spi4.userRelease(); + + // User dies, and binder kills the rest + spi2.binderDied(); + spi3.binderDied(); + transform2.binderDied(); + transform1.binderDied(); + + // Check resource states + assertResourceState(spi1, -1, 1, 2, 1, 1); + assertResourceState(spi2, -1, 1, 2, 1, 1); + assertResourceState(spi3, -1, 1, 2, 1, 1); + assertResourceState(spi4, -1, 1, 2, 1, 1); + assertResourceState(udpEncapSocket, -1, 1, 3, 1, 1); + assertResourceState(transform1, -1, 1, 1, 1, 1); + assertResourceState(transform2, -1, 1, 1, 1, 1); + } + + @Test + public void fuzzTest() throws RemoteException { + List<RefcountedResource<IResource>> resources = new ArrayList<>(); + + // Build a tree of resources + for (int i = 0; i < 100; i++) { + // Choose a random number of children from the existing list + int numChildren = ThreadLocalRandom.current().nextInt(0, resources.size() + 1); + + // Build a (random) list of children + Set<RefcountedResource<IResource>> children = new HashSet<>(); + for (int j = 0; j < numChildren; j++) { + int childIndex = ThreadLocalRandom.current().nextInt(0, resources.size()); + children.add(resources.get(childIndex)); + } + + RefcountedResource<IResource> newRefcountedResource = + getTestRefcountedResource( + children.toArray(new RefcountedResource[children.size()])); + resources.add(newRefcountedResource); + } + + // Cleanup all resources in a random order + List<RefcountedResource<IResource>> clonedResources = + new ArrayList<>(resources); // shallow copy + while (!clonedResources.isEmpty()) { + int index = ThreadLocalRandom.current().nextInt(0, clonedResources.size()); + RefcountedResource<IResource> refcountedResource = clonedResources.get(index); + refcountedResource.userRelease(); + clonedResources.remove(index); + } + + // Verify all resources were cleaned up properly + for (RefcountedResource<IResource> refcountedResource : resources) { + assertEquals(-1, refcountedResource.mRefCount); + } + } +} diff --git a/tests/unit/java/com/android/server/IpSecServiceTest.java b/tests/unit/java/com/android/server/IpSecServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6232423b4f9ecb822c202246c89840d7294475a4 --- /dev/null +++ b/tests/unit/java/com/android/server/IpSecServiceTest.java @@ -0,0 +1,670 @@ +/* + * 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.server; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.EADDRINUSE; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; + +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 static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.IpSecAlgorithm; +import android.net.IpSecConfig; +import android.net.IpSecManager; +import android.net.IpSecSpiResponse; +import android.net.IpSecUdpEncapResponse; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStat; +import android.util.Range; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import dalvik.system.SocketTagger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +/** Unit tests for {@link IpSecService}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecServiceTest { + + private static final int DROID_SPI = 0xD1201D; + private static final int MAX_NUM_ENCAP_SOCKETS = 100; + private static final int MAX_NUM_SPIS = 100; + private static final int TEST_UDP_ENCAP_INVALID_PORT = 100; + private static final int TEST_UDP_ENCAP_PORT_OUT_RANGE = 100000; + + private static final InetAddress INADDR_ANY; + + private static final byte[] AEAD_KEY = { + 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, + 0x73, 0x61, 0x6C, 0x74 + }; + private static final byte[] CRYPT_KEY = { + 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 + }; + private static final byte[] AUTH_KEY = { + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, + 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F + }; + + private static final IpSecAlgorithm AUTH_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_HMAC_SHA256, AUTH_KEY, AUTH_KEY.length * 4); + private static final IpSecAlgorithm CRYPT_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.CRYPT_AES_CBC, CRYPT_KEY); + private static final IpSecAlgorithm AEAD_ALGO = + new IpSecAlgorithm(IpSecAlgorithm.AUTH_CRYPT_AES_GCM, AEAD_KEY, 128); + + static { + try { + INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0}); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + Context mMockContext; + INetd mMockNetd; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + + @Before + public void setUp() throws Exception { + mMockContext = mock(Context.class); + mMockNetd = mock(INetd.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); + + // Injecting mock netd + when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd); + } + + @Test + public void testIpSecServiceCreate() throws InterruptedException { + IpSecService ipSecSrv = IpSecService.create(mMockContext); + assertNotNull(ipSecSrv); + } + + @Test + public void testReleaseInvalidSecurityParameterIndex() throws Exception { + try { + mIpSecService.releaseSecurityParameterIndex(1); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + /** This function finds an available port */ + int findUnusedPort() throws Exception { + // Get an available port. + ServerSocket s = new ServerSocket(0); + int port = s.getLocalPort(); + s.close(); + return port; + } + + @Test + public void testOpenAndCloseUdpEncapsulationSocket() throws Exception { + int localport = -1; + IpSecUdpEncapResponse udpEncapResp = null; + + for (int i = 0; i < IpSecService.MAX_PORT_BIND_ATTEMPTS; i++) { + localport = findUnusedPort(); + + udpEncapResp = mIpSecService.openUdpEncapsulationSocket(localport, new Binder()); + assertNotNull(udpEncapResp); + if (udpEncapResp.status == IpSecManager.Status.OK) { + break; + } + + // Else retry to reduce possibility for port-bind failures. + } + + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + assertEquals(localport, udpEncapResp.port); + + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + + // Verify quota and RefcountedResource objects cleaned up + IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(Os.getuid()); + assertEquals(0, userRecord.mSocketQuotaTracker.mCurrent); + try { + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(udpEncapResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testUdpEncapsulationSocketBinderDeath() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + IpSecService.UserRecord userRecord = + mIpSecService.mUserResourceTracker.getUserRecord(Os.getuid()); + IpSecService.RefcountedResource refcountedRecord = + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow( + udpEncapResp.resourceId); + + refcountedRecord.binderDied(); + + // Verify quota and RefcountedResource objects cleaned up + assertEquals(0, userRecord.mSocketQuotaTracker.mCurrent); + try { + userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(udpEncapResp.resourceId); + fail("Expected IllegalArgumentException on attempt to access deleted resource"); + } catch (IllegalArgumentException expected) { + + } + } + + @Test + public void testOpenUdpEncapsulationSocketAfterClose() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + int localport = udpEncapResp.port; + + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + + /** Check if localport is available. */ + FileDescriptor newSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Os.bind(newSocket, INADDR_ANY, localport); + Os.close(newSocket); + } + + /** + * This function checks if the IpSecService holds the reserved port. If + * closeUdpEncapsulationSocket is not called, the socket cleanup should not be complete. + */ + @Test + public void testUdpEncapPortNotReleased() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + int localport = udpEncapResp.port; + + udpEncapResp.fileDescriptor.close(); + + FileDescriptor newSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + try { + Os.bind(newSocket, INADDR_ANY, localport); + fail("ErrnoException not thrown"); + } catch (ErrnoException e) { + assertEquals(EADDRINUSE, e.errno); + } + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + } + + @Test + public void testOpenUdpEncapsulationSocketOnRandomPort() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + assertNotEquals(0, udpEncapResp.port); + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + } + + @Test + public void testOpenUdpEncapsulationSocketPortRange() throws Exception { + try { + mIpSecService.openUdpEncapsulationSocket(TEST_UDP_ENCAP_INVALID_PORT, new Binder()); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + + try { + mIpSecService.openUdpEncapsulationSocket(TEST_UDP_ENCAP_PORT_OUT_RANGE, new Binder()); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testOpenUdpEncapsulationSocketTwice() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + int localport = udpEncapResp.port; + + IpSecUdpEncapResponse testUdpEncapResp = + mIpSecService.openUdpEncapsulationSocket(localport, new Binder()); + assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, testUdpEncapResp.status); + + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + } + + @Test + public void testCloseInvalidUdpEncapsulationSocket() throws Exception { + try { + mIpSecService.closeUdpEncapsulationSocket(1); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testValidateAlgorithmsAuth() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setAuthentication(AUTH_ALGO); + mIpSecService.validateAlgorithms(config); + + // Validate that incorrect algorithm types fails + for (IpSecAlgorithm algo : new IpSecAlgorithm[] {CRYPT_ALGO, AEAD_ALGO}) { + try { + config = new IpSecConfig(); + config.setAuthentication(algo); + mIpSecService.validateAlgorithms(config); + fail("Did not throw exception on invalid algorithm type"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testValidateAlgorithmsCrypt() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setEncryption(CRYPT_ALGO); + mIpSecService.validateAlgorithms(config); + + // Validate that incorrect algorithm types fails + for (IpSecAlgorithm algo : new IpSecAlgorithm[] {AUTH_ALGO, AEAD_ALGO}) { + try { + config = new IpSecConfig(); + config.setEncryption(algo); + mIpSecService.validateAlgorithms(config); + fail("Did not throw exception on invalid algorithm type"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testValidateAlgorithmsAead() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + mIpSecService.validateAlgorithms(config); + + // Validate that incorrect algorithm types fails + for (IpSecAlgorithm algo : new IpSecAlgorithm[] {AUTH_ALGO, CRYPT_ALGO}) { + try { + config = new IpSecConfig(); + config.setAuthenticatedEncryption(algo); + mIpSecService.validateAlgorithms(config); + fail("Did not throw exception on invalid algorithm type"); + } catch (IllegalArgumentException expected) { + } + } + } + + @Test + public void testValidateAlgorithmsAuthCrypt() { + // Validate that correct algorithm type succeeds + IpSecConfig config = new IpSecConfig(); + config.setAuthentication(AUTH_ALGO); + config.setEncryption(CRYPT_ALGO); + mIpSecService.validateAlgorithms(config); + } + + @Test + public void testValidateAlgorithmsNoAlgorithms() { + IpSecConfig config = new IpSecConfig(); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; no algorithms specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testValidateAlgorithmsAeadWithAuth() { + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + config.setAuthentication(AUTH_ALGO); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; both AEAD and auth algorithm specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testValidateAlgorithmsAeadWithCrypt() { + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + config.setEncryption(CRYPT_ALGO); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; both AEAD and crypt algorithm specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testValidateAlgorithmsAeadWithAuthAndCrypt() { + IpSecConfig config = new IpSecConfig(); + config.setAuthenticatedEncryption(AEAD_ALGO); + config.setAuthentication(AUTH_ALGO); + config.setEncryption(CRYPT_ALGO); + try { + mIpSecService.validateAlgorithms(config); + fail("Expected exception; AEAD, auth and crypt algorithm specified"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testDeleteInvalidTransform() throws Exception { + try { + mIpSecService.deleteTransform(1); + fail("IllegalArgumentException not thrown"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testRemoveTransportModeTransform() throws Exception { + Socket socket = new Socket(); + socket.bind(null); + ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket); + mIpSecService.removeTransportModeTransforms(pfd); + + verify(mMockNetd).ipSecRemoveTransportModeTransform(pfd); + } + + @Test + public void testValidateIpAddresses() throws Exception { + String[] invalidAddresses = + new String[] {"www.google.com", "::", "2001::/64", "0.0.0.0", ""}; + for (String address : invalidAddresses) { + try { + IpSecSpiResponse spiResp = + mIpSecService.allocateSecurityParameterIndex( + address, DROID_SPI, new Binder()); + fail("Invalid address was passed through IpSecService validation: " + address); + } catch (IllegalArgumentException e) { + } catch (Exception e) { + fail( + "Invalid InetAddress was not caught in validation: " + + address + + ", Exception: " + + e); + } + } + } + + /** + * This function checks if the number of encap UDP socket that one UID can reserve has a + * reasonable limit. + */ + @Test + public void testSocketResourceTrackerLimitation() throws Exception { + List<IpSecUdpEncapResponse> openUdpEncapSockets = new ArrayList<IpSecUdpEncapResponse>(); + // Reserve sockets until it fails. + for (int i = 0; i < MAX_NUM_ENCAP_SOCKETS; i++) { + IpSecUdpEncapResponse newUdpEncapSocket = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(newUdpEncapSocket); + if (IpSecManager.Status.OK != newUdpEncapSocket.status) { + break; + } + openUdpEncapSockets.add(newUdpEncapSocket); + } + // Assert that the total sockets quota has a reasonable limit. + assertTrue("No UDP encap socket was open", !openUdpEncapSockets.isEmpty()); + assertTrue( + "Number of open UDP encap sockets is out of bound", + openUdpEncapSockets.size() < MAX_NUM_ENCAP_SOCKETS); + + // Try to reserve one more UDP encapsulation socket, and should fail. + IpSecUdpEncapResponse extraUdpEncapSocket = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(extraUdpEncapSocket); + assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, extraUdpEncapSocket.status); + + // Close one of the open UDP encapsulation sockets. + mIpSecService.closeUdpEncapsulationSocket(openUdpEncapSockets.get(0).resourceId); + openUdpEncapSockets.get(0).fileDescriptor.close(); + openUdpEncapSockets.remove(0); + + // Try to reserve one more UDP encapsulation socket, and should be successful. + extraUdpEncapSocket = mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(extraUdpEncapSocket); + assertEquals(IpSecManager.Status.OK, extraUdpEncapSocket.status); + openUdpEncapSockets.add(extraUdpEncapSocket); + + // Close open UDP sockets. + for (IpSecUdpEncapResponse openSocket : openUdpEncapSockets) { + mIpSecService.closeUdpEncapsulationSocket(openSocket.resourceId); + openSocket.fileDescriptor.close(); + } + } + + /** + * This function checks if the number of SPI that one UID can reserve has a reasonable limit. + * This test does not test for both address families or duplicate SPIs because resource tracking + * code does not depend on them. + */ + @Test + public void testSpiResourceTrackerLimitation() throws Exception { + List<IpSecSpiResponse> reservedSpis = new ArrayList<IpSecSpiResponse>(); + // Return the same SPI for all SPI allocation since IpSecService only + // tracks the resource ID. + when(mMockNetd.ipSecAllocateSpi( + anyInt(), + anyString(), + eq(InetAddress.getLoopbackAddress().getHostAddress()), + anyInt())) + .thenReturn(DROID_SPI); + // Reserve spis until it fails. + for (int i = 0; i < MAX_NUM_SPIS; i++) { + IpSecSpiResponse newSpi = + mIpSecService.allocateSecurityParameterIndex( + InetAddress.getLoopbackAddress().getHostAddress(), + DROID_SPI + i, + new Binder()); + assertNotNull(newSpi); + if (IpSecManager.Status.OK != newSpi.status) { + break; + } + reservedSpis.add(newSpi); + } + // Assert that the SPI quota has a reasonable limit. + assertTrue(reservedSpis.size() > 0 && reservedSpis.size() < MAX_NUM_SPIS); + + // Try to reserve one more SPI, and should fail. + IpSecSpiResponse extraSpi = + mIpSecService.allocateSecurityParameterIndex( + InetAddress.getLoopbackAddress().getHostAddress(), + DROID_SPI + MAX_NUM_SPIS, + new Binder()); + assertNotNull(extraSpi); + assertEquals(IpSecManager.Status.RESOURCE_UNAVAILABLE, extraSpi.status); + + // Release one reserved spi. + mIpSecService.releaseSecurityParameterIndex(reservedSpis.get(0).resourceId); + reservedSpis.remove(0); + + // Should successfully reserve one more spi. + extraSpi = + mIpSecService.allocateSecurityParameterIndex( + InetAddress.getLoopbackAddress().getHostAddress(), + DROID_SPI + MAX_NUM_SPIS, + new Binder()); + assertNotNull(extraSpi); + assertEquals(IpSecManager.Status.OK, extraSpi.status); + + // Release reserved SPIs. + for (IpSecSpiResponse spiResp : reservedSpis) { + mIpSecService.releaseSecurityParameterIndex(spiResp.resourceId); + } + } + + @Test + public void testUidFdtagger() throws Exception { + SocketTagger actualSocketTagger = SocketTagger.get(); + + try { + FileDescriptor sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + + // Has to be done after socket creation because BlockGuardOS calls tag on new sockets + SocketTagger mockSocketTagger = mock(SocketTagger.class); + SocketTagger.set(mockSocketTagger); + + mIpSecService.mUidFdTagger.tag(sockFd, Process.LAST_APPLICATION_UID); + verify(mockSocketTagger).tag(eq(sockFd)); + } finally { + SocketTagger.set(actualSocketTagger); + } + } + + /** + * Checks if two file descriptors point to the same file. + * + * <p>According to stat.h documentation, the correct way to check for equivalent or duplicated + * file descriptors is to check their inode and device. These two entries uniquely identify any + * file. + */ + private boolean fileDescriptorsEqual(FileDescriptor fd1, FileDescriptor fd2) { + try { + StructStat fd1Stat = Os.fstat(fd1); + StructStat fd2Stat = Os.fstat(fd2); + + return fd1Stat.st_ino == fd2Stat.st_ino && fd1Stat.st_dev == fd2Stat.st_dev; + } catch (ErrnoException e) { + return false; + } + } + + @Test + public void testOpenUdpEncapSocketTagsSocket() throws Exception { + IpSecService.UidFdTagger mockTagger = mock(IpSecService.UidFdTagger.class); + IpSecService testIpSecService = new IpSecService( + mMockContext, mMockIpSecSrvConfig, mockTagger); + + IpSecUdpEncapResponse udpEncapResp = + testIpSecService.openUdpEncapsulationSocket(0, new Binder()); + assertNotNull(udpEncapResp); + assertEquals(IpSecManager.Status.OK, udpEncapResp.status); + + FileDescriptor sockFd = udpEncapResp.fileDescriptor.getFileDescriptor(); + ArgumentMatcher<FileDescriptor> fdMatcher = + (argFd) -> { + return fileDescriptorsEqual(sockFd, argFd); + }; + verify(mockTagger).tag(argThat(fdMatcher), eq(Os.getuid())); + + testIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + udpEncapResp.fileDescriptor.close(); + } + + @Test + public void testOpenUdpEncapsulationSocketCallsSetEncapSocketOwner() throws Exception { + IpSecUdpEncapResponse udpEncapResp = + mIpSecService.openUdpEncapsulationSocket(0, new Binder()); + + FileDescriptor sockFd = udpEncapResp.fileDescriptor.getFileDescriptor(); + ArgumentMatcher<ParcelFileDescriptor> fdMatcher = (arg) -> { + try { + StructStat sockStat = Os.fstat(sockFd); + StructStat argStat = Os.fstat(arg.getFileDescriptor()); + + return sockStat.st_ino == argStat.st_ino + && sockStat.st_dev == argStat.st_dev; + } catch (ErrnoException e) { + return false; + } + }; + + verify(mMockNetd).ipSecSetEncapSocketOwner(argThat(fdMatcher), eq(Os.getuid())); + mIpSecService.closeUdpEncapsulationSocket(udpEncapResp.resourceId); + } + + @Test + public void testReserveNetId() { + final Range<Integer> netIdRange = ConnectivityManager.getIpSecNetIdRange(); + for (int netId = netIdRange.getLower(); netId <= netIdRange.getUpper(); netId++) { + assertEquals(netId, mIpSecService.reserveNetId()); + } + + // Check that resource exhaustion triggers an exception + try { + mIpSecService.reserveNetId(); + fail("Did not throw error for all netIds reserved"); + } catch (IllegalStateException expected) { + } + + // Now release one and try again + int releasedNetId = + netIdRange.getLower() + (netIdRange.getUpper() - netIdRange.getLower()) / 2; + mIpSecService.releaseNetId(releasedNetId); + assertEquals(releasedNetId, mIpSecService.reserveNetId()); + } +} diff --git a/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ec111954fccd5fe71b1bb74e9f9bc4f97c92bd6 --- /dev/null +++ b/tests/unit/java/com/android/server/LegacyTypeTrackerTest.kt @@ -0,0 +1,197 @@ +/* + * 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. + */ + +// Don't warn about deprecated types anywhere in this test, because LegacyTypeTracker's very reason +// for existence is to power deprecated APIs. The annotation has to apply to the whole file because +// otherwise warnings will be generated by the imports of deprecated constants like TYPE_xxx. +@file:Suppress("DEPRECATION") + +package com.android.server + +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PackageManager.FEATURE_WIFI +import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT +import android.net.ConnectivityManager.TYPE_ETHERNET +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_MOBILE_CBS +import android.net.ConnectivityManager.TYPE_MOBILE_DUN +import android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY +import android.net.ConnectivityManager.TYPE_MOBILE_FOTA +import android.net.ConnectivityManager.TYPE_MOBILE_HIPRI +import android.net.ConnectivityManager.TYPE_MOBILE_IA +import android.net.ConnectivityManager.TYPE_MOBILE_IMS +import android.net.ConnectivityManager.TYPE_MOBILE_MMS +import android.net.ConnectivityManager.TYPE_MOBILE_SUPL +import android.net.ConnectivityManager.TYPE_VPN +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.ConnectivityManager.TYPE_WIFI_P2P +import android.net.ConnectivityManager.TYPE_WIMAX +import android.net.EthernetManager +import android.net.NetworkInfo.DetailedState.CONNECTED +import android.net.NetworkInfo.DetailedState.DISCONNECTED +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.ConnectivityService.LegacyTypeTracker +import com.android.server.connectivity.NetworkAgentInfo +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +const val UNSUPPORTED_TYPE = TYPE_WIMAX + +@RunWith(AndroidJUnit4::class) +@SmallTest +class LegacyTypeTrackerTest { + private val supportedTypes = arrayOf(TYPE_WIFI, TYPE_WIFI_P2P, TYPE_ETHERNET, TYPE_MOBILE, + TYPE_MOBILE_SUPL, TYPE_MOBILE_MMS, TYPE_MOBILE_SUPL, TYPE_MOBILE_DUN, TYPE_MOBILE_HIPRI, + TYPE_MOBILE_FOTA, TYPE_MOBILE_IMS, TYPE_MOBILE_CBS, TYPE_MOBILE_IA, + TYPE_MOBILE_EMERGENCY, TYPE_VPN) + + private val mMockService = mock(ConnectivityService::class.java).apply { + doReturn(false).`when`(this).isDefaultNetwork(any()) + } + private val mPm = mock(PackageManager::class.java) + private val mContext = mock(Context::class.java).apply { + doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI) + doReturn(true).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT) + doReturn(mPm).`when`(this).packageManager + doReturn(mock(EthernetManager::class.java)).`when`(this).getSystemService( + Context.ETHERNET_SERVICE) + } + private val mTm = mock(TelephonyManager::class.java).apply { + doReturn(true).`when`(this).isDataCapable + } + + private fun makeTracker() = LegacyTypeTracker(mMockService).apply { + loadSupportedTypes(mContext, mTm) + } + + @Test + fun testSupportedTypes() { + val tracker = makeTracker() + supportedTypes.forEach { + assertTrue(tracker.isTypeSupported(it)) + } + assertFalse(tracker.isTypeSupported(UNSUPPORTED_TYPE)) + } + + @Test + fun testSupportedTypes_NoEthernet() { + doReturn(null).`when`(mContext).getSystemService(Context.ETHERNET_SERVICE) + assertFalse(makeTracker().isTypeSupported(TYPE_ETHERNET)) + } + + @Test + fun testSupportedTypes_NoTelephony() { + doReturn(false).`when`(mTm).isDataCapable + val tracker = makeTracker() + val nonMobileTypes = arrayOf(TYPE_WIFI, TYPE_WIFI_P2P, TYPE_ETHERNET, TYPE_VPN) + nonMobileTypes.forEach { + assertTrue(tracker.isTypeSupported(it)) + } + supportedTypes.toSet().minus(nonMobileTypes).forEach { + assertFalse(tracker.isTypeSupported(it)) + } + } + + @Test + fun testSupportedTypes_NoWifiDirect() { + doReturn(false).`when`(mPm).hasSystemFeature(FEATURE_WIFI_DIRECT) + val tracker = makeTracker() + assertFalse(tracker.isTypeSupported(TYPE_WIFI_P2P)) + supportedTypes.toSet().minus(TYPE_WIFI_P2P).forEach { + assertTrue(tracker.isTypeSupported(it)) + } + } + + @Test + fun testSupl() { + val tracker = makeTracker() + val mobileNai = mock(NetworkAgentInfo::class.java) + tracker.add(TYPE_MOBILE, mobileNai) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE) + reset(mMockService) + tracker.add(TYPE_MOBILE_SUPL, mobileNai) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE_SUPL) + reset(mMockService) + tracker.remove(TYPE_MOBILE_SUPL, mobileNai, false /* wasDefault */) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE_SUPL) + reset(mMockService) + tracker.add(TYPE_MOBILE_SUPL, mobileNai) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, CONNECTED, TYPE_MOBILE_SUPL) + reset(mMockService) + tracker.remove(mobileNai, false) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE_SUPL) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai, DISCONNECTED, TYPE_MOBILE) + } + + @Test + fun testAddNetwork() { + val tracker = makeTracker() + val mobileNai = mock(NetworkAgentInfo::class.java) + val wifiNai = mock(NetworkAgentInfo::class.java) + tracker.add(TYPE_MOBILE, mobileNai) + tracker.add(TYPE_WIFI, wifiNai) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Make sure adding a second NAI does not change the results. + val secondMobileNai = mock(NetworkAgentInfo::class.java) + tracker.add(TYPE_MOBILE, secondMobileNai) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Make sure removing a network that wasn't added for this type is a no-op. + tracker.remove(TYPE_MOBILE, wifiNai, false /* wasDefault */) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), mobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Remove the top network for mobile and make sure the second one becomes the network + // of record for this type. + tracker.remove(TYPE_MOBILE, mobileNai, false /* wasDefault */) + assertSame(tracker.getNetworkForType(TYPE_MOBILE), secondMobileNai) + assertSame(tracker.getNetworkForType(TYPE_WIFI), wifiNai) + // Make sure adding a network for an unsupported type does not register it. + tracker.add(UNSUPPORTED_TYPE, mobileNai) + assertNull(tracker.getNetworkForType(UNSUPPORTED_TYPE)) + } + + @Test + fun testBroadcastOnDisconnect() { + val tracker = makeTracker() + val mobileNai1 = mock(NetworkAgentInfo::class.java) + val mobileNai2 = mock(NetworkAgentInfo::class.java) + doReturn(false).`when`(mMockService).isDefaultNetwork(mobileNai1) + tracker.add(TYPE_MOBILE, mobileNai1) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai1, CONNECTED, TYPE_MOBILE) + reset(mMockService) + doReturn(false).`when`(mMockService).isDefaultNetwork(mobileNai2) + tracker.add(TYPE_MOBILE, mobileNai2) + verify(mMockService, never()).sendLegacyNetworkBroadcast(any(), any(), anyInt()) + tracker.remove(TYPE_MOBILE, mobileNai1, false /* wasDefault */) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai1, DISCONNECTED, TYPE_MOBILE) + verify(mMockService).sendLegacyNetworkBroadcast(mobileNai2, CONNECTED, TYPE_MOBILE) + } +} diff --git a/tests/unit/java/com/android/server/NetIdManagerTest.kt b/tests/unit/java/com/android/server/NetIdManagerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6f5e740d344c34bebb3cb976d5c783fb112e96fe --- /dev/null +++ b/tests/unit/java/com/android/server/NetIdManagerTest.kt @@ -0,0 +1,53 @@ +/* + * 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.server + +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.NetIdManager.MIN_NET_ID +import com.android.testutils.assertThrows +import com.android.testutils.ExceptionUtils.ThrowingRunnable +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetIdManagerTest { + @Test + fun testReserveReleaseNetId() { + val manager = NetIdManager(MIN_NET_ID + 4) + assertEquals(MIN_NET_ID, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 1, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 2, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 3, manager.reserveNetId()) + + manager.releaseNetId(MIN_NET_ID + 1) + manager.releaseNetId(MIN_NET_ID + 3) + // IDs only loop once there is no higher ID available + assertEquals(MIN_NET_ID + 4, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 1, manager.reserveNetId()) + assertEquals(MIN_NET_ID + 3, manager.reserveNetId()) + assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() }) + manager.releaseNetId(MIN_NET_ID + 5) + // Still no ID available: MIN_NET_ID + 5 was not reserved + assertThrows(IllegalStateException::class.java, ThrowingRunnable { manager.reserveNetId() }) + manager.releaseNetId(MIN_NET_ID + 2) + // Throwing an exception still leaves the manager in a working state + assertEquals(MIN_NET_ID + 2, manager.reserveNetId()) + } +} \ No newline at end of file diff --git a/tests/unit/java/com/android/server/NetworkManagementServiceTest.java b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..13516d75a50d7ab6cfb60f2bf64fd17a85981495 --- /dev/null +++ b/tests/unit/java/com/android/server/NetworkManagementServiceTest.java @@ -0,0 +1,315 @@ +/* + * 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 com.android.server; + +import static android.util.DebugUtils.valueToString; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.INetd; +import android.net.INetdUnsolicitedEventListener; +import android.net.LinkAddress; +import android.net.NetworkPolicyManager; +import android.os.BatteryStats; +import android.os.Binder; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.ArrayMap; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.app.IBatteryStats; +import com.android.server.NetworkManagementService.Dependencies; +import com.android.server.net.BaseNetworkObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.BiFunction; + +/** + * Tests for {@link NetworkManagementService}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkManagementServiceTest { + private NetworkManagementService mNMService; + @Mock private Context mContext; + @Mock private IBatteryStats.Stub mBatteryStatsService; + @Mock private INetd.Stub mNetdService; + + private static final int TEST_UID = 111; + + @NonNull + @Captor + private ArgumentCaptor<INetdUnsolicitedEventListener> mUnsolListenerCaptor; + + private final MockDependencies mDeps = new MockDependencies(); + + private final class MockDependencies extends Dependencies { + @Override + public IBinder getService(String name) { + switch (name) { + case BatteryStats.SERVICE_NAME: + return mBatteryStatsService; + default: + throw new UnsupportedOperationException("Unknown service " + name); + } + } + + @Override + public void registerLocalService(NetworkManagementInternal nmi) { + } + + @Override + public INetd getNetd() { + return mNetdService; + } + + @Override + public int getCallingUid() { + return Process.SYSTEM_UID; + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + doNothing().when(mNetdService) + .registerUnsolicitedEventListener(mUnsolListenerCaptor.capture()); + // Start the service and wait until it connects to our socket. + mNMService = NetworkManagementService.create(mContext, mDeps); + } + + @After + public void tearDown() throws Exception { + mNMService.shutdown(); + } + + private static <T> T expectSoon(T mock) { + return verify(mock, timeout(200)); + } + + /** + * Tests that network observers work properly. + */ + @Test + public void testNetworkObservers() throws Exception { + BaseNetworkObserver observer = mock(BaseNetworkObserver.class); + doReturn(new Binder()).when(observer).asBinder(); // Used by registerObserver. + mNMService.registerObserver(observer); + + // Forget everything that happened to the mock so far, so we can explicitly verify + // everything that happens and does not happen to it from now on. + + INetdUnsolicitedEventListener unsolListener = mUnsolListenerCaptor.getValue(); + reset(observer); + // Now call unsolListener methods and ensure that the observer methods are + // called. After every method we expect a callback soon after; to ensure that + // invalid messages don't cause any callbacks, we call verifyNoMoreInteractions at the end. + + /** + * Interface changes. + */ + unsolListener.onInterfaceAdded("rmnet12"); + expectSoon(observer).interfaceAdded("rmnet12"); + + unsolListener.onInterfaceRemoved("eth1"); + expectSoon(observer).interfaceRemoved("eth1"); + + unsolListener.onInterfaceChanged("clat4", true); + expectSoon(observer).interfaceStatusChanged("clat4", true); + + unsolListener.onInterfaceLinkStateChanged("rmnet0", false); + expectSoon(observer).interfaceLinkStateChanged("rmnet0", false); + + /** + * Bandwidth control events. + */ + unsolListener.onQuotaLimitReached("data", "rmnet_usb0"); + expectSoon(observer).limitReached("data", "rmnet_usb0"); + + /** + * Interface class activity. + */ + unsolListener.onInterfaceClassActivityChanged(true, 1, 1234, TEST_UID); + expectSoon(observer).interfaceClassDataActivityChanged(1, true, 1234, TEST_UID); + + unsolListener.onInterfaceClassActivityChanged(false, 9, 5678, TEST_UID); + expectSoon(observer).interfaceClassDataActivityChanged(9, false, 5678, TEST_UID); + + unsolListener.onInterfaceClassActivityChanged(false, 9, 4321, TEST_UID); + expectSoon(observer).interfaceClassDataActivityChanged(9, false, 4321, TEST_UID); + + /** + * IP address changes. + */ + unsolListener.onInterfaceAddressUpdated("fe80::1/64", "wlan0", 128, 253); + expectSoon(observer).addressUpdated("wlan0", new LinkAddress("fe80::1/64", 128, 253)); + + unsolListener.onInterfaceAddressRemoved("fe80::1/64", "wlan0", 128, 253); + expectSoon(observer).addressRemoved("wlan0", new LinkAddress("fe80::1/64", 128, 253)); + + unsolListener.onInterfaceAddressRemoved("2001:db8::1/64", "wlan0", 1, 0); + expectSoon(observer).addressRemoved("wlan0", new LinkAddress("2001:db8::1/64", 1, 0)); + + /** + * DNS information broadcasts. + */ + unsolListener.onInterfaceDnsServerInfo("rmnet_usb0", 3600, new String[]{"2001:db8::1"}); + expectSoon(observer).interfaceDnsServerInfo("rmnet_usb0", 3600, + new String[]{"2001:db8::1"}); + + unsolListener.onInterfaceDnsServerInfo("wlan0", 14400, + new String[]{"2001:db8::1", "2001:db8::2"}); + expectSoon(observer).interfaceDnsServerInfo("wlan0", 14400, + new String[]{"2001:db8::1", "2001:db8::2"}); + + // We don't check for negative lifetimes, only for parse errors. + unsolListener.onInterfaceDnsServerInfo("wlan0", -3600, new String[]{"::1"}); + expectSoon(observer).interfaceDnsServerInfo("wlan0", -3600, + new String[]{"::1"}); + + // No syntax checking on the addresses. + unsolListener.onInterfaceDnsServerInfo("wlan0", 600, + new String[]{"", "::", "", "foo", "::1"}); + expectSoon(observer).interfaceDnsServerInfo("wlan0", 600, + new String[]{"", "::", "", "foo", "::1"}); + + // Make sure nothing else was called. + verifyNoMoreInteractions(observer); + } + + @Test + public void testFirewallEnabled() { + mNMService.setFirewallEnabled(true); + assertTrue(mNMService.isFirewallEnabled()); + + mNMService.setFirewallEnabled(false); + assertFalse(mNMService.isFirewallEnabled()); + } + + @Test + public void testNetworkRestrictedDefault() { + assertFalse(mNMService.isNetworkRestricted(TEST_UID)); + } + + @Test + public void testMeteredNetworkRestrictions() throws RemoteException { + // Make sure the mocked netd method returns true. + doReturn(true).when(mNetdService).bandwidthEnableDataSaver(anyBoolean()); + + // Restrict usage of mobile data in background + mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, true); + assertTrue("Should be true since mobile data usage is restricted", + mNMService.isNetworkRestricted(TEST_UID)); + + mNMService.setDataSaverModeEnabled(true); + verify(mNetdService).bandwidthEnableDataSaver(true); + + mNMService.setUidOnMeteredNetworkDenylist(TEST_UID, false); + assertTrue("Should be true since data saver is on and the uid is not allowlisted", + mNMService.isNetworkRestricted(TEST_UID)); + + mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, true); + assertFalse("Should be false since data saver is on and the uid is allowlisted", + mNMService.isNetworkRestricted(TEST_UID)); + + // remove uid from allowlist and turn datasaver off again + mNMService.setUidOnMeteredNetworkAllowlist(TEST_UID, false); + mNMService.setDataSaverModeEnabled(false); + verify(mNetdService).bandwidthEnableDataSaver(false); + assertFalse("Network should not be restricted when data saver is off", + mNMService.isNetworkRestricted(TEST_UID)); + } + + @Test + public void testFirewallChains() { + final ArrayMap<Integer, ArrayMap<Integer, Boolean>> expected = new ArrayMap<>(); + // Dozable chain + final ArrayMap<Integer, Boolean> isRestrictedForDozable = new ArrayMap<>(); + isRestrictedForDozable.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedForDozable.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForDozable.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_DOZABLE, isRestrictedForDozable); + // Powersaver chain + final ArrayMap<Integer, Boolean> isRestrictedForPowerSave = new ArrayMap<>(); + isRestrictedForPowerSave.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForPowerSave.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_POWERSAVE, isRestrictedForPowerSave); + // Standby chain + final ArrayMap<Integer, Boolean> isRestrictedForStandby = new ArrayMap<>(); + isRestrictedForStandby.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, false); + isRestrictedForStandby.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForStandby.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_STANDBY, isRestrictedForStandby); + // Restricted mode chain + final ArrayMap<Integer, Boolean> isRestrictedForRestrictedMode = new ArrayMap<>(); + isRestrictedForRestrictedMode.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedForRestrictedMode.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(INetd.FIREWALL_CHAIN_RESTRICTED, isRestrictedForRestrictedMode); + + final int[] chains = { + INetd.FIREWALL_CHAIN_STANDBY, + INetd.FIREWALL_CHAIN_POWERSAVE, + INetd.FIREWALL_CHAIN_DOZABLE, + INetd.FIREWALL_CHAIN_RESTRICTED + }; + final int[] states = { + INetd.FIREWALL_RULE_ALLOW, + INetd.FIREWALL_RULE_DENY, + NetworkPolicyManager.FIREWALL_RULE_DEFAULT + }; + BiFunction<Integer, Integer, String> errorMsg = (chain, state) -> { + return String.format("Unexpected value for chain: %s and state: %s", + valueToString(INetd.class, "FIREWALL_CHAIN_", chain), + valueToString(INetd.class, "FIREWALL_RULE_", state)); + }; + for (int chain : chains) { + final ArrayMap<Integer, Boolean> expectedValues = expected.get(chain); + mNMService.setFirewallChainEnabled(chain, true); + for (int state : states) { + mNMService.setFirewallUidRule(chain, TEST_UID, state); + assertEquals(errorMsg.apply(chain, state), + expectedValues.get(state), mNMService.isNetworkRestricted(TEST_UID)); + } + mNMService.setFirewallChainEnabled(chain, false); + } + } +} diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a90fa6882c257bea12f6129c8932dff162083f2b --- /dev/null +++ b/tests/unit/java/com/android/server/NsdServiceTest.java @@ -0,0 +1,194 @@ +/* + * 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.server; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.NsdService.DaemonConnection; +import com.android.server.NsdService.DaemonConnectionSupplier; +import com.android.server.NsdService.NativeCallbackReceiver; + +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; + +// TODOs: +// - test client can send requests and receive replies +// - test NSD_ON ENABLE/DISABLED listening +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NsdServiceTest { + + static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD; + + long mTimeoutMs = 100; // non-final so that tests can adjust the value. + + @Mock Context mContext; + @Mock ContentResolver mResolver; + @Mock NsdService.NsdSettings mSettings; + @Mock DaemonConnection mDaemon; + NativeCallbackReceiver mDaemonCallback; + HandlerThread mThread; + TestHandler mHandler; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mThread = new HandlerThread("mock-service-handler"); + mThread.start(); + mHandler = new TestHandler(mThread.getLooper()); + when(mContext.getContentResolver()).thenReturn(mResolver); + } + + @After + public void tearDown() throws Exception { + if (mThread != null) { + mThread.quit(); + mThread = null; + } + } + + @Test + public void testClientsCanConnectAndDisconnect() { + when(mSettings.isEnabled()).thenReturn(true); + + NsdService service = makeService(); + + NsdManager client1 = connectClient(service); + verify(mDaemon, timeout(100).times(1)).start(); + + NsdManager client2 = connectClient(service); + + client1.disconnect(); + client2.disconnect(); + + verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); + + client1.disconnect(); + client2.disconnect(); + } + + @Test + public void testClientRequestsAreGCedAtDisconnection() { + when(mSettings.isEnabled()).thenReturn(true); + when(mDaemon.execute(any())).thenReturn(true); + + NsdService service = makeService(); + NsdManager client = connectClient(service); + + verify(mDaemon, timeout(100).times(1)).start(); + + NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); + request.setPort(2201); + + // Client registration request + NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); + client.registerService(request, PROTOCOL, listener1); + verifyDaemonCommand("register 2 a_name a_type 2201"); + + // Client discovery request + NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); + client.discoverServices("a_type", PROTOCOL, listener2); + verifyDaemonCommand("discover 3 a_type"); + + // Client resolve request + NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); + client.resolveService(request, listener3); + verifyDaemonCommand("resolve 4 a_name a_type local."); + + // Client disconnects + client.disconnect(); + verify(mDaemon, timeout(mTimeoutMs).times(1)).stop(); + + // checks that request are cleaned + verifyDaemonCommands("stop-register 2", "stop-discover 3", "stop-resolve 4"); + + client.disconnect(); + } + + NsdService makeService() { + DaemonConnectionSupplier supplier = (callback) -> { + mDaemonCallback = callback; + return mDaemon; + }; + NsdService service = new NsdService(mContext, mSettings, mHandler, supplier); + verify(mDaemon, never()).execute(any(String.class)); + return service; + } + + NsdManager connectClient(NsdService service) { + return new NsdManager(mContext, service); + } + + void verifyDaemonCommands(String... wants) { + verifyDaemonCommand(String.join(" ", wants), wants.length); + } + + void verifyDaemonCommand(String want) { + verifyDaemonCommand(want, 1); + } + + void verifyDaemonCommand(String want, int n) { + ArgumentCaptor<Object> argumentsCaptor = ArgumentCaptor.forClass(Object.class); + verify(mDaemon, timeout(mTimeoutMs).times(n)).execute(argumentsCaptor.capture()); + String got = ""; + for (Object o : argumentsCaptor.getAllValues()) { + got += o + " "; + } + assertEquals(want, got.trim()); + // rearm deamon for next command verification + reset(mDaemon); + when(mDaemon.execute(any())).thenReturn(true); + } + + public static class TestHandler extends Handler { + public Message lastMessage; + + TestHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + lastMessage = obtainMessage(); + lastMessage.copyFrom(msg); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0ffeec98cf90e8f777c167ca7c2d2872d4986173 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java @@ -0,0 +1,433 @@ +/* + * 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.server.connectivity; + +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER; +import static android.net.NetworkCapabilities.MAX_TRANSPORT; +import static android.net.NetworkCapabilities.MIN_TRANSPORT; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE; +import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS; + +import static com.android.testutils.MiscAsserts.assertContainsExactly; +import static com.android.testutils.MiscAsserts.assertContainsStringsExactly; +import static com.android.testutils.MiscAsserts.assertFieldCountEquals; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.ConnectivitySettingsManager; +import android.net.IDnsResolver; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.ResolverOptionsParcel; +import android.net.ResolverParamsParcel; +import android.net.RouteInfo; +import android.net.shared.PrivateDnsConfig; +import android.provider.Settings; +import android.test.mock.MockContentResolver; +import android.util.SparseArray; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.MessageUtils; +import com.android.internal.util.test.FakeSettingsProvider; + +import libcore.net.InetAddressUtils; + +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.Arrays; + +/** + * Tests for {@link DnsManager}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.DnsManagerTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DnsManagerTest { + static final String TEST_IFACENAME = "test_wlan0"; + static final int TEST_NETID = 100; + static final int TEST_NETID_ALTERNATE = 101; + static final int TEST_NETID_UNTRACKED = 102; + static final int TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800; + static final int TEST_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25; + static final int TEST_DEFAULT_MIN_SAMPLES = 8; + static final int TEST_DEFAULT_MAX_SAMPLES = 64; + static final int[] TEST_TRANSPORT_TYPES = {TRANSPORT_WIFI, TRANSPORT_VPN}; + + DnsManager mDnsManager; + MockContentResolver mContentResolver; + + @Mock Context mCtx; + @Mock IDnsResolver mMockDnsResolver; + + private void assertResolverOptionsEquals( + @NonNull ResolverOptionsParcel actual, + @NonNull ResolverOptionsParcel expected) { + assertEquals(actual.hosts, expected.hosts); + assertEquals(actual.tcMode, expected.tcMode); + assertEquals(actual.enforceDnsUid, expected.enforceDnsUid); + assertFieldCountEquals(3, ResolverOptionsParcel.class); + } + + private void assertResolverParamsEquals(@NonNull ResolverParamsParcel actual, + @NonNull ResolverParamsParcel expected) { + assertEquals(actual.netId, expected.netId); + assertEquals(actual.sampleValiditySeconds, expected.sampleValiditySeconds); + assertEquals(actual.successThreshold, expected.successThreshold); + assertEquals(actual.minSamples, expected.minSamples); + assertEquals(actual.maxSamples, expected.maxSamples); + assertEquals(actual.baseTimeoutMsec, expected.baseTimeoutMsec); + assertEquals(actual.retryCount, expected.retryCount); + assertContainsStringsExactly(actual.servers, expected.servers); + assertContainsStringsExactly(actual.domains, expected.domains); + assertEquals(actual.tlsName, expected.tlsName); + assertContainsStringsExactly(actual.tlsServers, expected.tlsServers); + assertContainsStringsExactly(actual.tlsFingerprints, expected.tlsFingerprints); + assertEquals(actual.caCertificate, expected.caCertificate); + assertEquals(actual.tlsConnectTimeoutMs, expected.tlsConnectTimeoutMs); + assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions); + assertContainsExactly(actual.transportTypes, expected.transportTypes); + assertFieldCountEquals(16, ResolverParamsParcel.class); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, + new FakeSettingsProvider()); + when(mCtx.getContentResolver()).thenReturn(mContentResolver); + mDnsManager = new DnsManager(mCtx, mMockDnsResolver); + + // Clear the private DNS settings + Settings.Global.putString(mContentResolver, PRIVATE_DNS_DEFAULT_MODE, ""); + Settings.Global.putString(mContentResolver, PRIVATE_DNS_MODE, ""); + Settings.Global.putString(mContentResolver, PRIVATE_DNS_SPECIFIER, ""); + } + + @Test + public void testTrackedValidationUpdates() throws Exception { + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + mDnsManager.updatePrivateDns(new Network(TEST_NETID_ALTERNATE), + mDnsManager.getPrivateDnsConfig()); + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(TEST_IFACENAME); + lp.addDnsServer(InetAddress.getByName("3.3.3.3")); + lp.addDnsServer(InetAddress.getByName("4.4.4.4")); + + // Send a validation event that is tracked on the alternate netId + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID_ALTERNATE, + InetAddress.parseNumericAddress("4.4.4.4"), "", + VALIDATION_RESULT_SUCCESS)); + LinkProperties fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertFalse(fixedLp.isPrivateDnsActive()); + assertNull(fixedLp.getPrivateDnsServerName()); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID_ALTERNATE, fixedLp); + assertTrue(fixedLp.isPrivateDnsActive()); + assertNull(fixedLp.getPrivateDnsServerName()); + assertEquals(Arrays.asList(InetAddress.getByName("4.4.4.4")), + fixedLp.getValidatedPrivateDnsServers()); + + // Set up addresses for strict mode and switch to it. + lp.addLinkAddress(new LinkAddress("192.0.2.4/24")); + lp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), + TEST_IFACENAME)); + lp.addLinkAddress(new LinkAddress("2001:db8:1::1/64")); + lp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), + TEST_IFACENAME)); + + ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); + ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com"); + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + new PrivateDnsConfig("strictmode.com", new InetAddress[] { + InetAddress.parseNumericAddress("6.6.6.6"), + InetAddress.parseNumericAddress("2001:db8:66:66::1") + })); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertTrue(fixedLp.isPrivateDnsActive()); + assertEquals("strictmode.com", fixedLp.getPrivateDnsServerName()); + // No validation events yet. + assertEquals(Arrays.asList(new InetAddress[0]), fixedLp.getValidatedPrivateDnsServers()); + // Validate one. + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("6.6.6.6"), "strictmode.com", + VALIDATION_RESULT_SUCCESS)); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertEquals(Arrays.asList(InetAddress.parseNumericAddress("6.6.6.6")), + fixedLp.getValidatedPrivateDnsServers()); + // Validate the 2nd one. + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("2001:db8:66:66::1"), "strictmode.com", + VALIDATION_RESULT_SUCCESS)); + fixedLp = new LinkProperties(lp); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, fixedLp); + assertEquals(Arrays.asList( + InetAddress.parseNumericAddress("2001:db8:66:66::1"), + InetAddress.parseNumericAddress("6.6.6.6")), + fixedLp.getValidatedPrivateDnsServers()); + } + + @Test + public void testIgnoreUntrackedValidationUpdates() throws Exception { + // The PrivateDnsConfig map is empty, so no validation events will + // be tracked. + LinkProperties lp = new LinkProperties(); + lp.addDnsServer(InetAddress.getByName("3.3.3.3")); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event has untracked netId + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID_UNTRACKED, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event has untracked ipAddress + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("4.4.4.4"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event has untracked hostname + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "hostname", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Validation event failed + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_FAILURE)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Network removed + mDnsManager.removeNetwork(new Network(TEST_NETID)); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + + // Turn private DNS mode off + ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF); + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, + InetAddress.parseNumericAddress("3.3.3.3"), "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + assertFalse(lp.isPrivateDnsActive()); + assertNull(lp.getPrivateDnsServerName()); + } + + @Test + public void testOverrideDefaultMode() throws Exception { + // Hard-coded default is opportunistic mode. + final PrivateDnsConfig cfgAuto = DnsManager.getPrivateDnsConfig(mCtx); + assertTrue(cfgAuto.useTls); + assertEquals("", cfgAuto.hostname); + assertEquals(new InetAddress[0], cfgAuto.ips); + + // Pretend a gservices push sets the default to "off". + ConnectivitySettingsManager.setPrivateDnsDefaultMode(mCtx, PRIVATE_DNS_MODE_OFF); + final PrivateDnsConfig cfgOff = DnsManager.getPrivateDnsConfig(mCtx); + assertFalse(cfgOff.useTls); + assertEquals("", cfgOff.hostname); + assertEquals(new InetAddress[0], cfgOff.ips); + + // Strict mode still works. + ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); + ConnectivitySettingsManager.setPrivateDnsHostname(mCtx, "strictmode.com"); + final PrivateDnsConfig cfgStrict = DnsManager.getPrivateDnsConfig(mCtx); + assertTrue(cfgStrict.useTls); + assertEquals("strictmode.com", cfgStrict.hostname); + assertEquals(new InetAddress[0], cfgStrict.ips); + } + + @Test + public void testSendDnsConfiguration() throws Exception { + reset(mMockDnsResolver); + mDnsManager.updatePrivateDns(new Network(TEST_NETID), + mDnsManager.getPrivateDnsConfig()); + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(TEST_IFACENAME); + lp.addDnsServer(InetAddress.getByName("3.3.3.3")); + lp.addDnsServer(InetAddress.getByName("4.4.4.4")); + mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.flushVmDnsCache(); + + final ArgumentCaptor<ResolverParamsParcel> resolverParamsParcelCaptor = + ArgumentCaptor.forClass(ResolverParamsParcel.class); + verify(mMockDnsResolver, times(1)).setResolverConfiguration( + resolverParamsParcelCaptor.capture()); + final ResolverParamsParcel actualParams = resolverParamsParcelCaptor.getValue(); + final ResolverParamsParcel expectedParams = new ResolverParamsParcel(); + expectedParams.netId = TEST_NETID; + expectedParams.sampleValiditySeconds = TEST_DEFAULT_SAMPLE_VALIDITY_SECONDS; + expectedParams.successThreshold = TEST_DEFAULT_SUCCESS_THRESHOLD_PERCENT; + expectedParams.minSamples = TEST_DEFAULT_MIN_SAMPLES; + expectedParams.maxSamples = TEST_DEFAULT_MAX_SAMPLES; + expectedParams.servers = new String[]{"3.3.3.3", "4.4.4.4"}; + expectedParams.domains = new String[]{}; + expectedParams.tlsName = ""; + expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"}; + expectedParams.transportTypes = TEST_TRANSPORT_TYPES; + expectedParams.resolverOptions = new ResolverOptionsParcel(); + assertResolverParamsEquals(actualParams, expectedParams); + } + + @Test + public void testTransportTypesEqual() throws Exception { + SparseArray<String> ncTransTypes = MessageUtils.findMessageNames( + new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" }); + SparseArray<String> dnsTransTypes = MessageUtils.findMessageNames( + new Class[] { IDnsResolver.class }, new String[]{ "TRANSPORT_" }); + assertEquals(0, MIN_TRANSPORT); + assertEquals(MAX_TRANSPORT + 1, ncTransTypes.size()); + // TRANSPORT_UNKNOWN in IDnsResolver is defined to -1 and only for resolver. + assertEquals("TRANSPORT_UNKNOWN", dnsTransTypes.get(-1)); + assertEquals(ncTransTypes.size(), dnsTransTypes.size() - 1); + for (int i = MIN_TRANSPORT; i < MAX_TRANSPORT; i++) { + String name = ncTransTypes.get(i, null); + assertNotNull("Could not find NetworkCapabilies.TRANSPORT_* constant equal to " + + i, name); + assertEquals(name, dnsTransTypes.get(i)); + } + } + + @Test + public void testGetPrivateDnsConfigForNetwork() throws Exception { + final Network network = new Network(TEST_NETID); + final InetAddress dnsAddr = InetAddressUtils.parseNumericAddress("3.3.3.3"); + final InetAddress[] tlsAddrs = new InetAddress[]{ + InetAddressUtils.parseNumericAddress("6.6.6.6"), + InetAddressUtils.parseNumericAddress("2001:db8:66:66::1") + }; + final String tlsName = "strictmode.com"; + LinkProperties lp = new LinkProperties(); + lp.addDnsServer(dnsAddr); + + // The PrivateDnsConfig map is empty, so the default PRIVATE_DNS_OFF is returned. + PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertFalse(privateDnsCfg.useTls); + assertEquals("", privateDnsCfg.hostname); + assertEquals(new InetAddress[0], privateDnsCfg.ips); + + // An entry with default PrivateDnsConfig is added to the PrivateDnsConfig map. + mDnsManager.updatePrivateDns(network, mDnsManager.getPrivateDnsConfig()); + mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp); + mDnsManager.updatePrivateDnsValidation( + new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, dnsAddr, "", + VALIDATION_RESULT_SUCCESS)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertTrue(privateDnsCfg.useTls); + assertEquals("", privateDnsCfg.hostname); + assertEquals(new InetAddress[0], privateDnsCfg.ips); + + // The original entry is overwritten by a new PrivateDnsConfig. + mDnsManager.updatePrivateDns(network, new PrivateDnsConfig(tlsName, tlsAddrs)); + mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp); + privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertTrue(privateDnsCfg.useTls); + assertEquals(tlsName, privateDnsCfg.hostname); + assertEquals(tlsAddrs, privateDnsCfg.ips); + + // The network is removed, so the PrivateDnsConfig map becomes empty again. + mDnsManager.removeNetwork(network); + privateDnsCfg = mDnsManager.getPrivateDnsConfig(network); + assertFalse(privateDnsCfg.useTls); + assertEquals("", privateDnsCfg.hostname); + assertEquals(new InetAddress[0], privateDnsCfg.ips); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..45b575a4365d49118d0c5648d981a5e8ba186e18 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity + +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkScore.KEEP_CONNECTED_NONE +import android.text.TextUtils +import android.util.ArraySet +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.server.connectivity.FullScore.MAX_CS_MANAGED_POLICY +import com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED +import com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED +import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED +import com.android.server.connectivity.FullScore.POLICY_IS_VPN +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.collections.minOfOrNull +import kotlin.collections.maxOfOrNull +import kotlin.reflect.full.staticProperties +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@SmallTest +class FullScoreTest { + // Convenience methods + fun FullScore.withPolicies( + validated: Boolean = false, + vpn: Boolean = false, + onceChosen: Boolean = false, + acceptUnvalidated: Boolean = false + ): FullScore { + val nac = NetworkAgentConfig.Builder().apply { + setUnvalidatedConnectivityAcceptable(acceptUnvalidated) + setExplicitlySelected(onceChosen) + }.build() + val nc = NetworkCapabilities.Builder().apply { + if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN) + if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + }.build() + return mixInScore(nc, nac, validated, false /* yieldToBadWifi */) + } + + @Test + fun testGetLegacyInt() { + val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE) + assertEquals(10, ns.legacyInt) // -40 penalty for not being validated + assertEquals(50, ns.legacyIntAsValidated) + + val vpnNs = FullScore(101, 0L /* policy */, KEEP_CONNECTED_NONE).withPolicies(vpn = true) + assertEquals(101, vpnNs.legacyInt) // VPNs are not subject to unvalidation penalty + assertEquals(101, vpnNs.legacyIntAsValidated) + assertEquals(101, vpnNs.withPolicies(validated = true).legacyInt) + assertEquals(101, vpnNs.withPolicies(validated = true).legacyIntAsValidated) + + val validatedNs = ns.withPolicies(validated = true) + assertEquals(50, validatedNs.legacyInt) // No penalty, this is validated + assertEquals(50, validatedNs.legacyIntAsValidated) + + val chosenNs = ns.withPolicies(onceChosen = true) + assertEquals(10, chosenNs.legacyInt) + assertEquals(100, chosenNs.legacyIntAsValidated) + assertEquals(10, chosenNs.withPolicies(acceptUnvalidated = true).legacyInt) + assertEquals(50, chosenNs.withPolicies(acceptUnvalidated = true).legacyIntAsValidated) + } + + @Test + fun testToString() { + val string = FullScore(10, 0L /* policy */, KEEP_CONNECTED_NONE) + .withPolicies(vpn = true, acceptUnvalidated = true).toString() + assertTrue(string.contains("Score(10"), string) + assertTrue(string.contains("ACCEPT_UNVALIDATED"), string) + assertTrue(string.contains("IS_VPN"), string) + assertFalse(string.contains("IS_VALIDATED"), string) + val foundNames = ArraySet<String>() + getAllPolicies().forEach { + val name = FullScore.policyNameOf(it.get() as Int) + assertFalse(TextUtils.isEmpty(name)) + assertFalse(foundNames.contains(name)) + foundNames.add(name) + } + assertFailsWith<IllegalArgumentException> { + FullScore.policyNameOf(MAX_CS_MANAGED_POLICY + 1) + } + } + + fun getAllPolicies() = Regex("POLICY_.*").let { nameRegex -> + FullScore::class.staticProperties.filter { it.name.matches(nameRegex) } + } + + @Test + fun testHasPolicy() { + val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE) + assertFalse(ns.hasPolicy(POLICY_IS_VALIDATED)) + assertFalse(ns.hasPolicy(POLICY_IS_VPN)) + assertFalse(ns.hasPolicy(POLICY_EVER_USER_SELECTED)) + assertFalse(ns.hasPolicy(POLICY_ACCEPT_UNVALIDATED)) + assertTrue(ns.withPolicies(validated = true).hasPolicy(POLICY_IS_VALIDATED)) + assertTrue(ns.withPolicies(vpn = true).hasPolicy(POLICY_IS_VPN)) + assertTrue(ns.withPolicies(onceChosen = true).hasPolicy(POLICY_EVER_USER_SELECTED)) + assertTrue(ns.withPolicies(acceptUnvalidated = true).hasPolicy(POLICY_ACCEPT_UNVALIDATED)) + } + + @Test + fun testMinMaxPolicyConstants() { + val policies = getAllPolicies() + + policies.forEach { policy -> + assertTrue(policy.get() as Int >= FullScore.MIN_CS_MANAGED_POLICY) + assertTrue(policy.get() as Int <= FullScore.MAX_CS_MANAGED_POLICY) + } + assertEquals(FullScore.MIN_CS_MANAGED_POLICY, + policies.minOfOrNull { it.get() as Int }) + assertEquals(FullScore.MAX_CS_MANAGED_POLICY, + policies.maxOfOrNull { it.get() as Int }) + } +} diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..70495cced5366b688bec136e1f0d838d2aefe55a --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java @@ -0,0 +1,561 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static com.android.server.connectivity.MetricsTestUtil.aLong; +import static com.android.server.connectivity.MetricsTestUtil.aString; +import static com.android.server.connectivity.MetricsTestUtil.aType; +import static com.android.server.connectivity.MetricsTestUtil.anInt; +import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.BLUETOOTH; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.CELLULAR; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.MULTIPLE; +import static com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.WIFI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.net.ConnectivityMetricsEvent; +import android.net.metrics.ApfProgramEvent; +import android.net.metrics.ApfStats; +import android.net.metrics.DefaultNetworkEvent; +import android.net.metrics.DhcpClientEvent; +import android.net.metrics.DhcpErrorEvent; +import android.net.metrics.IpManagerEvent; +import android.net.metrics.IpReachabilityEvent; +import android.net.metrics.NetworkEvent; +import android.net.metrics.RaEvent; +import android.net.metrics.ValidationProbeEvent; +import android.net.metrics.WakeupStats; +import android.test.suitebuilder.annotation.SmallTest; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +// TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto. +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpConnectivityEventBuilderTest { + + @Test + public void testLinkLayerInferrence() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(IpReachabilityEvent.class), + anInt(IpReachabilityEvent.NUD_FAILED)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.netId = 123; + ev.transports = 3; // transports have priority for inferrence of link layer + ev.ifname = "wlan0"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", MULTIPLE), + " network_id: 123", + " time_ms: 1", + " transports: 3", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.transports = 1; + ev.ifname = null; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", CELLULAR), + " network_id: 123", + " time_ms: 1", + " transports: 1", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.transports = 0; + ev.ifname = "not_inferred"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"not_inferred\"", + " link_layer: 0", + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.ifname = "bt-pan"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", BLUETOOTH), + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.ifname = "rmnet_ipa0"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", CELLULAR), + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + + ev.ifname = "wlan0"; + want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + String.format(" link_layer: %d", WIFI), + " network_id: 123", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + verifySerialization(want, ev); + } + + @Test + public void testDefaultNetworkEventSerialization() { + DefaultNetworkEvent ev = new DefaultNetworkEvent(1001); + ev.netId = 102; + ev.transports = 2; + ev.previousTransports = 4; + ev.ipv4 = true; + ev.initialScore = 20; + ev.finalScore = 60; + ev.durationMs = 54; + ev.validatedMs = 27; + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 102", + " time_ms: 0", + " transports: 2", + " default_network_event <", + " default_network_duration_ms: 54", + " final_score: 60", + " initial_score: 20", + " ip_support: 1", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 1", + " previous_network_ip_support: 0", + " validation_duration_ms: 27", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, IpConnectivityEventBuilder.toProto(ev)); + } + + @Test + public void testDhcpClientEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(DhcpClientEvent.class), + aString("SomeState"), + anInt(192)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " dhcp_event <", + " duration_ms: 192", + " if_name: \"\"", + " state_transition: \"SomeState\"", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testDhcpErrorEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(DhcpErrorEvent.class), + anInt(DhcpErrorEvent.L4_NOT_UDP)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " dhcp_event <", + " duration_ms: 0", + " if_name: \"\"", + " error_code: 50397184", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testIpManagerEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(IpManagerEvent.class), + anInt(IpManagerEvent.PROVISIONING_OK), + aLong(5678)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ip_provisioning_event <", + " event_type: 1", + " if_name: \"\"", + " latency_ms: 5678", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testIpReachabilityEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(IpReachabilityEvent.class), + anInt(IpReachabilityEvent.NUD_FAILED)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testNetworkEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(NetworkEvent.class), + anInt(5), + aLong(20410)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " network_event <", + " event_type: 5", + " latency_ms: 20410", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testValidationProbeEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(ValidationProbeEvent.class), + aLong(40730), + anInt(ValidationProbeEvent.PROBE_HTTP), + anInt(204)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " validation_probe_event <", + " latency_ms: 40730", + " probe_result: 204", + " probe_type: 1", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testApfProgramEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(ApfProgramEvent.class), + aLong(200), + aLong(18), + anInt(7), + anInt(9), + anInt(2048), + anInt(3)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " apf_program_event <", + " current_ras: 9", + " drop_multicast: true", + " effective_lifetime: 18", + " filtered_ras: 7", + " has_ipv4_addr: true", + " lifetime: 200", + " program_length: 2048", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testApfStatsSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(ApfStats.class), + aLong(45000), + anInt(10), + anInt(2), + anInt(2), + anInt(1), + anInt(2), + anInt(4), + anInt(7), + anInt(3), + anInt(2048)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " apf_statistics <", + " dropped_ras: 2", + " duration_ms: 45000", + " matching_ras: 2", + " max_program_size: 2048", + " parse_errors: 2", + " program_updates: 4", + " program_updates_all: 7", + " program_updates_allowing_multicast: 3", + " received_ras: 10", + " total_packet_dropped: 0", + " total_packet_processed: 0", + " zero_lifetime_ras: 1", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testRaEventSerialization() { + ConnectivityMetricsEvent ev = describeIpEvent( + aType(RaEvent.class), + aLong(2000), + aLong(400), + aLong(300), + aLong(-1), + aLong(1000), + aLong(-1)); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 0", + " network_id: 0", + " time_ms: 1", + " transports: 0", + " ra_event <", + " dnssl_lifetime: -1", + " prefix_preferred_lifetime: 300", + " prefix_valid_lifetime: 400", + " rdnss_lifetime: 1000", + " route_info_lifetime: -1", + " router_lifetime: 2000", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, ev); + } + + @Test + public void testWakeupStatsSerialization() { + WakeupStats stats = new WakeupStats("wlan0"); + stats.totalWakeups = 14; + stats.applicationWakeups = 5; + stats.nonApplicationWakeups = 1; + stats.rootWakeups = 2; + stats.systemWakeups = 3; + stats.noUidWakeups = 3; + stats.l2UnicastCount = 5; + stats.l2MulticastCount = 1; + stats.l2BroadcastCount = 2; + stats.ethertypes.put(0x800, 3); + stats.ethertypes.put(0x86dd, 3); + stats.ipNextHeaders.put(6, 5); + + + IpConnectivityEvent got = IpConnectivityEventBuilder.toProto(stats); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 5", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 3", + " >", + " ethertype_counts <", + " key: 34525", + " value: 3", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 5", + " >", + " l2_broadcast_count: 2", + " l2_multicast_count: 1", + " l2_unicast_count: 5", + " no_uid_wakeups: 3", + " non_application_wakeups: 1", + " root_wakeups: 2", + " system_wakeups: 3", + " total_wakeups: 14", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, got); + } + + static void verifySerialization(String want, ConnectivityMetricsEvent... input) { + List<IpConnectivityEvent> protoInput = + IpConnectivityEventBuilder.toProto(Arrays.asList(input)); + verifySerialization(want, protoInput.toArray(new IpConnectivityEvent[0])); + } + + static void verifySerialization(String want, IpConnectivityEvent... input) { + try { + byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input)); + IpConnectivityLog log = IpConnectivityLog.parseFrom(got); + assertEquals(want, log.toString()); + } catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8b072c49de82684fc49f86ed9547e3386440238c --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/IpConnectivityMetricsTest.java @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO; +import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityMetricsEvent; +import android.net.IIpConnectivityMetrics; +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.metrics.ApfProgramEvent; +import android.net.metrics.ApfStats; +import android.net.metrics.DhcpClientEvent; +import android.net.metrics.IpConnectivityLog; +import android.net.metrics.IpManagerEvent; +import android.net.metrics.IpReachabilityEvent; +import android.net.metrics.RaEvent; +import android.net.metrics.ValidationProbeEvent; +import android.os.Parcelable; +import android.system.OsConstants; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.BitUtils; +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpConnectivityMetricsTest { + static final IpReachabilityEvent FAKE_EV = + new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED); + + private static final String EXAMPLE_IPV4 = "192.0.2.1"; + private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1"; + + private static final byte[] MAC_ADDR = + {(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b}; + + @Mock Context mCtx; + @Mock IIpConnectivityMetrics mMockService; + @Mock ConnectivityManager mCm; + + IpConnectivityMetrics mService; + NetdEventListenerService mNetdListener; + private static final NetworkCapabilities CAPABILITIES_WIFI = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + private static final NetworkCapabilities CAPABILITIES_CELL = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mService = new IpConnectivityMetrics(mCtx, (ctx) -> 2000); + mNetdListener = new NetdEventListenerService(mCm); + mService.mNetdListener = mNetdListener; + } + + @Test + public void testBufferFlushing() { + String output1 = getdump("flush"); + assertEquals("", output1); + + new IpConnectivityLog(mService.impl).log(1, FAKE_EV); + String output2 = getdump("flush"); + assertFalse("".equals(output2)); + + String output3 = getdump("flush"); + assertEquals("", output3); + } + + @Test + public void testRateLimiting() { + final IpConnectivityLog logger = new IpConnectivityLog(mService.impl); + final ApfProgramEvent ev = new ApfProgramEvent.Builder().build(); + final long fakeTimestamp = 1; + + int attempt = 100; // More than burst quota, but less than buffer size. + for (int i = 0; i < attempt; i++) { + logger.log(ev); + } + + String output1 = getdump("flush"); + assertFalse("".equals(output1)); + + for (int i = 0; i < attempt; i++) { + assertFalse("expected event to be dropped", logger.log(fakeTimestamp, ev)); + } + + String output2 = getdump("flush"); + assertEquals("", output2); + } + + private void logDefaultNetworkEvent(long timeMs, NetworkAgentInfo nai, + NetworkAgentInfo oldNai) { + final Network network = (nai != null) ? nai.network() : null; + final int score = (nai != null) ? nai.getCurrentScore() : 0; + final boolean validated = (nai != null) ? nai.lastValidated : false; + final LinkProperties lp = (nai != null) ? nai.linkProperties : null; + final NetworkCapabilities nc = (nai != null) ? nai.networkCapabilities : null; + + final Network prevNetwork = (oldNai != null) ? oldNai.network() : null; + final int prevScore = (oldNai != null) ? oldNai.getCurrentScore() : 0; + final LinkProperties prevLp = (oldNai != null) ? oldNai.linkProperties : null; + final NetworkCapabilities prevNc = (oldNai != null) ? oldNai.networkCapabilities : null; + + mService.mDefaultNetworkMetrics.logDefaultNetworkEvent(timeMs, network, score, validated, + lp, nc, prevNetwork, prevScore, prevLp, prevNc); + } + @Test + public void testDefaultNetworkEvents() throws Exception { + final long cell = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_CELLULAR}); + final long wifi = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_WIFI}); + + NetworkAgentInfo[][] defaultNetworks = { + // nothing -> cell + {null, makeNai(100, 10, false, true, cell)}, + // cell -> wifi + {makeNai(100, 50, true, true, cell), makeNai(101, 20, true, false, wifi)}, + // wifi -> nothing + {makeNai(101, 60, true, false, wifi), null}, + // nothing -> cell + {null, makeNai(102, 10, true, true, cell)}, + // cell -> wifi + {makeNai(102, 50, true, true, cell), makeNai(103, 20, true, false, wifi)}, + }; + + long timeMs = mService.mDefaultNetworkMetrics.creationTimeMs; + long durationMs = 1001; + for (NetworkAgentInfo[] pair : defaultNetworks) { + timeMs += durationMs; + durationMs += durationMs; + logDefaultNetworkEvent(timeMs, pair[1], pair[0]); + } + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 5", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " default_network_event <", + " default_network_duration_ms: 1001", + " final_score: 0", + " initial_score: 0", + " ip_support: 0", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 100", + " time_ms: 0", + " transports: 1", + " default_network_event <", + " default_network_duration_ms: 2002", + " final_score: 50", + " initial_score: 10", + " ip_support: 3", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 2002", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 101", + " time_ms: 0", + " transports: 2", + " default_network_event <", + " default_network_duration_ms: 4004", + " final_score: 60", + " initial_score: 20", + " ip_support: 1", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 2", + " previous_network_ip_support: 0", + " validation_duration_ms: 4004", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 5", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " default_network_event <", + " default_network_duration_ms: 8008", + " final_score: 0", + " initial_score: 0", + " ip_support: 0", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 4", + " previous_network_ip_support: 0", + " validation_duration_ms: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 102", + " time_ms: 0", + " transports: 1", + " default_network_event <", + " default_network_duration_ms: 16016", + " final_score: 50", + " initial_score: 10", + " ip_support: 3", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 4", + " previous_network_ip_support: 0", + " validation_duration_ms: 16016", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, getdump("flush")); + } + + @Test + public void testEndToEndLogging() throws Exception { + // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto. + IpConnectivityLog logger = new IpConnectivityLog(mService.impl); + + ApfStats apfStats = new ApfStats.Builder() + .setDurationMs(45000) + .setReceivedRas(10) + .setMatchingRas(2) + .setDroppedRas(2) + .setParseErrors(2) + .setZeroLifetimeRas(1) + .setProgramUpdates(4) + .setProgramUpdatesAll(7) + .setProgramUpdatesAllowingMulticast(3) + .setMaxProgramSize(2048) + .build(); + + final ValidationProbeEvent validationEv = new ValidationProbeEvent.Builder() + .setDurationMs(40730) + .setProbeType(ValidationProbeEvent.PROBE_HTTP, true) + .setReturnCode(204) + .build(); + + final DhcpClientEvent event = new DhcpClientEvent.Builder() + .setMsg("SomeState") + .setDurationMs(192) + .build(); + Parcelable[] events = { + new IpReachabilityEvent(IpReachabilityEvent.NUD_FAILED), event, + new IpManagerEvent(IpManagerEvent.PROVISIONING_OK, 5678), + validationEv, + apfStats, + new RaEvent(2000, 400, 300, -1, 1000, -1) + }; + + for (int i = 0; i < events.length; i++) { + ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent(); + ev.timestamp = 100 * (i + 1); + ev.ifname = "wlan0"; + ev.data = events[i]; + logger.log(ev); + } + + // netId, errno, latency, destination + connectEvent(100, OsConstants.EALREADY, 0, EXAMPLE_IPV4); + connectEvent(100, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6); + connectEvent(100, 0, 110, EXAMPLE_IPV4); + connectEvent(101, 0, 23, EXAMPLE_IPV4); + connectEvent(101, 0, 45, EXAMPLE_IPV6); + connectEvent(100, OsConstants.EAGAIN, 0, EXAMPLE_IPV4); + + // netId, type, return code, latency + dnsEvent(100, EVENT_GETADDRINFO, 0, 3456); + dnsEvent(100, EVENT_GETADDRINFO, 3, 45); + dnsEvent(100, EVENT_GETHOSTBYNAME, 0, 638); + dnsEvent(101, EVENT_GETADDRINFO, 0, 56); + dnsEvent(101, EVENT_GETHOSTBYNAME, 0, 34); + + // iface, uid + final byte[] mac = {0x48, 0x7c, 0x2b, 0x6a, 0x3e, 0x4b}; + final String srcIp = "192.168.2.1"; + final String dstIp = "192.168.2.23"; + final int sport = 2356; + final int dport = 13489; + final long now = 1001L; + final int v4 = 0x800; + final int tcp = 6; + final int udp = 17; + wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 10008, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", -1, v4, udp, mac, srcIp, dstIp, sport, dport, 1001L); + wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, 1001L); + + long timeMs = mService.mDefaultNetworkMetrics.creationTimeMs; + final long cell = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_CELLULAR}); + final long wifi = BitUtils.packBits(new int[]{NetworkCapabilities.TRANSPORT_WIFI}); + NetworkAgentInfo cellNai = makeNai(100, 50, false, true, cell); + NetworkAgentInfo wifiNai = makeNai(101, 60, true, false, wifi); + logDefaultNetworkEvent(timeMs + 200L, cellNai, null); + logDefaultNetworkEvent(timeMs + 300L, wifiNai, cellNai); + + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 100", + " transports: 0", + " ip_reachability_event <", + " event_type: 512", + " if_name: \"\"", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 200", + " transports: 0", + " dhcp_event <", + " duration_ms: 192", + " if_name: \"\"", + " state_transition: \"SomeState\"", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 300", + " transports: 0", + " ip_provisioning_event <", + " event_type: 1", + " if_name: \"\"", + " latency_ms: 5678", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 400", + " transports: 0", + " validation_probe_event <", + " latency_ms: 40730", + " probe_result: 204", + " probe_type: 257", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 500", + " transports: 0", + " apf_statistics <", + " dropped_ras: 2", + " duration_ms: 45000", + " matching_ras: 2", + " max_program_size: 2048", + " parse_errors: 2", + " program_updates: 4", + " program_updates_all: 7", + " program_updates_allowing_multicast: 3", + " received_ras: 10", + " total_packet_dropped: 0", + " total_packet_processed: 0", + " zero_lifetime_ras: 1", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 600", + " transports: 0", + " ra_event <", + " dnssl_lifetime: -1", + " prefix_preferred_lifetime: 300", + " prefix_valid_lifetime: 400", + " rdnss_lifetime: 1000", + " route_info_lifetime: -1", + " router_lifetime: 2000", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 5", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " default_network_event <", + " default_network_duration_ms: 200", + " final_score: 0", + " initial_score: 0", + " ip_support: 0", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 100", + " time_ms: 0", + " transports: 1", + " default_network_event <", + " default_network_duration_ms: 100", + " final_score: 50", + " initial_score: 50", + " ip_support: 2", + " no_default_network_duration_ms: 0", + " previous_default_network_link_layer: 0", + " previous_network_ip_support: 0", + " validation_duration_ms: 100", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " connect_statistics <", + " connect_blocking_count: 1", + " connect_count: 3", + " errnos_counters <", + " key: 11", + " value: 1", + " >", + " ipv6_addr_count: 1", + " latencies_ms: 110", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " connect_statistics <", + " connect_blocking_count: 2", + " connect_count: 2", + " ipv6_addr_count: 1", + " latencies_ms: 23", + " latencies_ms: 45", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 3456", + " latencies_ms: 45", + " latencies_ms: 638", + " return_codes: 0", + " return_codes: 3", + " return_codes: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 2", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 56", + " latencies_ms: 34", + " return_codes: 0", + " return_codes: 0", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 3", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 6", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 3", + " >", + " ip_next_header_counts <", + " key: 17", + " value: 3", + " >", + " l2_broadcast_count: 0", + " l2_multicast_count: 0", + " l2_unicast_count: 6", + " no_uid_wakeups: 1", + " non_application_wakeups: 0", + " root_wakeups: 0", + " system_wakeups: 2", + " total_wakeups: 6", + " >", + ">", + "version: 2\n"); + + verifySerialization(want, getdump("flush")); + } + + String getdump(String ... command) { + StringWriter buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + mService.impl.dump(null, writer, command); + return buffer.toString(); + } + + private void setCapabilities(int netId) { + final ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback = + ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); + verify(mCm).registerNetworkCallback(any(), networkCallback.capture()); + networkCallback.getValue().onCapabilitiesChanged(new Network(netId), + netId == 100 ? CAPABILITIES_WIFI : CAPABILITIES_CELL); + } + + void connectEvent(int netId, int error, int latencyMs, String ipAddr) throws Exception { + setCapabilities(netId); + mNetdListener.onConnectEvent(netId, error, latencyMs, ipAddr, 80, 1); + } + + void dnsEvent(int netId, int type, int result, int latency) throws Exception { + setCapabilities(netId); + mNetdListener.onDnsEvent(netId, type, result, latency, "", null, 0, 0); + } + + void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp, + String dstIp, int sport, int dport, long now) throws Exception { + String prefix = NetdEventListenerService.WAKEUP_EVENT_IFACE_PREFIX + iface; + mNetdListener.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now); + } + + NetworkAgentInfo makeNai(int netId, int score, boolean ipv4, boolean ipv6, long transports) { + NetworkAgentInfo nai = mock(NetworkAgentInfo.class); + when(nai.network()).thenReturn(new Network(netId)); + when(nai.getCurrentScore()).thenReturn(score); + nai.linkProperties = new LinkProperties(); + nai.networkCapabilities = new NetworkCapabilities(); + nai.lastValidated = true; + for (int t : BitUtils.unpackBits(transports)) { + nai.networkCapabilities.addTransportType(t); + } + if (ipv4) { + nai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.12/24")); + nai.linkProperties.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"))); + } + if (ipv6) { + nai.linkProperties.addLinkAddress(new LinkAddress("2001:db8:dead:beef:f00::a0/64")); + nai.linkProperties.addRoute(new RouteInfo(new IpPrefix("::/0"))); + } + return nai; + } + + + + static void verifySerialization(String want, String output) { + try { + byte[] got = Base64.decode(output, Base64.DEFAULT); + IpConnectivityLogClass.IpConnectivityLog log = + IpConnectivityLogClass.IpConnectivityLog.parseFrom(got); + assertEquals(want, log.toString()); + } catch (Exception e) { + fail(e.toString()); + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..36e229d8aa731c6ffe8798d2ca0a8694df98ddc0 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +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.when; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.ConnectivityResources; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkProvider; +import android.net.NetworkScore; +import android.os.Binder; +import android.text.format.DateUtils; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.server.ConnectivityService; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class LingerMonitorTest { + static final String CELLULAR = "CELLULAR"; + static final String WIFI = "WIFI"; + + static final long LOW_RATE_LIMIT = DateUtils.MINUTE_IN_MILLIS; + static final long HIGH_RATE_LIMIT = 0; + + static final int LOW_DAILY_LIMIT = 2; + static final int HIGH_DAILY_LIMIT = 1000; + + private static final int TEST_LINGER_DELAY_MS = 400; + + LingerMonitor mMonitor; + + @Mock ConnectivityService mConnService; + @Mock IDnsResolver mDnsResolver; + @Mock INetd mNetd; + @Mock Context mCtx; + @Mock NetworkNotificationManager mNotifier; + @Mock Resources mResources; + @Mock QosCallbackTracker mQosCallbackTracker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mCtx.getResources()).thenReturn(mResources); + when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity"); + ConnectivityResources.setResourcesContextForTest(mCtx); + + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT); + } + + @After + public void tearDown() { + ConnectivityResources.setResourcesContextForTest(null); + } + + @Test + public void testTransitions() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo nai1 = wifiNai(100); + NetworkAgentInfo nai2 = cellNai(101); + + assertTrue(mMonitor.isNotificationEnabled(nai1, nai2)); + assertFalse(mMonitor.isNotificationEnabled(nai2, nai1)); + } + + @Test + public void testNotificationOnLinger() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + } + + @Test + public void testToastOnLinger() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyToast(from, to); + } + + @Test + public void testNotificationClearedAfterDisconnect() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + + mMonitor.noteDisconnect(to); + verify(mNotifier, times(1)).clearNotification(100); + } + + @Test + public void testNotificationClearedAfterSwitchingBack() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + + mMonitor.noteLingerDefaultNetwork(to, from); + verify(mNotifier, times(1)).clearNotification(100); + } + + @Test + public void testUniqueToast() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyToast(from, to); + + mMonitor.noteLingerDefaultNetwork(to, from); + verify(mNotifier, times(1)).clearNotification(100); + + reset(mNotifier); + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testMultipleNotifications() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo cell = cellNai(102); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + verify(mNotifier, times(1)).clearNotification(100); + + reset(mNotifier); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNotification(wifi2, cell); + } + + @Test + public void testRateLimiting() throws InterruptedException { + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, LOW_RATE_LIMIT); + + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo wifi3 = wifiNai(102); + NetworkAgentInfo cell = cellNai(103); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNoNotifications(); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi3); + mMonitor.noteLingerDefaultNetwork(wifi3, cell); + verifyNoNotifications(); + } + + @Test + public void testDailyLimiting() throws InterruptedException { + mMonitor = new TestableLingerMonitor(mCtx, mNotifier, LOW_DAILY_LIMIT, HIGH_RATE_LIMIT); + + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo wifi1 = wifiNai(100); + NetworkAgentInfo wifi2 = wifiNai(101); + NetworkAgentInfo wifi3 = wifiNai(102); + NetworkAgentInfo cell = cellNai(103); + + mMonitor.noteLingerDefaultNetwork(wifi1, cell); + verifyNotification(wifi1, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi2); + mMonitor.noteLingerDefaultNetwork(wifi2, cell); + verifyNotification(wifi2, cell); + reset(mNotifier); + + Thread.sleep(50); + mMonitor.noteLingerDefaultNetwork(cell, wifi3); + mMonitor.noteLingerDefaultNetwork(wifi3, cell); + verifyNoNotifications(); + } + + @Test + public void testUniqueNotification() { + setNotificationSwitch(transition(WIFI, CELLULAR)); + setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + + mMonitor.noteLingerDefaultNetwork(to, from); + verify(mNotifier, times(1)).clearNotification(100); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNotification(from, to); + } + + @Test + public void testIgnoreNeverValidatedNetworks() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + from.everValidated = false; + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testIgnoreCurrentlyValidatedNetworks() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + from.lastValidated = true; + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testNoNotificationType() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testNoTransitionToNotify() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_NONE); + setNotificationSwitch(transition(WIFI, CELLULAR)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + @Test + public void testDifferentTransitionToNotify() { + setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST); + setNotificationSwitch(transition(CELLULAR, WIFI)); + NetworkAgentInfo from = wifiNai(100); + NetworkAgentInfo to = cellNai(101); + + mMonitor.noteLingerDefaultNetwork(from, to); + verifyNoNotifications(); + } + + void setNotificationSwitch(String... transitions) { + when(mResources.getStringArray(R.array.config_networkNotifySwitches)) + .thenReturn(transitions); + } + + String transition(String from, String to) { + return from + "-" + to; + } + + void setNotificationType(int type) { + when(mResources.getInteger(R.integer.config_networkNotifySwitchType)).thenReturn(type); + } + + void verifyNoToast() { + verify(mNotifier, never()).showToast(any(), any()); + } + + void verifyNoNotification() { + verify(mNotifier, never()) + .showNotification(anyInt(), any(), any(), any(), any(), anyBoolean()); + } + + void verifyNoNotifications() { + verifyNoToast(); + verifyNoNotification(); + } + + void verifyToast(NetworkAgentInfo from, NetworkAgentInfo to) { + verifyNoNotification(); + verify(mNotifier, times(1)).showToast(from, to); + } + + void verifyNotification(NetworkAgentInfo from, NetworkAgentInfo to) { + verifyNoToast(); + verify(mNotifier, times(1)).showNotification(eq(from.network.netId), + eq(NotificationType.NETWORK_SWITCH), eq(from), eq(to), any(), eq(true)); + } + + NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName) { + NetworkInfo info = new NetworkInfo(networkType, 0, networkTypeName, ""); + NetworkCapabilities caps = new NetworkCapabilities(); + caps.addCapability(0); + caps.addTransportType(transport); + NetworkAgentInfo nai = new NetworkAgentInfo(null, new Network(netId), info, + new LinkProperties(), caps, new NetworkScore.Builder().setLegacyInt(50).build(), + mCtx, null, new NetworkAgentConfig.Builder().build(), mConnService, mNetd, + mDnsResolver, NetworkProvider.ID_NONE, Binder.getCallingUid(), TEST_LINGER_DELAY_MS, + mQosCallbackTracker, new ConnectivityService.Dependencies()); + nai.everValidated = true; + return nai; + } + + NetworkAgentInfo wifiNai(int netId) { + return nai(netId, NetworkCapabilities.TRANSPORT_WIFI, + ConnectivityManager.TYPE_WIFI, WIFI); + } + + NetworkAgentInfo cellNai(int netId) { + return nai(netId, NetworkCapabilities.TRANSPORT_CELLULAR, + ConnectivityManager.TYPE_MOBILE, CELLULAR); + } + + public static class TestableLingerMonitor extends LingerMonitor { + public TestableLingerMonitor(Context c, NetworkNotificationManager n, int l, long r) { + super(c, n, l, r); + } + @Override protected PendingIntent createNotificationIntent() { + return null; + } + } +} diff --git a/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java b/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..5064b9bd91b9b326126c1d06a3a3f8746fbcadcf --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/MetricsTestUtil.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.net.ConnectivityMetricsEvent; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.function.Consumer; + +abstract public class MetricsTestUtil { + private MetricsTestUtil() { + } + + static ConnectivityMetricsEvent ev(Parcelable p) { + ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent(); + ev.timestamp = 1L; + ev.data = p; + return ev; + } + + static ConnectivityMetricsEvent describeIpEvent(Consumer<Parcel>... fs) { + Parcel p = Parcel.obtain(); + for (Consumer<Parcel> f : fs) { + f.accept(p); + } + p.setDataPosition(0); + return ev(p.readParcelable(ClassLoader.getSystemClassLoader())); + } + + static Consumer<Parcel> aType(Class<?> c) { + return aString(c.getName()); + } + + static Consumer<Parcel> aBool(boolean b) { + return aByte((byte) (b ? 1 : 0)); + } + + static Consumer<Parcel> aByte(byte b) { + return (p) -> p.writeByte(b); + } + + static Consumer<Parcel> anInt(int i) { + return (p) -> p.writeInt(i); + } + + static Consumer<Parcel> aLong(long l) { + return (p) -> p.writeLong(l); + } + + static Consumer<Parcel> aString(String s) { + return (p) -> p.writeString(s); + } + + static Consumer<Parcel> aByteArray(byte... ary) { + return (p) -> p.writeByteArray(ary); + } + + static Consumer<Parcel> anIntArray(int... ary) { + return (p) -> p.writeIntArray(ary); + } + + static byte b(int i) { + return (byte) i; + } +} diff --git a/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..38f6d7f3172d90dea1163f62967c69f1dc0d8c0f --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/MultipathPolicyTrackerTest.java @@ -0,0 +1,381 @@ +/* + * 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.server.connectivity; + +import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkPolicy.LIMIT_DISABLED; +import static android.net.NetworkPolicy.SNOOZE_NEVER; +import static android.net.NetworkPolicy.WARNING_DISABLED; +import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES; + +import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH; +import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN; + +import static junit.framework.TestCase.assertNotNull; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.usage.NetworkStatsManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.EthernetNetworkSpecifier; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkPolicy; +import android.net.NetworkPolicyManager; +import android.net.NetworkTemplate; +import android.net.TelephonyNetworkSpecifier; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.test.mock.MockContentResolver; +import android.util.DataUnit; +import android.util.RecurrenceRule; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.server.LocalServices; +import com.android.server.net.NetworkPolicyManagerInternal; +import com.android.server.net.NetworkStatsManagerInternal; + +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.Mockito; +import org.mockito.MockitoAnnotations; + +import java.time.Clock; +import java.time.Instant; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MultipathPolicyTrackerTest { + private static final Network TEST_NETWORK = new Network(123); + private static final int POLICY_SNOOZED = -100; + + @Mock private Context mContext; + @Mock private Context mUserAllContext; + @Mock private Resources mResources; + @Mock private Handler mHandler; + @Mock private MultipathPolicyTracker.Dependencies mDeps; + @Mock private Clock mClock; + @Mock private ConnectivityManager mCM; + @Mock private NetworkPolicyManager mNPM; + @Mock private NetworkStatsManager mStatsManager; + @Mock private NetworkPolicyManagerInternal mNPMI; + @Mock private NetworkStatsManagerInternal mNetworkStatsManagerInternal; + @Mock private TelephonyManager mTelephonyManager; + private MockContentResolver mContentResolver; + + private ArgumentCaptor<BroadcastReceiver> mConfigChangeReceiverCaptor; + + private MultipathPolicyTracker mTracker; + + private Clock mPreviousRecurrenceRuleClock; + private boolean mRecurrenceRuleClockMocked; + + private <T> void mockService(String serviceName, Class<T> serviceClass, T service) { + when(mContext.getSystemServiceName(serviceClass)).thenReturn(serviceName); + when(mContext.getSystemService(serviceName)).thenReturn(service); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mPreviousRecurrenceRuleClock = RecurrenceRule.sClock; + RecurrenceRule.sClock = mClock; + mRecurrenceRuleClockMocked = true; + + mConfigChangeReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); + + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo()); + // Mock user id to all users that Context#registerReceiver will register with all users too. + doReturn(UserHandle.ALL.getIdentifier()).when(mUserAllContext).getUserId(); + when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())) + .thenReturn(mUserAllContext); + when(mUserAllContext.registerReceiver(mConfigChangeReceiverCaptor.capture(), + argThat(f -> f.hasAction(ACTION_CONFIGURATION_CHANGED)), any(), any())) + .thenReturn(null); + + when(mDeps.getClock()).thenReturn(mClock); + + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + mContentResolver = Mockito.spy(new MockContentResolver(mContext)); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + Settings.Global.clearProviderForTest(); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + + mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, mCM); + mockService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, mNPM); + mockService(Context.NETWORK_STATS_SERVICE, NetworkStatsManager.class, mStatsManager); + mockService(Context.TELEPHONY_SERVICE, TelephonyManager.class, mTelephonyManager); + + LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class); + LocalServices.addService(NetworkPolicyManagerInternal.class, mNPMI); + + LocalServices.removeServiceForTest(NetworkStatsManagerInternal.class); + LocalServices.addService(NetworkStatsManagerInternal.class, mNetworkStatsManagerInternal); + + mTracker = new MultipathPolicyTracker(mContext, mHandler, mDeps); + } + + @After + public void tearDown() { + // Avoid setting static clock to null (which should normally not be the case) + // if MockitoAnnotations.initMocks threw an exception + if (mRecurrenceRuleClockMocked) { + RecurrenceRule.sClock = mPreviousRecurrenceRuleClock; + } + mRecurrenceRuleClockMocked = false; + } + + private void setDefaultQuotaGlobalSetting(long setting) { + Settings.Global.putInt(mContentResolver, NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES, + (int) setting); + } + + private void testGetMultipathPreference( + long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit, + long defaultGlobalSetting, long defaultResSetting, boolean roaming) { + + // TODO: tests should not use ZoneId.systemDefault() once code handles TZ correctly. + final ZonedDateTime now = ZonedDateTime.ofInstant( + Instant.parse("2017-04-02T10:11:12Z"), ZoneId.systemDefault()); + final ZonedDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS); + when(mClock.millis()).thenReturn(now.toInstant().toEpochMilli()); + when(mClock.instant()).thenReturn(now.toInstant()); + when(mClock.getZone()).thenReturn(ZoneId.systemDefault()); + + // Setup plan quota + when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH)) + .thenReturn(subscriptionQuota); + + // Setup user policy warning / limit + if (policyWarning != WARNING_DISABLED || policyLimit != LIMIT_DISABLED) { + final Instant recurrenceStart = Instant.parse("2017-04-01T00:00:00Z"); + final RecurrenceRule recurrenceRule = new RecurrenceRule( + ZonedDateTime.ofInstant( + recurrenceStart, + ZoneId.systemDefault()), + null /* end */, + Period.ofMonths(1)); + final boolean snoozeWarning = policyWarning == POLICY_SNOOZED; + final boolean snoozeLimit = policyLimit == POLICY_SNOOZED; + when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[] { + new NetworkPolicy( + NetworkTemplate.buildTemplateMobileWildcard(), + recurrenceRule, + snoozeWarning ? 0 : policyWarning, + snoozeLimit ? 0 : policyLimit, + snoozeWarning ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER, + snoozeLimit ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER, + SNOOZE_NEVER, + true /* metered */, + false /* inferred */) + }); + } else { + when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[0]); + } + + // Setup default quota in settings and resources + if (defaultGlobalSetting > 0) { + setDefaultQuotaGlobalSetting(defaultGlobalSetting); + } + when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes)) + .thenReturn((int) defaultResSetting); + + when(mNetworkStatsManagerInternal.getNetworkTotalBytes( + any(), + eq(startOfDay.toInstant().toEpochMilli()), + eq(now.toInstant().toEpochMilli()))).thenReturn(usedBytesToday); + + ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback = + ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); + mTracker.start(); + verify(mCM).registerNetworkCallback(any(), networkCallback.capture(), any()); + + // Simulate callback after capability changes + NetworkCapabilities capabilities = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_CELLULAR) + .setNetworkSpecifier(new EthernetNetworkSpecifier("eth234")); + if (!roaming) { + capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING); + } + networkCallback.getValue().onCapabilitiesChanged( + TEST_NETWORK, + capabilities); + + // make sure it also works with the new introduced TelephonyNetworkSpecifier + capabilities = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_CELLULAR) + .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(234).build()); + if (!roaming) { + capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING); + } + networkCallback.getValue().onCapabilitiesChanged( + TEST_NETWORK, + capabilities); + } + + @Test + public void testGetMultipathPreference_SubscriptionQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */, + DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */, + DataUnit.MEGABYTES.toBytes(100) /* policyWarning */, + LIMIT_DISABLED, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + } + + @Test + public void testGetMultipathPreference_UserWarningQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + // 29 days from Apr. 2nd to May 1st + DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyWarning */, + LIMIT_DISABLED, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + } + + @Test + public void testGetMultipathPreference_SnoozedWarningQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + // 29 days from Apr. 2nd to May 1st + POLICY_SNOOZED /* policyWarning */, + DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyLimit */, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + } + + @Test + public void testGetMultipathPreference_SnoozedBothQuota() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + // 29 days from Apr. 2nd to May 1st + POLICY_SNOOZED /* policyWarning */, + POLICY_SNOOZED /* policyLimit */, + DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */, + 2_500_000 /* defaultResSetting */, + false /* roaming */); + + // Default global setting should be used: 12 - 7 = 5 + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any()); + } + + @Test + public void testGetMultipathPreference_SettingChanged() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + WARNING_DISABLED, + LIMIT_DISABLED, + -1 /* defaultGlobalSetting */, + DataUnit.MEGABYTES.toBytes(10) /* defaultResSetting */, + false /* roaming */); + + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any()); + + // Update setting + setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14)); + mTracker.mSettingsObserver.onChange( + false, Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES)); + + // Callback must have been re-registered with new setting + verify(mStatsManager, times(1)).unregisterUsageCallback(any()); + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + } + + @Test + public void testGetMultipathPreference_ResourceChanged() { + testGetMultipathPreference( + DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */, + OPPORTUNISTIC_QUOTA_UNKNOWN, + WARNING_DISABLED, + LIMIT_DISABLED, + -1 /* defaultGlobalSetting */, + DataUnit.MEGABYTES.toBytes(14) /* defaultResSetting */, + false /* roaming */); + + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any()); + + when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes)) + .thenReturn((int) DataUnit.MEGABYTES.toBytes(16)); + + final BroadcastReceiver configChangeReceiver = mConfigChangeReceiverCaptor.getValue(); + assertNotNull(configChangeReceiver); + configChangeReceiver.onReceive(mContext, new Intent()); + + // Uses the new setting (16 - 2 = 14MB) + verify(mStatsManager, times(1)).registerUsageCallback( + any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any()); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9b2a638f8b3991f0aae5c704e966f535186d93c9 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/Nat464XlatTest.java @@ -0,0 +1,555 @@ +/* + * 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.server.connectivity; + +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.net.ConnectivityManager; +import android.net.IDnsResolver; +import android.net.INetd; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.ConnectivityService; + +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; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class Nat464XlatTest { + + static final String BASE_IFACE = "test0"; + static final String STACKED_IFACE = "v4-test0"; + static final LinkAddress V6ADDR = new LinkAddress("2001:db8:1::f00/64"); + static final LinkAddress ADDR = new LinkAddress("192.0.2.5/29"); + static final String NAT64_PREFIX = "64:ff9b::/96"; + static final String OTHER_NAT64_PREFIX = "2001:db8:0:64::/96"; + static final int NETID = 42; + + @Mock ConnectivityService mConnectivity; + @Mock IDnsResolver mDnsResolver; + @Mock INetd mNetd; + @Mock NetworkAgentInfo mNai; + + TestLooper mLooper; + Handler mHandler; + NetworkAgentConfig mAgentConfig = new NetworkAgentConfig(); + + Nat464Xlat makeNat464Xlat(boolean isCellular464XlatEnabled) { + return new Nat464Xlat(mNai, mNetd, mDnsResolver, new ConnectivityService.Dependencies()) { + @Override protected int getNetId() { + return NETID; + } + + @Override protected boolean isCellular464XlatEnabled() { + return isCellular464XlatEnabled; + } + }; + } + + private void markNetworkConnected() { + mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, "", ""); + } + + private void markNetworkDisconnected() { + mNai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED, "", ""); + } + + @Before + public void setUp() throws Exception { + mLooper = new TestLooper(); + mHandler = new Handler(mLooper.getLooper()); + + MockitoAnnotations.initMocks(this); + + mNai.linkProperties = new LinkProperties(); + mNai.linkProperties.setInterfaceName(BASE_IFACE); + mNai.networkInfo = new NetworkInfo(null); + mNai.networkInfo.setType(ConnectivityManager.TYPE_WIFI); + mNai.networkCapabilities = new NetworkCapabilities(); + markNetworkConnected(); + when(mNai.connService()).thenReturn(mConnectivity); + when(mNai.netAgentConfig()).thenReturn(mAgentConfig); + when(mNai.handler()).thenReturn(mHandler); + final InterfaceConfigurationParcel mConfig = new InterfaceConfigurationParcel(); + when(mNetd.interfaceGetCfg(eq(STACKED_IFACE))).thenReturn(mConfig); + mConfig.ipv4Addr = ADDR.getAddress().getHostAddress(); + mConfig.prefixLength = ADDR.getPrefixLength(); + } + + private void assertRequiresClat(boolean expected, NetworkAgentInfo nai) { + Nat464Xlat nat = makeNat464Xlat(true); + String msg = String.format("requiresClat expected %b for type=%d state=%s skip=%b " + + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(), + nai.networkInfo.getDetailedState(), + mAgentConfig.skip464xlat, nai.linkProperties.getNat64Prefix(), + nai.linkProperties.getLinkAddresses()); + assertEquals(msg, expected, nat.requiresClat(nai)); + } + + private void assertShouldStartClat(boolean expected, NetworkAgentInfo nai) { + Nat464Xlat nat = makeNat464Xlat(true); + String msg = String.format("shouldStartClat expected %b for type=%d state=%s skip=%b " + + "nat64Prefix=%s addresses=%s", expected, nai.networkInfo.getType(), + nai.networkInfo.getDetailedState(), + mAgentConfig.skip464xlat, nai.linkProperties.getNat64Prefix(), + nai.linkProperties.getLinkAddresses()); + assertEquals(msg, expected, nat.shouldStartClat(nai)); + } + + @Test + public void testRequiresClat() throws Exception { + final int[] supportedTypes = { + ConnectivityManager.TYPE_MOBILE, + ConnectivityManager.TYPE_WIFI, + ConnectivityManager.TYPE_ETHERNET, + }; + + // NetworkInfo doesn't allow setting the State directly, but rather + // requires setting DetailedState in order set State as a side-effect. + final NetworkInfo.DetailedState[] supportedDetailedStates = { + NetworkInfo.DetailedState.CONNECTED, + NetworkInfo.DetailedState.SUSPENDED, + }; + + LinkProperties oldLp = new LinkProperties(mNai.linkProperties); + for (int type : supportedTypes) { + mNai.networkInfo.setType(type); + for (NetworkInfo.DetailedState state : supportedDetailedStates) { + mNai.networkInfo.setDetailedState(state, "reason", "extraInfo"); + + mNai.linkProperties.setNat64Prefix(new IpPrefix(OTHER_NAT64_PREFIX)); + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties.addLinkAddress(new LinkAddress("fc00::1/64")); + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + assertRequiresClat(true, mNai); + assertShouldStartClat(true, mNai); + + mAgentConfig.skip464xlat = true; + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mAgentConfig.skip464xlat = false; + assertRequiresClat(true, mNai); + assertShouldStartClat(true, mNai); + + mNai.linkProperties.addLinkAddress(new LinkAddress("192.0.2.2/24")); + assertRequiresClat(false, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties.removeLinkAddress(new LinkAddress("192.0.2.2/24")); + assertRequiresClat(true, mNai); + assertShouldStartClat(true, mNai); + + mNai.linkProperties.setNat64Prefix(null); + assertRequiresClat(true, mNai); + assertShouldStartClat(false, mNai); + + mNai.linkProperties = new LinkProperties(oldLp); + } + } + } + + private void makeClatUnnecessary(boolean dueToDisconnect) { + if (dueToDisconnect) { + markNetworkDisconnected(); + } else { + mNai.linkProperties.addLinkAddress(ADDR); + } + } + + private void checkNormalStartAndStop(boolean dueToDisconnect) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class); + + mNai.linkProperties.addLinkAddress(V6ADDR); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + // Start clat. + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE)); + verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // Stop clat (Network disconnects, IPv4 addr appears, ...). + makeClatUnnecessary(dueToDisconnect); + nat.stop(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // Stacked interface removed notification arrives and is ignored. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + @Test + public void testNormalStartAndStopDueToDisconnect() throws Exception { + checkNormalStartAndStop(true); + } + + @Test + public void testNormalStartAndStopDueToIpv4Addr() throws Exception { + checkNormalStartAndStop(false); + } + + private void checkStartStopStart(boolean interfaceRemovedFirst) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class); + InOrder inOrder = inOrder(mNetd, mConnectivity); + + mNai.linkProperties.addLinkAddress(V6ADDR); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // ConnectivityService stops clat (Network disconnects, IPv4 addr appears, ...). + nat.stop(); + + inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE)); + + inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + + if (interfaceRemovedFirst) { + // Stacked interface removed notification arrives and is ignored. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + nat.interfaceLinkStateChanged(STACKED_IFACE, false); + mLooper.dispatchNext(); + } + + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + inOrder.verifyNoMoreInteractions(); + + nat.start(); + + inOrder.verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + if (!interfaceRemovedFirst) { + // Stacked interface removed notification arrives and is ignored. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + nat.interfaceLinkStateChanged(STACKED_IFACE, false); + mLooper.dispatchNext(); + } + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + inOrder.verify(mConnectivity).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // ConnectivityService stops clat again. + nat.stop(); + + inOrder.verify(mNetd).clatdStop(eq(BASE_IFACE)); + + inOrder.verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testStartStopStart() throws Exception { + checkStartStopStart(true); + } + + @Test + public void testStartStopStartBeforeInterfaceRemoved() throws Exception { + checkStartStopStart(false); + } + + @Test + public void testClatdCrashWhileRunning() throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + ArgumentCaptor<LinkProperties> c = ArgumentCaptor.forClass(LinkProperties.class); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // Stacked interface up notification arrives. + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + verify(mNetd).interfaceGetCfg(eq(STACKED_IFACE)); + verify(mConnectivity, times(1)).handleUpdateLinkProperties(eq(mNai), c.capture()); + assertFalse(c.getValue().getStackedLinks().isEmpty()); + assertTrue(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertRunning(nat); + + // Stacked interface removed notification arrives (clatd crashed, ...). + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mConnectivity, times(2)).handleUpdateLinkProperties(eq(mNai), c.capture()); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertTrue(c.getValue().getStackedLinks().isEmpty()); + assertFalse(c.getValue().getAllInterfaceNames().contains(STACKED_IFACE)); + assertIdle(nat); + + // ConnectivityService stops clat: no-op. + nat.stop(); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + private void checkStopBeforeClatdStarts(boolean dueToDisconnect) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + + mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...) + makeClatUnnecessary(dueToDisconnect); + nat.stop(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // In-flight interface up notification arrives: no-op + nat.interfaceLinkStateChanged(STACKED_IFACE, true); + mLooper.dispatchNext(); + + // Interface removed notification arrives after stopClatd() takes effect: no-op. + nat.interfaceRemoved(STACKED_IFACE); + mLooper.dispatchNext(); + + assertIdle(nat); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + @Test + public void testStopDueToDisconnectBeforeClatdStarts() throws Exception { + checkStopBeforeClatdStarts(true); + } + + @Test + public void testStopDueToIpv4AddrBeforeClatdStarts() throws Exception { + checkStopBeforeClatdStarts(false); + } + + private void checkStopAndClatdNeverStarts(boolean dueToDisconnect) throws Exception { + Nat464Xlat nat = makeNat464Xlat(true); + + mNai.linkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + + nat.setNat64PrefixFromDns(new IpPrefix(NAT64_PREFIX)); + + nat.start(); + + verify(mNetd).clatdStart(eq(BASE_IFACE), eq(NAT64_PREFIX)); + + // ConnectivityService immediately stops clat (Network disconnects, IPv4 addr appears, ...) + makeClatUnnecessary(dueToDisconnect); + nat.stop(); + + verify(mNetd).clatdStop(eq(BASE_IFACE)); + verify(mDnsResolver).stopPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + verifyNoMoreInteractions(mNetd, mConnectivity); + } + + @Test + public void testStopDueToDisconnectAndClatdNeverStarts() throws Exception { + checkStopAndClatdNeverStarts(true); + } + + @Test + public void testStopDueToIpv4AddressAndClatdNeverStarts() throws Exception { + checkStopAndClatdNeverStarts(false); + } + + @Test + public void testNat64PrefixPreference() throws Exception { + final IpPrefix prefixFromDns = new IpPrefix(NAT64_PREFIX); + final IpPrefix prefixFromRa = new IpPrefix(OTHER_NAT64_PREFIX); + + Nat464Xlat nat = makeNat464Xlat(true); + + final LinkProperties emptyLp = new LinkProperties(); + LinkProperties fixedupLp; + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromDns(prefixFromDns); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromDns, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(prefixFromRa); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromRa, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(null); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromDns, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(prefixFromRa); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromRa, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromDns(null); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(prefixFromRa, fixedupLp.getNat64Prefix()); + + fixedupLp = new LinkProperties(); + nat.setNat64PrefixFromRa(null); + nat.fixupLinkProperties(emptyLp, fixedupLp); + assertEquals(null, fixedupLp.getNat64Prefix()); + } + + private void checkClatDisabledOnCellular(boolean onCellular) throws Exception { + // Disable 464xlat on cellular networks. + Nat464Xlat nat = makeNat464Xlat(false); + mNai.linkProperties.addLinkAddress(V6ADDR); + mNai.networkCapabilities.setTransportType(TRANSPORT_CELLULAR, onCellular); + nat.update(); + + final IpPrefix nat64Prefix = new IpPrefix(NAT64_PREFIX); + if (onCellular) { + // Prefix discovery is never started. + verify(mDnsResolver, never()).startPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // If a NAT64 prefix comes in from an RA, clat is not started either. + mNai.linkProperties.setNat64Prefix(nat64Prefix); + nat.setNat64PrefixFromRa(nat64Prefix); + nat.update(); + verify(mNetd, never()).clatdStart(anyString(), anyString()); + assertIdle(nat); + } else { + // Prefix discovery is started. + verify(mDnsResolver).startPrefix64Discovery(eq(NETID)); + assertIdle(nat); + + // If a NAT64 prefix comes in from an RA, clat is started. + mNai.linkProperties.setNat64Prefix(nat64Prefix); + nat.setNat64PrefixFromRa(nat64Prefix); + nat.update(); + verify(mNetd).clatdStart(BASE_IFACE, NAT64_PREFIX); + assertStarting(nat); + } + } + + @Test + public void testClatDisabledOnCellular() throws Exception { + checkClatDisabledOnCellular(true); + } + + @Test + public void testClatDisabledOnNonCellular() throws Exception { + checkClatDisabledOnCellular(false); + } + + static void assertIdle(Nat464Xlat nat) { + assertTrue("Nat464Xlat was not IDLE", !nat.isStarted()); + } + + static void assertStarting(Nat464Xlat nat) { + assertTrue("Nat464Xlat was not STARTING", nat.isStarting()); + } + + static void assertRunning(Nat464Xlat nat) { + assertTrue("Nat464Xlat was not RUNNING", nat.isRunning()); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..50aaaee2441895e2bbbf26070fa4705d701838e9 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java @@ -0,0 +1,554 @@ +/* + * Copyright (C) 2016, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.net.metrics.INetdEventListener.EVENT_GETADDRINFO; +import static android.net.metrics.INetdEventListener.EVENT_GETHOSTBYNAME; + +import static com.android.testutils.MiscAsserts.assertStringContains; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.system.OsConstants; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Base64; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent; +import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityLog; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetdEventListenerServiceTest { + private static final String EXAMPLE_IPV4 = "192.0.2.1"; + private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1"; + + private static final byte[] MAC_ADDR = + {(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b}; + + NetdEventListenerService mService; + ConnectivityManager mCm; + private static final NetworkCapabilities CAPABILITIES_WIFI = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + private static final NetworkCapabilities CAPABILITIES_CELL = new NetworkCapabilities.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(); + + @Before + public void setUp() { + mCm = mock(ConnectivityManager.class); + mService = new NetdEventListenerService(mCm); + } + + @Test + public void testWakeupEventLogging() throws Exception { + final int BUFFER_LENGTH = NetdEventListenerService.WAKEUP_EVENT_BUFFER_LENGTH; + final long now = System.currentTimeMillis(); + final String iface = "wlan0"; + final byte[] mac = MAC_ADDR; + final String srcIp = "192.168.2.1"; + final String dstIp = "192.168.2.23"; + final String srcIp6 = "2001:db8:4:fd00:a585:13d1:6a23:4fb4"; + final String dstIp6 = "2001:db8:4006:807::200a"; + final int sport = 2356; + final int dport = 13489; + + final int v4 = 0x800; + final int v6 = 0x86dd; + final int tcp = 6; + final int udp = 17; + final int icmp6 = 58; + + // Baseline without any event + String[] baseline = listNetdEvent(); + + int[] uids = {10001, 10002, 10004, 1000, 10052, 10023, 10002, 10123, 10004}; + wakeupEvent(iface, uids[0], v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent(iface, uids[1], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[2], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[3], v4, icmp6, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent(iface, uids[4], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[5], v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent(iface, uids[6], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[7], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent(iface, uids[8], v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + + String[] events2 = remove(listNetdEvent(), baseline); + int expectedLength2 = uids.length + 1; // +1 for the WakeupStats line + assertEquals(expectedLength2, events2.length); + assertStringContains(events2[0], "WakeupStats"); + assertStringContains(events2[0], "wlan0"); + assertStringContains(events2[0], "0x800"); + assertStringContains(events2[0], "0x86dd"); + for (int i = 0; i < uids.length; i++) { + String got = events2[i+1]; + assertStringContains(got, "WakeupEvent"); + assertStringContains(got, "wlan0"); + assertStringContains(got, "uid: " + uids[i]); + } + + int uid = 20000; + for (int i = 0; i < BUFFER_LENGTH * 2; i++) { + long ts = now + 10; + wakeupEvent(iface, uid, 0x800, 6, mac, srcIp, dstIp, 23, 24, ts); + } + + String[] events3 = remove(listNetdEvent(), baseline); + int expectedLength3 = BUFFER_LENGTH + 1; // +1 for the WakeupStats line + assertEquals(expectedLength3, events3.length); + assertStringContains(events2[0], "WakeupStats"); + assertStringContains(events2[0], "wlan0"); + for (int i = 1; i < expectedLength3; i++) { + String got = events3[i]; + assertStringContains(got, "WakeupEvent"); + assertStringContains(got, "wlan0"); + assertStringContains(got, "uid: " + uid); + } + + uid = 45678; + wakeupEvent(iface, uid, 0x800, 6, mac, srcIp, dstIp, 23, 24, now); + + String[] events4 = remove(listNetdEvent(), baseline); + String lastEvent = events4[events4.length - 1]; + assertStringContains(lastEvent, "WakeupEvent"); + assertStringContains(lastEvent, "wlan0"); + assertStringContains(lastEvent, "uid: " + uid); + } + + @Test + public void testWakeupStatsLogging() throws Exception { + final byte[] mac = MAC_ADDR; + final String srcIp = "192.168.2.1"; + final String dstIp = "192.168.2.23"; + final String srcIp6 = "2401:fa00:4:fd00:a585:13d1:6a23:4fb4"; + final String dstIp6 = "2404:6800:4006:807::200a"; + final int sport = 2356; + final int dport = 13489; + final long now = 1001L; + + final int v4 = 0x800; + final int v6 = 0x86dd; + final int tcp = 6; + final int udp = 17; + final int icmp6 = 58; + + wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("rmnet0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("rmnet0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("rmnet0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 10004, v4, udp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("rmnet0", 10052, v4, tcp, mac, srcIp, dstIp, sport, dport, now); + wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("rmnet0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now); + wakeupEvent("wlan0", 1010, v4, udp, mac, srcIp, dstIp, sport, dport, now); + + String got = flushStatistics(); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 3", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 4", + " >", + " ethertype_counts <", + " key: 34525", + " value: 1", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 5", + " >", + " l2_broadcast_count: 0", + " l2_multicast_count: 0", + " l2_unicast_count: 5", + " no_uid_wakeups: 0", + " non_application_wakeups: 0", + " root_wakeups: 0", + " system_wakeups: 2", + " total_wakeups: 5", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 0", + " time_ms: 0", + " transports: 0", + " wakeup_stats <", + " application_wakeups: 2", + " duration_sec: 0", + " ethertype_counts <", + " key: 2048", + " value: 5", + " >", + " ethertype_counts <", + " key: 34525", + " value: 5", + " >", + " ip_next_header_counts <", + " key: 6", + " value: 3", + " >", + " ip_next_header_counts <", + " key: 17", + " value: 5", + " >", + " ip_next_header_counts <", + " key: 58", + " value: 2", + " >", + " l2_broadcast_count: 0", + " l2_multicast_count: 0", + " l2_unicast_count: 10", + " no_uid_wakeups: 2", + " non_application_wakeups: 1", + " root_wakeups: 2", + " system_wakeups: 3", + " total_wakeups: 10", + " >", + ">", + "version: 2\n"); + assertEquals(want, got); + } + + @Test + public void testDnsLogging() throws Exception { + asyncDump(100); + + dnsEvent(100, EVENT_GETADDRINFO, 0, 3456); + dnsEvent(100, EVENT_GETADDRINFO, 0, 267); + dnsEvent(100, EVENT_GETHOSTBYNAME, 22, 1230); + dnsEvent(100, EVENT_GETADDRINFO, 3, 45); + dnsEvent(100, EVENT_GETADDRINFO, 1, 2111); + dnsEvent(100, EVENT_GETADDRINFO, 0, 450); + dnsEvent(100, EVENT_GETHOSTBYNAME, 200, 638); + dnsEvent(100, EVENT_GETHOSTBYNAME, 178, 1300); + dnsEvent(101, EVENT_GETADDRINFO, 0, 56); + dnsEvent(101, EVENT_GETADDRINFO, 0, 78); + dnsEvent(101, EVENT_GETADDRINFO, 0, 14); + dnsEvent(101, EVENT_GETHOSTBYNAME, 0, 56); + dnsEvent(101, EVENT_GETADDRINFO, 0, 78); + dnsEvent(101, EVENT_GETADDRINFO, 0, 14); + + String got = flushStatistics(); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " event_types: 1", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " event_types: 2", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 3456", + " latencies_ms: 267", + " latencies_ms: 1230", + " latencies_ms: 45", + " latencies_ms: 2111", + " latencies_ms: 450", + " latencies_ms: 638", + " latencies_ms: 1300", + " return_codes: 0", + " return_codes: 0", + " return_codes: 22", + " return_codes: 3", + " return_codes: 1", + " return_codes: 0", + " return_codes: 200", + " return_codes: 178", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " dns_lookup_batch <", + " event_types: 1", + " event_types: 1", + " event_types: 1", + " event_types: 2", + " event_types: 1", + " event_types: 1", + " getaddrinfo_error_count: 0", + " getaddrinfo_query_count: 0", + " gethostbyname_error_count: 0", + " gethostbyname_query_count: 0", + " latencies_ms: 56", + " latencies_ms: 78", + " latencies_ms: 14", + " latencies_ms: 56", + " latencies_ms: 78", + " latencies_ms: 14", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " return_codes: 0", + " >", + ">", + "version: 2\n"); + assertEquals(want, got); + } + + @Test + public void testConnectLogging() throws Exception { + asyncDump(100); + + final int OK = 0; + Thread[] logActions = { + // ignored + connectEventAction(100, OsConstants.EALREADY, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.EALREADY, 0, EXAMPLE_IPV6), + connectEventAction(100, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6), + connectEventAction(101, OsConstants.EINPROGRESS, 0, EXAMPLE_IPV6), + // valid latencies + connectEventAction(100, OK, 110, EXAMPLE_IPV4), + connectEventAction(100, OK, 23, EXAMPLE_IPV4), + connectEventAction(100, OK, 45, EXAMPLE_IPV4), + connectEventAction(101, OK, 56, EXAMPLE_IPV4), + connectEventAction(101, OK, 523, EXAMPLE_IPV6), + connectEventAction(101, OK, 214, EXAMPLE_IPV6), + connectEventAction(101, OK, 67, EXAMPLE_IPV6), + // errors + connectEventAction(100, OsConstants.EPERM, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EPERM, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.EAGAIN, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.EACCES, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EACCES, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.EACCES, 0, EXAMPLE_IPV6), + connectEventAction(100, OsConstants.EADDRINUSE, 0, EXAMPLE_IPV4), + connectEventAction(101, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV4), + connectEventAction(100, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6), + connectEventAction(100, OsConstants.ETIMEDOUT, 0, EXAMPLE_IPV6), + connectEventAction(101, OsConstants.ECONNREFUSED, 0, EXAMPLE_IPV4), + }; + + for (Thread t : logActions) { + t.start(); + } + for (Thread t : logActions) { + t.join(); + } + + String got = flushStatistics(); + String want = String.join("\n", + "dropped_events: 0", + "events <", + " if_name: \"\"", + " link_layer: 4", + " network_id: 100", + " time_ms: 0", + " transports: 2", + " connect_statistics <", + " connect_blocking_count: 3", + " connect_count: 6", + " errnos_counters <", + " key: 1", + " value: 1", + " >", + " errnos_counters <", + " key: 11", + " value: 1", + " >", + " errnos_counters <", + " key: 13", + " value: 1", + " >", + " errnos_counters <", + " key: 98", + " value: 1", + " >", + " errnos_counters <", + " key: 110", + " value: 2", + " >", + " ipv6_addr_count: 1", + " latencies_ms: 23", + " latencies_ms: 45", + " latencies_ms: 110", + " >", + ">", + "events <", + " if_name: \"\"", + " link_layer: 2", + " network_id: 101", + " time_ms: 0", + " transports: 1", + " connect_statistics <", + " connect_blocking_count: 4", + " connect_count: 6", + " errnos_counters <", + " key: 1", + " value: 1", + " >", + " errnos_counters <", + " key: 13", + " value: 2", + " >", + " errnos_counters <", + " key: 110", + " value: 1", + " >", + " errnos_counters <", + " key: 111", + " value: 1", + " >", + " ipv6_addr_count: 5", + " latencies_ms: 56", + " latencies_ms: 67", + " latencies_ms: 214", + " latencies_ms: 523", + " >", + ">", + "version: 2\n"); + assertEquals(want, got); + } + + private void setCapabilities(int netId) { + final ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback = + ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); + verify(mCm).registerNetworkCallback(any(), networkCallback.capture()); + networkCallback.getValue().onCapabilitiesChanged(new Network(netId), + netId == 100 ? CAPABILITIES_WIFI : CAPABILITIES_CELL); + } + + Thread connectEventAction(int netId, int error, int latencyMs, String ipAddr) { + setCapabilities(netId); + return new Thread(() -> { + try { + mService.onConnectEvent(netId, error, latencyMs, ipAddr, 80, 1); + } catch (Exception e) { + fail(e.toString()); + } + }); + } + + void dnsEvent(int netId, int type, int result, int latency) throws Exception { + setCapabilities(netId); + mService.onDnsEvent(netId, type, result, latency, "", null, 0, 0); + } + + void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp, + String dstIp, int sport, int dport, long now) throws Exception { + String prefix = NetdEventListenerService.WAKEUP_EVENT_IFACE_PREFIX + iface; + mService.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now); + } + + void asyncDump(long durationMs) throws Exception { + final long stop = System.currentTimeMillis() + durationMs; + final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null")); + new Thread(() -> { + while (System.currentTimeMillis() < stop) { + mService.list(pw); + } + }).start(); + } + + // TODO: instead of comparing textpb to textpb, parse textpb and compare proto to proto. + String flushStatistics() throws Exception { + IpConnectivityMetrics metricsService = + new IpConnectivityMetrics(mock(Context.class), (ctx) -> 2000); + metricsService.mNetdListener = mService; + + StringWriter buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + metricsService.impl.dump(null, writer, new String[]{"flush"}); + byte[] bytes = Base64.decode(buffer.toString(), Base64.DEFAULT); + IpConnectivityLog log = IpConnectivityLog.parseFrom(bytes); + for (IpConnectivityEvent ev : log.events) { + if (ev.getConnectStatistics() == null) { + continue; + } + // Sort repeated fields of connect() events arriving in non-deterministic order. + Arrays.sort(ev.getConnectStatistics().latenciesMs); + Arrays.sort(ev.getConnectStatistics().errnosCounters, + Comparator.comparingInt((p) -> p.key)); + } + return log.toString(); + } + + String[] listNetdEvent() throws Exception { + StringWriter buffer = new StringWriter(); + PrintWriter writer = new PrintWriter(buffer); + mService.list(writer); + return buffer.toString().split("\\n"); + } + + static <T> T[] remove(T[] array, T[] filtered) { + List<T> c = Arrays.asList(filtered); + int next = 0; + for (int i = 0; i < array.length; i++) { + if (c.contains(array[i])) { + continue; + } + array[next++] = array[i]; + } + return Arrays.copyOf(array, next); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c353cea266bb03b2975e9fb032fcd3df36b76acf --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java @@ -0,0 +1,333 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.app.Notification.FLAG_ONGOING_EVENT; + +import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.LOST_INTERNET; +import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NETWORK_SWITCH; +import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NO_INTERNET; +import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PARTIAL_CONNECTIVITY; +import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PRIVATE_DNS_BROKEN; +import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.ConnectivityResources; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.UserHandle; +import android.telephony.TelephonyManager; +import android.util.DisplayMetrics; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.server.connectivity.NetworkNotificationManager.NotificationType; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkNotificationManagerTest { + + private static final String TEST_SSID = "Test SSID"; + private static final String TEST_EXTRA_INFO = "extra"; + static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities(); + static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities(); + static final NetworkCapabilities VPN_CAPABILITIES = new NetworkCapabilities(); + static { + CELL_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + CELL_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + WIFI_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + WIFI_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + WIFI_CAPABILITIES.setSSID(TEST_SSID); + + // Set the underyling network to wifi. + VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + VPN_CAPABILITIES.addTransportType(NetworkCapabilities.TRANSPORT_VPN); + VPN_CAPABILITIES.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + VPN_CAPABILITIES.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN); + } + + @Mock Context mCtx; + @Mock Resources mResources; + @Mock DisplayMetrics mDisplayMetrics; + @Mock PackageManager mPm; + @Mock TelephonyManager mTelephonyManager; + @Mock NotificationManager mNotificationManager; + @Mock NetworkAgentInfo mWifiNai; + @Mock NetworkAgentInfo mCellNai; + @Mock NetworkAgentInfo mVpnNai; + @Mock NetworkInfo mNetworkInfo; + ArgumentCaptor<Notification> mCaptor; + + NetworkNotificationManager mManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mCaptor = ArgumentCaptor.forClass(Notification.class); + mWifiNai.networkCapabilities = WIFI_CAPABILITIES; + mWifiNai.networkInfo = mNetworkInfo; + mCellNai.networkCapabilities = CELL_CAPABILITIES; + mCellNai.networkInfo = mNetworkInfo; + mVpnNai.networkCapabilities = VPN_CAPABILITIES; + mVpnNai.networkInfo = mNetworkInfo; + mDisplayMetrics.density = 2.275f; + doReturn(true).when(mVpnNai).isVPN(); + when(mCtx.getResources()).thenReturn(mResources); + when(mCtx.getPackageManager()).thenReturn(mPm); + when(mCtx.getApplicationInfo()).thenReturn(new ApplicationInfo()); + final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mCtx)); + doReturn(UserHandle.ALL).when(asUserCtx).getUser(); + when(mCtx.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx); + when(mCtx.getSystemService(eq(Context.NOTIFICATION_SERVICE))) + .thenReturn(mNotificationManager); + when(mNetworkInfo.getExtraInfo()).thenReturn(TEST_EXTRA_INFO); + ConnectivityResources.setResourcesContextForTest(mCtx); + when(mResources.getColor(anyInt(), any())).thenReturn(0xFF607D8B); + when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics); + + // Come up with some credible-looking transport names. The actual values do not matter. + String[] transportNames = new String[NetworkCapabilities.MAX_TRANSPORT + 1]; + for (int transport = 0; transport <= NetworkCapabilities.MAX_TRANSPORT; transport++) { + transportNames[transport] = NetworkCapabilities.transportNameOf(transport); + } + when(mResources.getStringArray(R.array.network_switch_type_name)) + .thenReturn(transportNames); + + mManager = new NetworkNotificationManager(mCtx, mTelephonyManager); + } + + @After + public void tearDown() { + ConnectivityResources.setResourcesContextForTest(null); + } + + private void verifyTitleByNetwork(final int id, final NetworkAgentInfo nai, final int title) { + final String tag = NetworkNotificationManager.tagFor(id); + mManager.showNotification(id, PRIVATE_DNS_BROKEN, nai, null, null, true); + verify(mNotificationManager, times(1)) + .notify(eq(tag), eq(PRIVATE_DNS_BROKEN.eventId), any()); + final int transportType = NetworkNotificationManager.approximateTransportType(nai); + if (transportType == NetworkCapabilities.TRANSPORT_WIFI) { + verify(mResources, times(1)).getString(eq(title), eq(TEST_EXTRA_INFO)); + } else { + verify(mResources, times(1)).getString(title); + } + verify(mResources, times(1)).getString(eq(R.string.private_dns_broken_detailed)); + } + + @Test + public void testTitleOfPrivateDnsBroken() { + // Test the title of mobile data. + verifyTitleByNetwork(100, mCellNai, R.string.mobile_no_internet); + clearInvocations(mResources); + + // Test the title of wifi. + verifyTitleByNetwork(101, mWifiNai, R.string.wifi_no_internet); + clearInvocations(mResources); + + // Test the title of other networks. + verifyTitleByNetwork(102, mVpnNai, R.string.other_networks_no_internet); + clearInvocations(mResources); + } + + @Test + public void testNotificationsShownAndCleared() { + final int NETWORK_ID_BASE = 100; + List<NotificationType> types = Arrays.asList(NotificationType.values()); + List<Integer> ids = new ArrayList<>(types.size()); + for (int i = 0; i < types.size(); i++) { + ids.add(NETWORK_ID_BASE + i); + } + Collections.shuffle(ids); + Collections.shuffle(types); + + for (int i = 0; i < ids.size(); i++) { + mManager.showNotification(ids.get(i), types.get(i), mWifiNai, mCellNai, null, false); + } + + List<Integer> idsToClear = new ArrayList<>(ids); + Collections.shuffle(idsToClear); + for (int i = 0; i < ids.size(); i++) { + mManager.clearNotification(idsToClear.get(i)); + } + + for (int i = 0; i < ids.size(); i++) { + final int id = ids.get(i); + final int eventId = types.get(i).eventId; + final String tag = NetworkNotificationManager.tagFor(id); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any()); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(eventId)); + } + } + + @Test + @Ignore + // Ignored because the code under test calls Log.wtf, which crashes the tests on eng builds. + // TODO: re-enable after fixing this (e.g., turn Log.wtf into exceptions that this test catches) + public void testNoInternetNotificationsNotShownForCellular() { + mManager.showNotification(100, NO_INTERNET, mCellNai, mWifiNai, null, false); + mManager.showNotification(101, LOST_INTERNET, mCellNai, mWifiNai, null, false); + + verify(mNotificationManager, never()).notify(any(), anyInt(), any()); + + mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false); + + final int eventId = NO_INTERNET.eventId; + final String tag = NetworkNotificationManager.tagFor(102); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(eventId), any()); + } + + @Test + public void testNotificationsNotShownIfNoInternetCapability() { + mWifiNai.networkCapabilities = new NetworkCapabilities(); + mWifiNai.networkCapabilities .addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + mManager.showNotification(102, NO_INTERNET, mWifiNai, mCellNai, null, false); + mManager.showNotification(103, LOST_INTERNET, mWifiNai, mCellNai, null, false); + mManager.showNotification(104, NETWORK_SWITCH, mWifiNai, mCellNai, null, false); + + verify(mNotificationManager, never()).notify(any(), anyInt(), any()); + } + + private void assertNotification(NotificationType type, boolean ongoing) { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class); + mManager.showNotification(id, type, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(type.eventId), + noteCaptor.capture()); + + assertEquals("Notification ongoing flag should be " + (ongoing ? "set" : "unset"), + ongoing, (noteCaptor.getValue().flags & FLAG_ONGOING_EVENT) != 0); + } + + @Test + public void testDuplicatedNotificationsNoInternetThenSignIn() { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // Show first NO_INTERNET + assertNotification(NO_INTERNET, false /* ongoing */); + + // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET + assertNotification(SIGN_IN, false /* ongoing */); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId)); + + // Network disconnects + mManager.clearNotification(id); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId)); + } + + @Test + public void testOngoingSignInNotification() { + doReturn(true).when(mResources).getBoolean(R.bool.config_ongoingSignInNotification); + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // Show first NO_INTERNET + assertNotification(NO_INTERNET, false /* ongoing */); + + // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET + assertNotification(SIGN_IN, true /* ongoing */); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId)); + + // Network disconnects + mManager.clearNotification(id); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId)); + } + + @Test + public void testDuplicatedNotificationsSignInThenNoInternet() { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // Show first SIGN_IN + mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any()); + reset(mNotificationManager); + + // NO_INTERNET arrives after, but is ignored. + mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, never()).cancel(any(), anyInt()); + verify(mNotificationManager, never()).notify(any(), anyInt(), any()); + + // Network disconnects + mManager.clearNotification(id); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId)); + } + + @Test + public void testClearNotificationByType() { + final int id = 101; + final String tag = NetworkNotificationManager.tagFor(id); + + // clearNotification(int id, NotificationType notifyType) will check if given type is equal + // to previous type or not. If they are equal then clear the notification; if they are not + // equal then return. + mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any()); + + // Previous notification is NO_INTERNET and given type is NO_INTERNET too. The notification + // should be cleared. + mManager.clearNotification(id, NO_INTERNET); + verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId)); + + // SIGN_IN is popped-up. + mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false); + verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any()); + + // The notification type is not matching previous one, PARTIAL_CONNECTIVITY won't be + // cleared. + mManager.clearNotification(id, PARTIAL_CONNECTIVITY); + verify(mNotificationManager, never()).cancel(eq(tag), eq(PARTIAL_CONNECTIVITY.eventId)); + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..409f8c3099351e52cf156ee2fd65e7d269b241b5 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkOfferTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity + +import android.net.INetworkOfferCallback +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.net.NetworkScore.KEEP_CONNECTED_NONE +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +const val POLICY_NONE = 0L + +@RunWith(AndroidJUnit4::class) +@SmallTest +class NetworkOfferTest { + val mockCallback = mock(INetworkOfferCallback::class.java) + + @Test + fun testOfferNeededUnneeded() { + val score = FullScore(50, POLICY_NONE, KEEP_CONNECTED_NONE) + val offer = NetworkOffer(score, NetworkCapabilities.Builder().build(), mockCallback, + 1 /* providerId */) + val request1 = mock(NetworkRequest::class.java) + val request2 = mock(NetworkRequest::class.java) + offer.onNetworkNeeded(request1) + verify(mockCallback).onNetworkNeeded(eq(request1)) + assertTrue(offer.neededFor(request1)) + assertFalse(offer.neededFor(request2)) + + offer.onNetworkNeeded(request2) + verify(mockCallback).onNetworkNeeded(eq(request2)) + assertTrue(offer.neededFor(request1)) + assertTrue(offer.neededFor(request2)) + + // Note that the framework never calls onNetworkNeeded multiple times with the same + // request without calling onNetworkUnneeded first. It would be incorrect usage and the + // behavior would be undefined, so there is nothing to test. + + offer.onNetworkUnneeded(request1) + verify(mockCallback).onNetworkUnneeded(eq(request1)) + assertFalse(offer.neededFor(request1)) + assertTrue(offer.neededFor(request2)) + + offer.onNetworkUnneeded(request2) + verify(mockCallback).onNetworkUnneeded(eq(request2)) + assertFalse(offer.neededFor(request1)) + assertFalse(offer.neededFor(request2)) + } +} diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..4408958734404cd307e9d027afdfbc0c71a5f2ac --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity + +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkScore.KEEP_CONNECTED_NONE +import android.net.NetworkScore.POLICY_EXITING +import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY +import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD +import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +private fun score(vararg policies: Int) = FullScore(0, + policies.fold(0L) { acc, e -> acc or (1L shl e) }, KEEP_CONNECTED_NONE) +private fun caps(transport: Int) = NetworkCapabilities.Builder().addTransportType(transport).build() + +@SmallTest +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class NetworkRankerTest { + private val mRanker = NetworkRanker() + + private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities) + : NetworkRanker.Scoreable { + override fun getScore() = sc + override fun getCapsNoCopy(): NetworkCapabilities = nc + } + + @Test + fun testYieldToBadWiFiOneCell() { + // Only cell, it wins + val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + val scores = listOf(winner) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiOneCellOneBadWiFi() { + // Bad wifi wins against yielding validated cell + val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD), + caps(TRANSPORT_WIFI)) + val scores = listOf( + winner, + TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiOneCellTwoBadWiFi() { + // Bad wifi wins against yielding validated cell. Prefer the one that's primary. + val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD, + POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)) + val scores = listOf( + winner, + TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD), + caps(TRANSPORT_WIFI)), + TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiOneCellTwoBadWiFiOneNotAvoided() { + // Bad wifi ever validated wins against bad wifi that never was validated (or was + // avoided when bad). + val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD), + caps(TRANSPORT_WIFI)) + val scores = listOf( + winner, + TestScore(score(), caps(TRANSPORT_WIFI)), + TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiOneCellOneBadWiFiOneGoodWiFi() { + // Good wifi wins + val winner = TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD, + POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI)) + val scores = listOf( + winner, + TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD, + POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)), + TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiTwoCellsOneBadWiFi() { + // Cell that doesn't yield wins over cell that yields and bad wifi + val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR)) + val scores = listOf( + winner, + TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD, + POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)), + TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiTwoCellsOneBadWiFiOneGoodWiFi() { + // Good wifi wins over cell that doesn't yield and cell that yields + val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI)) + val scores = listOf( + winner, + TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD, + POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI)), + TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR)), + TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiOneExitingGoodWiFi() { + // Yielding cell wins over good exiting wifi + val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + val scores = listOf( + winner, + TestScore(score(POLICY_IS_VALIDATED, POLICY_EXITING), caps(TRANSPORT_WIFI)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } + + @Test + fun testYieldToBadWiFiOneExitingBadWiFi() { + // Yielding cell wins over bad exiting wifi + val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED), + caps(TRANSPORT_CELLULAR)) + val scores = listOf( + winner, + TestScore(score(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD, + POLICY_EXITING), caps(TRANSPORT_WIFI)) + ) + assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null)) + } +} diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c75618f43cdebcd10278345ae5117436c6403f9c --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -0,0 +1,1001 @@ +/* + * 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.server.connectivity; + +import static android.Manifest.permission.CHANGE_NETWORK_STATE; +import static android.Manifest.permission.CHANGE_WIFI_STATE; +import static android.Manifest.permission.CONNECTIVITY_INTERNAL; +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.INTERNET; +import static android.Manifest.permission.NETWORK_STACK; +import static android.Manifest.permission.UPDATE_DEVICE_STATS; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_OEM; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRODUCT; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_VENDOR; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.MATCH_ANY_USER; +import static android.net.ConnectivitySettingsManager.APPS_ALLOWED_ON_RESTRICTED_NETWORKS; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.os.Process.SYSTEM_UID; + +import static com.android.server.connectivity.PermissionMonitor.NETWORK; +import static com.android.server.connectivity.PermissionMonitor.SYSTEM; + +import static junit.framework.Assert.fail; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +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.when; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.net.INetd; +import android.net.UidRange; +import android.net.Uri; +import android.os.Build; +import android.os.SystemConfigManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArraySet; +import android.util.SparseIntArray; + +import androidx.test.InstrumentationRegistry; +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.AdditionalAnswers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PermissionMonitorTest { + private static final UserHandle MOCK_USER1 = UserHandle.of(0); + private static final UserHandle MOCK_USER2 = UserHandle.of(1); + private static final int MOCK_UID1 = 10001; + private static final int MOCK_UID2 = 10086; + private static final int SYSTEM_UID1 = 1000; + private static final int SYSTEM_UID2 = 1008; + private static final int VPN_UID = 10002; + private static final String REAL_SYSTEM_PACKAGE_NAME = "android"; + private static final String MOCK_PACKAGE1 = "appName1"; + private static final String MOCK_PACKAGE2 = "appName2"; + private static final String SYSTEM_PACKAGE1 = "sysName1"; + private static final String SYSTEM_PACKAGE2 = "sysName2"; + private static final String PARTITION_SYSTEM = "system"; + private static final String PARTITION_OEM = "oem"; + private static final String PARTITION_PRODUCT = "product"; + private static final String PARTITION_VENDOR = "vendor"; + private static final int VERSION_P = Build.VERSION_CODES.P; + private static final int VERSION_Q = Build.VERSION_CODES.Q; + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + @Mock private INetd mNetdService; + @Mock private UserManager mUserManager; + @Mock private PermissionMonitor.Dependencies mDeps; + @Mock private SystemConfigManager mSystemConfigManager; + + private PermissionMonitor mPermissionMonitor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager); + when(mUserManager.getUserHandles(eq(true))).thenReturn( + Arrays.asList(new UserHandle[] { MOCK_USER1, MOCK_USER2 })); + when(mContext.getSystemServiceName(SystemConfigManager.class)) + .thenReturn(Context.SYSTEM_CONFIG_SERVICE); + when(mContext.getSystemService(Context.SYSTEM_CONFIG_SERVICE)) + .thenReturn(mSystemConfigManager); + when(mSystemConfigManager.getSystemPermissionUids(anyString())).thenReturn(new int[0]); + final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mContext)); + doReturn(UserHandle.ALL).when(asUserCtx).getUser(); + when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx); + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); + + mPermissionMonitor = spy(new PermissionMonitor(mContext, mNetdService, mDeps)); + + when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(/* empty app list */ null); + mPermissionMonitor.startMonitoring(); + } + + private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion, int uid, + String... permissions) { + return hasRestrictedNetworkPermission( + partition, targetSdkVersion, "" /* packageName */, uid, permissions); + } + + private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion, + String packageName, int uid, String... permissions) { + final PackageInfo packageInfo = + packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, permissions, partition); + packageInfo.packageName = packageName; + packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion; + packageInfo.applicationInfo.uid = uid; + return mPermissionMonitor.hasRestrictedNetworkPermission(packageInfo); + } + + private static PackageInfo systemPackageInfoWithPermissions(String... permissions) { + return packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM); + } + + private static PackageInfo vendorPackageInfoWithPermissions(String... permissions) { + return packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_VENDOR); + } + + private static PackageInfo packageInfoWithPermissions(int permissionsFlags, + String[] permissions, String partition) { + int[] requestedPermissionsFlags = new int[permissions.length]; + for (int i = 0; i < permissions.length; i++) { + requestedPermissionsFlags[i] = permissionsFlags; + } + final PackageInfo packageInfo = new PackageInfo(); + packageInfo.requestedPermissions = permissions; + packageInfo.applicationInfo = new ApplicationInfo(); + packageInfo.requestedPermissionsFlags = requestedPermissionsFlags; + int privateFlags = 0; + switch (partition) { + case PARTITION_OEM: + privateFlags = PRIVATE_FLAG_OEM; + break; + case PARTITION_PRODUCT: + privateFlags = PRIVATE_FLAG_PRODUCT; + break; + case PARTITION_VENDOR: + privateFlags = PRIVATE_FLAG_VENDOR; + break; + } + packageInfo.applicationInfo.privateFlags = privateFlags; + return packageInfo; + } + + private static PackageInfo buildPackageInfo(boolean hasSystemPermission, int uid, + UserHandle user) { + final PackageInfo pkgInfo; + if (hasSystemPermission) { + pkgInfo = systemPackageInfoWithPermissions( + CHANGE_NETWORK_STATE, NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS); + } else { + pkgInfo = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, new String[] {}, ""); + } + pkgInfo.applicationInfo.uid = user.getUid(UserHandle.getAppId(uid)); + return pkgInfo; + } + + @Test + public void testHasPermission() { + PackageInfo app = systemPackageInfoWithPermissions(); + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE, NETWORK_STACK); + assertTrue(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertTrue(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = systemPackageInfoWithPermissions( + CONNECTIVITY_USE_RESTRICTED_NETWORKS, CONNECTIVITY_INTERNAL); + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertTrue(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertTrue(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = packageInfoWithPermissions(REQUESTED_PERMISSION_REQUIRED, new String[] { + CONNECTIVITY_USE_RESTRICTED_NETWORKS, CONNECTIVITY_INTERNAL, NETWORK_STACK }, + PARTITION_SYSTEM); + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + assertFalse(mPermissionMonitor.hasPermission(app, NETWORK_STACK)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(mPermissionMonitor.hasPermission(app, CONNECTIVITY_INTERNAL)); + + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + app.requestedPermissions = null; + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + app.requestedPermissionsFlags = null; + assertFalse(mPermissionMonitor.hasPermission(app, CHANGE_NETWORK_STATE)); + } + + @Test + public void testIsVendorApp() { + PackageInfo app = systemPackageInfoWithPermissions(); + assertFalse(mPermissionMonitor.isVendorApp(app.applicationInfo)); + app = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, + new String[] {}, PARTITION_OEM); + assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo)); + app = packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, + new String[] {}, PARTITION_PRODUCT); + assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo)); + app = vendorPackageInfoWithPermissions(); + assertTrue(mPermissionMonitor.isVendorApp(app.applicationInfo)); + } + + @Test + public void testHasNetworkPermission() { + PackageInfo app = systemPackageInfoWithPermissions(); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + assertTrue(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(NETWORK_STACK); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(CONNECTIVITY_USE_RESTRICTED_NETWORKS); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + app = systemPackageInfoWithPermissions(CONNECTIVITY_INTERNAL); + assertFalse(mPermissionMonitor.hasNetworkPermission(app)); + } + + @Test + public void testHasRestrictedNetworkPermission() { + assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_P, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_NETWORK_STATE)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, NETWORK_STACK)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, MOCK_UID1, PERMISSION_MAINLINE_NETWORK_STACK)); + + assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_Q, MOCK_UID1, CONNECTIVITY_INTERNAL)); + } + + @Test + public void testHasRestrictedNetworkPermissionSystemUid() { + doReturn(VERSION_P).when(mDeps).getDeviceFirstSdkInt(); + assertTrue(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, SYSTEM_UID, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_P, SYSTEM_UID, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + + doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt(); + assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + } + + @Test + public void testHasRestrictedNetworkPermissionVendorApp() { + assertTrue(hasRestrictedNetworkPermission(PARTITION_VENDOR, VERSION_P, MOCK_UID1)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CHANGE_NETWORK_STATE)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, NETWORK_STACK)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CONNECTIVITY_INTERNAL)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE)); + + assertFalse(hasRestrictedNetworkPermission(PARTITION_VENDOR, VERSION_Q, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CONNECTIVITY_INTERNAL)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CHANGE_NETWORK_STATE)); + } + + @Test + public void testHasRestrictedNetworkPermissionAppAllowedOnRestrictedNetworks() { + mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( + new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CHANGE_NETWORK_STATE)); + assertTrue(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CONNECTIVITY_INTERNAL)); + + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1, CHANGE_NETWORK_STATE)); + assertFalse(hasRestrictedNetworkPermission( + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1, CONNECTIVITY_INTERNAL)); + + } + + private boolean wouldBeCarryoverPackage(String partition, int targetSdkVersion, int uid) { + final PackageInfo packageInfo = packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, new String[] {}, partition); + packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion; + packageInfo.applicationInfo.uid = uid; + return mPermissionMonitor.isCarryoverPackage(packageInfo.applicationInfo); + } + + @Test + public void testIsCarryoverPackage() { + doReturn(VERSION_P).when(mDeps).getDeviceFirstSdkInt(); + assertTrue(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID)); + assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, MOCK_UID1)); + assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, MOCK_UID1)); + assertTrue(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID)); + assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1)); + assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, MOCK_UID1)); + + doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt(); + assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID)); + assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, MOCK_UID1)); + assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, MOCK_UID1)); + assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1)); + assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, MOCK_UID1)); + + assertFalse(wouldBeCarryoverPackage(PARTITION_OEM, VERSION_Q, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, SYSTEM_UID)); + assertFalse(wouldBeCarryoverPackage(PARTITION_OEM, VERSION_Q, MOCK_UID1)); + assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, MOCK_UID1)); + } + + private boolean wouldBeAppAllowedOnRestrictedNetworks(String packageName) { + final PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = packageName; + return mPermissionMonitor.isAppAllowedOnRestrictedNetworks(packageInfo); + } + + @Test + public void testIsAppAllowedOnRestrictedNetworks() { + mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(new ArraySet<>()); + assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); + assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + + mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( + new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + assertTrue(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); + assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + + mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( + new ArraySet<>(new String[] { MOCK_PACKAGE2 })); + assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); + assertTrue(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + + mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( + new ArraySet<>(new String[] { "com.android.test" })); + assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); + assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + } + + private void assertBackgroundPermission(boolean hasPermission, String name, int uid, + String... permissions) throws Exception { + when(mPackageManager.getPackageInfo(eq(name), anyInt())) + .thenReturn(packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM)); + mPermissionMonitor.onPackageAdded(name, uid); + assertEquals(hasPermission, mPermissionMonitor.hasUseBackgroundNetworksPermission(uid)); + } + + @Test + public void testHasUseBackgroundNetworksPermission() throws Exception { + assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(SYSTEM_UID)); + assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID); + assertBackgroundPermission(false, SYSTEM_PACKAGE1, SYSTEM_UID, CONNECTIVITY_INTERNAL); + assertBackgroundPermission(true, SYSTEM_PACKAGE1, SYSTEM_UID, CHANGE_NETWORK_STATE); + assertBackgroundPermission(true, SYSTEM_PACKAGE1, SYSTEM_UID, NETWORK_STACK); + + assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(MOCK_UID1)); + assertBackgroundPermission(false, MOCK_PACKAGE1, MOCK_UID1); + assertBackgroundPermission(true, MOCK_PACKAGE1, MOCK_UID1, + CONNECTIVITY_USE_RESTRICTED_NETWORKS); + + assertFalse(mPermissionMonitor.hasUseBackgroundNetworksPermission(MOCK_UID2)); + assertBackgroundPermission(false, MOCK_PACKAGE2, MOCK_UID2); + assertBackgroundPermission(false, MOCK_PACKAGE2, MOCK_UID2, + CONNECTIVITY_INTERNAL); + assertBackgroundPermission(true, MOCK_PACKAGE2, MOCK_UID2, NETWORK_STACK); + } + + private class NetdMonitor { + private final HashMap<Integer, Boolean> mApps = new HashMap<>(); + + NetdMonitor(INetd mockNetd) throws Exception { + // Add hook to verify and track result of setPermission. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + final Boolean isSystem = args[0].equals(INetd.PERMISSION_SYSTEM); + for (final int uid : (int[]) args[1]) { + // TODO: Currently, permission monitor will send duplicate commands for each uid + // corresponding to each user. Need to fix that and uncomment below test. + // if (mApps.containsKey(uid) && mApps.get(uid) == isSystem) { + // fail("uid " + uid + " is already set to " + isSystem); + // } + mApps.put(uid, isSystem); + } + return null; + }).when(mockNetd).networkSetPermissionForUser(anyInt(), any(int[].class)); + + // Add hook to verify and track result of clearPermission. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + for (final int uid : (int[]) args[0]) { + // TODO: Currently, permission monitor will send duplicate commands for each uid + // corresponding to each user. Need to fix that and uncomment below test. + // if (!mApps.containsKey(uid)) { + // fail("uid " + uid + " does not exist."); + // } + mApps.remove(uid); + } + return null; + }).when(mockNetd).networkClearPermissionForUser(any(int[].class)); + } + + public void expectPermission(Boolean permission, UserHandle[] users, int[] apps) { + for (final UserHandle user : users) { + for (final int app : apps) { + final int uid = user.getUid(app); + if (!mApps.containsKey(uid)) { + fail("uid " + uid + " does not exist."); + } + if (mApps.get(uid) != permission) { + fail("uid " + uid + " has wrong permission: " + permission); + } + } + } + } + + public void expectNoPermission(UserHandle[] users, int[] apps) { + for (final UserHandle user : users) { + for (final int app : apps) { + final int uid = user.getUid(app); + if (mApps.containsKey(uid)) { + fail("uid " + uid + " has listed permissions, expected none."); + } + } + } + } + } + + @Test + public void testUserAndPackageAddRemove() throws Exception { + final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService); + + // MOCK_UID1: MOCK_PACKAGE1 only has network permission. + // SYSTEM_UID: SYSTEM_PACKAGE1 has system permission. + // SYSTEM_UID: SYSTEM_PACKAGE2 only has network permission. + doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(eq(SYSTEM), anyString()); + doReturn(SYSTEM).when(mPermissionMonitor).highestPermissionForUid(any(), + eq(SYSTEM_PACKAGE1)); + doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(), + eq(SYSTEM_PACKAGE2)); + doReturn(NETWORK).when(mPermissionMonitor).highestPermissionForUid(any(), + eq(MOCK_PACKAGE1)); + + // Add SYSTEM_PACKAGE2, expect only have network permission. + mPermissionMonitor.onUserAdded(MOCK_USER1); + addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE2, SYSTEM_UID); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID}); + + // Add SYSTEM_PACKAGE1, expect permission escalate. + addPackageForUsers(new UserHandle[]{MOCK_USER1}, SYSTEM_PACKAGE1, SYSTEM_UID); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{SYSTEM_UID}); + + mPermissionMonitor.onUserAdded(MOCK_USER2); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID}); + + addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID}); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{MOCK_UID1}); + + // Remove MOCK_UID1, expect no permission left for all user. + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_UID1); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{MOCK_UID1}); + + // Remove SYSTEM_PACKAGE1, expect permission downgrade. + when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{SYSTEM_PACKAGE2}); + removePackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + SYSTEM_PACKAGE1, SYSTEM_UID); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID}); + + mPermissionMonitor.onUserRemoved(MOCK_USER1); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER2}, new int[]{SYSTEM_UID}); + + // Remove all packages, expect no permission left. + when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(new String[]{}); + removePackageForUsers(new UserHandle[]{MOCK_USER2}, SYSTEM_PACKAGE2, SYSTEM_UID); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID, MOCK_UID1}); + + // Remove last user, expect no redundant clearPermission is invoked. + mPermissionMonitor.onUserRemoved(MOCK_USER2); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1, MOCK_USER2}, + new int[]{SYSTEM_UID, MOCK_UID1}); + } + + @Test + public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception { + when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + Arrays.asList(new PackageInfo[] { + buildPackageInfo(true /* hasSystemPermission */, SYSTEM_UID1, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID2, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, VPN_UID, MOCK_USER1) + })); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), + eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1)); + mPermissionMonitor.startMonitoring(); + // Every app on user 0 except MOCK_UID2 are under VPN. + final Set<UidRange> vpnRange1 = new HashSet<>(Arrays.asList(new UidRange[] { + new UidRange(0, MOCK_UID2 - 1), + new UidRange(MOCK_UID2 + 1, UserHandle.PER_USER_RANGE - 1)})); + final Set<UidRange> vpnRange2 = Collections.singleton(new UidRange(MOCK_UID2, MOCK_UID2)); + + // When VPN is connected, expect a rule to be set up for user app MOCK_UID1 + mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID1})); + + reset(mNetdService); + + // When MOCK_UID1 package is uninstalled and reinstalled, expect Netd to be updated + mPermissionMonitor.onPackageRemoved( + MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1})); + mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID1})); + + reset(mNetdService); + + // During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the + // old UID rules then adds the new ones. Expect netd to be updated + mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1})); + mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID2})); + + reset(mNetdService); + + // When VPN is disconnected, expect rules to be torn down + mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID2})); + assertNull(mPermissionMonitor.getVpnUidRanges("tun0")); + } + + @Test + public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception { + when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + Arrays.asList(new PackageInfo[] { + buildPackageInfo(true /* hasSystemPermission */, SYSTEM_UID1, MOCK_USER1), + buildPackageInfo(false /* hasSystemPermission */, VPN_UID, MOCK_USER1) + })); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), + eq(GET_PERMISSIONS | MATCH_ANY_USER))).thenReturn( + buildPackageInfo(false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1)); + + mPermissionMonitor.startMonitoring(); + final Set<UidRange> vpnRange = Collections.singleton(UidRange.createForUser(MOCK_USER1)); + mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID); + + // Newly-installed package should have uid rules added + mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallAddUidInterfaceRules(eq("tun0"), + aryEq(new int[] {MOCK_UID1})); + + // Removed package should have its uid rules removed + mPermissionMonitor.onPackageRemoved( + MOCK_PACKAGE1, MOCK_USER1.getUid(MOCK_UID1)); + verify(mNetdService).firewallRemoveUidInterfaceRules(aryEq(new int[] {MOCK_UID1})); + } + + + // Normal package add/remove operations will trigger multiple intent for uids corresponding to + // each user. To simulate generic package operations, the onPackageAdded/Removed will need to be + // called multiple times with the uid corresponding to each user. + private void addPackageForUsers(UserHandle[] users, String packageName, int uid) { + for (final UserHandle user : users) { + mPermissionMonitor.onPackageAdded(packageName, user.getUid(uid)); + } + } + + private void removePackageForUsers(UserHandle[] users, String packageName, int uid) { + for (final UserHandle user : users) { + mPermissionMonitor.onPackageRemoved(packageName, user.getUid(uid)); + } + } + + private class NetdServiceMonitor { + private final HashMap<Integer, Integer> mPermissions = new HashMap<>(); + + NetdServiceMonitor(INetd mockNetdService) throws Exception { + // Add hook to verify and track result of setPermission. + doAnswer((InvocationOnMock invocation) -> { + final Object[] args = invocation.getArguments(); + final int permission = (int) args[0]; + for (final int uid : (int[]) args[1]) { + mPermissions.put(uid, permission); + } + return null; + }).when(mockNetdService).trafficSetNetPermForUids(anyInt(), any(int[].class)); + } + + public void expectPermission(int permission, int[] apps) { + for (final int app : apps) { + if (!mPermissions.containsKey(app)) { + fail("uid " + app + " does not exist."); + } + if (mPermissions.get(app) != permission) { + fail("uid " + app + " has wrong permission: " + mPermissions.get(app)); + } + } + } + } + + @Test + public void testPackagePermissionUpdate() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + // MOCK_UID1: MOCK_PACKAGE1 only has internet permission. + // MOCK_UID2: MOCK_PACKAGE2 does not have any permission. + // SYSTEM_UID1: SYSTEM_PACKAGE1 has internet permission and update device stats permission. + // SYSTEM_UID2: SYSTEM_PACKAGE2 has only update device stats permission. + + SparseIntArray netdPermissionsAppIds = new SparseIntArray(); + netdPermissionsAppIds.put(MOCK_UID1, INetd.PERMISSION_INTERNET); + netdPermissionsAppIds.put(MOCK_UID2, INetd.PERMISSION_NONE); + netdPermissionsAppIds.put(SYSTEM_UID1, INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS); + netdPermissionsAppIds.put(SYSTEM_UID2, INetd.PERMISSION_UPDATE_DEVICE_STATS); + + // Send the permission information to netd, expect permission updated. + mPermissionMonitor.sendPackagePermissionsToNetd(netdPermissionsAppIds); + + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, + new int[]{MOCK_UID1}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID2}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{SYSTEM_UID1}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UPDATE_DEVICE_STATS, + new int[]{SYSTEM_UID2}); + + // Update permission of MOCK_UID1, expect new permission show up. + mPermissionMonitor.sendPackagePermissionsForUid(MOCK_UID1, + INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + // Change permissions of SYSTEM_UID2, expect new permission show up and old permission + // revoked. + mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID2, + INetd.PERMISSION_INTERNET); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{SYSTEM_UID2}); + + // Revoke permission from SYSTEM_UID1, expect no permission stored. + mPermissionMonitor.sendPackagePermissionsForUid(SYSTEM_UID1, INetd.PERMISSION_NONE); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{SYSTEM_UID1}); + } + + private PackageInfo setPackagePermissions(String packageName, int uid, String[] permissions) + throws Exception { + PackageInfo packageInfo = packageInfoWithPermissions( + REQUESTED_PERMISSION_GRANTED, permissions, PARTITION_SYSTEM); + when(mPackageManager.getPackageInfo(eq(packageName), anyInt())).thenReturn(packageInfo); + when(mPackageManager.getPackagesForUid(eq(uid))).thenReturn(new String[]{packageName}); + return packageInfo; + } + + private PackageInfo addPackage(String packageName, int uid, String[] permissions) + throws Exception { + PackageInfo packageInfo = setPackagePermissions(packageName, uid, permissions); + mPermissionMonitor.onPackageAdded(packageName, uid); + return packageInfo; + } + + @Test + public void testPackageInstall() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + addPackage(MOCK_PACKAGE2, MOCK_UID2, new String[] {INTERNET}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID2}); + } + + @Test + public void testPackageInstallSharedUid() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + PackageInfo packageInfo1 = addPackage(MOCK_PACKAGE1, MOCK_UID1, + new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + // Install another package with the same uid and no permissions should not cause the UID to + // lose permissions. + PackageInfo packageInfo2 = systemPackageInfoWithPermissions(); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); + when(mPackageManager.getPackagesForUid(MOCK_UID1)) + .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2}); + mPermissionMonitor.onPackageAdded(MOCK_PACKAGE2, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageUninstallBasic() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{}); + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageRemoveThenAdd() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{}); + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[]{MOCK_UID1}); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageUpdate() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_NONE, new int[]{MOCK_UID1}); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1}); + } + + @Test + public void testPackageUninstallWithMultiplePackages() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + + addPackage(MOCK_PACKAGE1, MOCK_UID1, new String[] {INTERNET, UPDATE_DEVICE_STATS}); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[]{MOCK_UID1}); + + // Mock another package with the same uid but different permissions. + PackageInfo packageInfo2 = systemPackageInfoWithPermissions(INTERNET); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{ + MOCK_PACKAGE2}); + + mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID1); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{MOCK_UID1}); + } + + @Test + public void testRealSystemPermission() throws Exception { + // Use the real context as this test must ensure the *real* system package holds the + // necessary permission. + final Context realContext = InstrumentationRegistry.getContext(); + final PermissionMonitor monitor = new PermissionMonitor(realContext, mNetdService); + final PackageManager manager = realContext.getPackageManager(); + final PackageInfo systemInfo = manager.getPackageInfo(REAL_SYSTEM_PACKAGE_NAME, + GET_PERMISSIONS | MATCH_ANY_USER); + assertTrue(monitor.hasPermission(systemInfo, CONNECTIVITY_USE_RESTRICTED_NETWORKS)); + } + + @Test + public void testUpdateUidPermissionsFromSystemConfig() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(new ArrayList<>()); + when(mSystemConfigManager.getSystemPermissionUids(eq(INTERNET))) + .thenReturn(new int[]{ MOCK_UID1, MOCK_UID2 }); + when(mSystemConfigManager.getSystemPermissionUids(eq(UPDATE_DEVICE_STATS))) + .thenReturn(new int[]{ MOCK_UID2 }); + + mPermissionMonitor.startMonitoring(); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET, new int[]{ MOCK_UID1 }); + mNetdServiceMonitor.expectPermission( + INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS, + new int[]{ MOCK_UID2 }); + } + + @Test + public void testIntentReceiver() throws Exception { + final NetdServiceMonitor mNetdServiceMonitor = new NetdServiceMonitor(mNetdService); + final ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + verify(mContext, times(1)).registerReceiver(receiverCaptor.capture(), any(), any(), any()); + final BroadcastReceiver receiver = receiverCaptor.getValue(); + + // Verify receiving PACKAGE_ADDED intent. + final Intent addedIntent = new Intent(Intent.ACTION_PACKAGE_ADDED, + Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); + addedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1); + setPackagePermissions(MOCK_PACKAGE1, MOCK_UID1, + new String[] { INTERNET, UPDATE_DEVICE_STATS }); + receiver.onReceive(mContext, addedIntent); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_INTERNET + | INetd.PERMISSION_UPDATE_DEVICE_STATS, new int[] { MOCK_UID1 }); + + // Verify receiving PACKAGE_REMOVED intent. + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(null); + final Intent removedIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED, + Uri.fromParts("package", MOCK_PACKAGE1, null /* fragment */)); + removedIntent.putExtra(Intent.EXTRA_UID, MOCK_UID1); + receiver.onReceive(mContext, removedIntent); + mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[] { MOCK_UID1 }); + } + + @Test + public void testAppsAllowedOnRestrictedNetworksChanged() throws Exception { + final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService); + final ArgumentCaptor<ContentObserver> captor = + ArgumentCaptor.forClass(ContentObserver.class); + verify(mDeps, times(1)).registerContentObserver(any(), + argThat(uri -> uri.getEncodedPath().contains(APPS_ALLOWED_ON_RESTRICTED_NETWORKS)), + anyBoolean(), captor.capture()); + final ContentObserver contentObserver = captor.getValue(); + + mPermissionMonitor.onUserAdded(MOCK_USER1); + // Prepare PackageInfo for MOCK_PACKAGE1 + final PackageInfo packageInfo = buildPackageInfo( + false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1); + packageInfo.packageName = MOCK_PACKAGE1; + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), anyInt())).thenReturn(packageInfo); + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE1}); + // Prepare PackageInfo for MOCK_PACKAGE2 + final PackageInfo packageInfo2 = buildPackageInfo( + false /* hasSystemPermission */, MOCK_UID2, MOCK_USER1); + packageInfo2.packageName = MOCK_PACKAGE2; + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); + when(mPackageManager.getPackagesForUid(MOCK_UID2)).thenReturn(new String[]{MOCK_PACKAGE2}); + + // MOCK_PACKAGE1 is listed in setting that allow to use restricted networks, MOCK_UID1 + // should have SYSTEM permission. + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + contentObserver.onChange(true /* selfChange */); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2}); + + // MOCK_PACKAGE2 is listed in setting that allow to use restricted networks, MOCK_UID2 + // should have SYSTEM permission but MOCK_UID1 should revoke permission. + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new String[] { MOCK_PACKAGE2 })); + contentObserver.onChange(true /* selfChange */); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2}); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + + // No app lists in setting, should revoke permission from all uids. + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); + contentObserver.onChange(true /* selfChange */); + mNetdMonitor.expectNoPermission( + new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1, MOCK_UID2}); + } + + @Test + public void testAppsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception { + final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService); + final ArgumentCaptor<ContentObserver> captor = + ArgumentCaptor.forClass(ContentObserver.class); + verify(mDeps, times(1)).registerContentObserver(any(), + argThat(uri -> uri.getEncodedPath().contains(APPS_ALLOWED_ON_RESTRICTED_NETWORKS)), + anyBoolean(), captor.capture()); + final ContentObserver contentObserver = captor.getValue(); + + mPermissionMonitor.onUserAdded(MOCK_USER1); + // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2 with shared uid MOCK_UID1. + final PackageInfo packageInfo = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE); + packageInfo.applicationInfo.uid = MOCK_USER1.getUid(MOCK_UID1); + packageInfo.packageName = MOCK_PACKAGE1; + final PackageInfo packageInfo2 = buildPackageInfo( + false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1); + packageInfo2.packageName = MOCK_PACKAGE2; + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), anyInt())).thenReturn(packageInfo); + when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); + when(mPackageManager.getPackagesForUid(MOCK_UID1)) + .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2}); + + // MOCK_PACKAGE1 have CHANGE_NETWORK_STATE, MOCK_UID1 should have NETWORK permission. + addPackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + + // MOCK_PACKAGE2 is listed in setting that allow to use restricted networks, MOCK_UID1 + // should upgrade to SYSTEM permission. + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new String[] { MOCK_PACKAGE2 })); + contentObserver.onChange(true /* selfChange */); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + + // MOCK_PACKAGE1 is listed in setting that allow to use restricted networks, MOCK_UID1 + // should still have SYSTEM permission. + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + contentObserver.onChange(true /* selfChange */); + mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + + // No app lists in setting, MOCK_UID1 should downgrade to NETWORK permission. + when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); + contentObserver.onChange(true /* selfChange */); + mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + + // MOCK_PACKAGE1 removed, should revoke permission from MOCK_UID1. + when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE2}); + removePackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1); + mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); + } +} \ No newline at end of file diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b725b826b14f0f342ab53ead0b0bccfa68978f44 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java @@ -0,0 +1,1283 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static android.content.pm.UserInfo.FLAG_ADMIN; +import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE; +import static android.content.pm.UserInfo.FLAG_PRIMARY; +import static android.content.pm.UserInfo.FLAG_RESTRICTED; +import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.INetd.IF_STATE_DOWN; +import static android.net.INetd.IF_STATE_UP; +import static android.os.UserHandle.PER_USER_RANGE; + +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.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppOpsManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.INetd; +import android.net.Ikev2VpnProfile; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.IpSecManager; +import android.net.IpSecTunnelInterfaceResponse; +import android.net.LinkProperties; +import android.net.LocalSocket; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo.DetailedState; +import android.net.RouteInfo; +import android.net.UidRangeParcel; +import android.net.VpnManager; +import android.net.VpnService; +import android.net.VpnTransportInfo; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.INetworkManagementService; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.test.TestLooper; +import android.provider.Settings; +import android.security.Credentials; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Range; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; +import com.android.internal.net.LegacyVpnInfo; +import com.android.internal.net.VpnConfig; +import com.android.internal.net.VpnProfile; +import com.android.server.IpSecService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +/** + * Tests for {@link Vpn}. + * + * Build, install and run with: + * runtest frameworks-net -c com.android.server.connectivity.VpnTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VpnTest { + private static final String TAG = "VpnTest"; + + // Mock users + static final UserInfo primaryUser = new UserInfo(27, "Primary", FLAG_ADMIN | FLAG_PRIMARY); + static final UserInfo secondaryUser = new UserInfo(15, "Secondary", FLAG_ADMIN); + static final UserInfo restrictedProfileA = new UserInfo(40, "RestrictedA", FLAG_RESTRICTED); + static final UserInfo restrictedProfileB = new UserInfo(42, "RestrictedB", FLAG_RESTRICTED); + static final UserInfo managedProfileA = new UserInfo(45, "ManagedA", FLAG_MANAGED_PROFILE); + static { + restrictedProfileA.restrictedProfileParentId = primaryUser.id; + restrictedProfileB.restrictedProfileParentId = secondaryUser.id; + managedProfileA.profileGroupId = primaryUser.id; + } + + static final Network EGRESS_NETWORK = new Network(101); + static final String EGRESS_IFACE = "wlan0"; + static final String TEST_VPN_PKG = "com.testvpn.vpn"; + private static final String TEST_VPN_SERVER = "1.2.3.4"; + private static final String TEST_VPN_IDENTITY = "identity"; + private static final byte[] TEST_VPN_PSK = "psk".getBytes(); + + private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE); + private static final String TEST_IFACE_NAME = "TEST_IFACE"; + private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345; + private static final long TEST_TIMEOUT_MS = 500L; + + /** + * Names and UIDs for some fake packages. Important points: + * - UID is ordered increasing. + * - One pair of packages have consecutive UIDs. + */ + static final String[] PKGS = {"com.example", "org.example", "net.example", "web.vpn"}; + static final int[] PKG_UIDS = {66, 77, 78, 400}; + + // Mock packages + static final Map<String, Integer> mPackages = new ArrayMap<>(); + static { + for (int i = 0; i < PKGS.length; i++) { + mPackages.put(PKGS[i], PKG_UIDS[i]); + } + } + private static final Range<Integer> PRI_USER_RANGE = uidRangeForUser(primaryUser.id); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; + @Mock private UserManager mUserManager; + @Mock private PackageManager mPackageManager; + @Mock private INetworkManagementService mNetService; + @Mock private INetd mNetd; + @Mock private AppOpsManager mAppOps; + @Mock private NotificationManager mNotificationManager; + @Mock private Vpn.SystemServices mSystemServices; + @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; + @Mock private ConnectivityManager mConnectivityManager; + @Mock private IpSecService mIpSecService; + @Mock private VpnProfileStore mVpnProfileStore; + private final VpnProfile mVpnProfile; + + private IpSecManager mIpSecManager; + + public VpnTest() throws Exception { + // Build an actual VPN profile that is capable of being converted to and from an + // Ikev2VpnProfile + final Ikev2VpnProfile.Builder builder = + new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY); + builder.setAuthPsk(TEST_VPN_PSK); + mVpnProfile = builder.build().toVpnProfile(); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mIpSecManager = new IpSecManager(mContext, mIpSecService); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + setMockedPackages(mPackages); + + when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG); + when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG); + when(mContext.getSystemServiceName(UserManager.class)) + .thenReturn(Context.USER_SERVICE); + when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager); + when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps); + when(mContext.getSystemServiceName(NotificationManager.class)) + .thenReturn(Context.NOTIFICATION_SERVICE); + when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) + .thenReturn(mNotificationManager); + when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))) + .thenReturn(mConnectivityManager); + when(mContext.getSystemServiceName(eq(ConnectivityManager.class))) + .thenReturn(Context.CONNECTIVITY_SERVICE); + when(mContext.getSystemService(eq(Context.IPSEC_SERVICE))).thenReturn(mIpSecManager); + when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)) + .thenReturn(Resources.getSystem().getString( + R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) + .thenReturn(true); + + // Used by {@link Notification.Builder} + ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; + when(mContext.getApplicationInfo()).thenReturn(applicationInfo); + when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenReturn(applicationInfo); + + doNothing().when(mNetService).registerObserver(any()); + + // Deny all appops by default. + when(mAppOps.noteOpNoThrow(anyString(), anyInt(), anyString(), any(), any())) + .thenReturn(AppOpsManager.MODE_IGNORED); + + // Setup IpSecService + final IpSecTunnelInterfaceResponse tunnelResp = + new IpSecTunnelInterfaceResponse( + IpSecManager.Status.OK, TEST_TUNNEL_RESOURCE_ID, TEST_IFACE_NAME); + when(mIpSecService.createTunnelInterface(any(), any(), any(), any(), any())) + .thenReturn(tunnelResp); + } + + private Set<Range<Integer>> rangeSet(Range<Integer> ... ranges) { + final Set<Range<Integer>> range = new ArraySet<>(); + for (Range<Integer> r : ranges) range.add(r); + + return range; + } + + private static Range<Integer> uidRangeForUser(int userId) { + return new Range<Integer>(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1); + } + + private Range<Integer> uidRange(int start, int stop) { + return new Range<Integer>(start, stop); + } + + @Test + public void testRestrictedProfilesAreAddedToVpn() { + setMockedUsers(primaryUser, secondaryUser, restrictedProfileA, restrictedProfileB); + + final Vpn vpn = createVpn(primaryUser.id); + + // Assume the user can have restricted profiles. + doReturn(true).when(mUserManager).canHaveRestrictedProfile(); + final Set<Range<Integer>> ranges = + vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, null, null); + + assertEquals(rangeSet(PRI_USER_RANGE, uidRangeForUser(restrictedProfileA.id)), ranges); + } + + @Test + public void testManagedProfilesAreNotAddedToVpn() { + setMockedUsers(primaryUser, managedProfileA); + + final Vpn vpn = createVpn(primaryUser.id); + final Set<Range<Integer>> ranges = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, + null, null); + + assertEquals(rangeSet(PRI_USER_RANGE), ranges); + } + + @Test + public void testAddUserToVpnOnlyAddsOneUser() { + setMockedUsers(primaryUser, restrictedProfileA, managedProfileA); + + final Vpn vpn = createVpn(primaryUser.id); + final Set<Range<Integer>> ranges = new ArraySet<>(); + vpn.addUserToRanges(ranges, primaryUser.id, null, null); + + assertEquals(rangeSet(PRI_USER_RANGE), ranges); + } + + @Test + public void testUidAllowAndDenylist() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final Range<Integer> user = PRI_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + final String[] packages = {PKGS[0], PKGS[1], PKGS[2]}; + + // Allowed list + final Set<Range<Integer>> allow = vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, + Arrays.asList(packages), null /* disallowedApplications */); + assertEquals(rangeSet( + uidRange(userStart + PKG_UIDS[0], userStart + PKG_UIDS[0]), + uidRange(userStart + PKG_UIDS[1], userStart + PKG_UIDS[2])), + allow); + + // Denied list + final Set<Range<Integer>> disallow = + vpn.createUserAndRestrictedProfilesRanges(primaryUser.id, + null /* allowedApplications */, Arrays.asList(packages)); + assertEquals(rangeSet( + uidRange(userStart, userStart + PKG_UIDS[0] - 1), + uidRange(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + /* Empty range between UIDS[1] and UIDS[2], should be excluded, */ + uidRange(userStart + PKG_UIDS[2] + 1, userStop)), + disallow); + } + + @Test + public void testGetAlwaysAndOnGetLockDown() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + + // Default state. + assertFalse(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + + // Set always-on without lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, Collections.emptyList())); + assertTrue(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, Collections.emptyList())); + assertTrue(vpn.getAlwaysOn()); + assertTrue(vpn.getLockdown()); + + // Remove always-on configuration. + assertTrue(vpn.setAlwaysOnPackage(null, false, Collections.emptyList())); + assertFalse(vpn.getAlwaysOn()); + assertFalse(vpn.getLockdown()); + } + + @Test + public void testLockdownChangingPackage() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final Range<Integer> user = PRI_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + // Set always-on without lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], false, null)); + + // Set always-on with lockdown. + assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + + // Switch to another app. + assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop) + })); + } + + @Test + public void testLockdownAllowlist() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final Range<Integer> user = PRI_USER_RANGE; + final int userStart = user.getLower(); + final int userStop = user.getUpper(); + // Set always-on with lockdown and allow app PKGS[2] from lockdown. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[2]))); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop) + })); + // Change allowed app list to PKGS[3]. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[1], true, Collections.singletonList(PKGS[3]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop) + })); + + // Change the VPN app. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[3]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStart + PKG_UIDS[3] - 1) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart, userStart + PKG_UIDS[0] - 1), + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1) + })); + + // Remove the list of allowed packages. + assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null)); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[3] - 1), + new UidRangeParcel(userStart + PKG_UIDS[3] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop), + })); + + // Add the list of allowed packages. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList(PKGS[1]))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + + // Try allowing a package with a comma, should be rejected. + assertFalse(vpn.setAlwaysOnPackage( + PKGS[0], true, Collections.singletonList("a.b,c.d"))); + + // Pass a non-existent packages in the allowlist, they (and only they) should be ignored. + // allowed package should change from PGKS[1] to PKGS[2]. + assertTrue(vpn.setAlwaysOnPackage( + PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"))); + verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[1] - 1), + new UidRangeParcel(userStart + PKG_UIDS[1] + 1, userStop) + })); + verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] { + new UidRangeParcel(userStart + PKG_UIDS[0] + 1, userStart + PKG_UIDS[2] - 1), + new UidRangeParcel(userStart + PKG_UIDS[2] + 1, userStop) + })); + } + + @Test + public void testLockdownRuleRepeatability() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] { + new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper())}; + // Given legacy lockdown is already enabled, + vpn.setLockdown(true); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(true, + toRanges(primaryUserRangeParcel)); + + // Enabling legacy lockdown twice should do nothing. + vpn.setLockdown(true); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any()); + + // And disabling should remove the rules exactly once. + vpn.setLockdown(false); + verify(mConnectivityManager, times(1)).setRequireVpnForUids(false, + toRanges(primaryUserRangeParcel)); + + // Removing the lockdown again should have no effect. + vpn.setLockdown(false); + verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any()); + } + + private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) { + ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length); + for (int i = 0; i < ranges.length; i++) { + rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop)); + } + return rangesArray; + } + + @Test + public void testLockdownRuleReversibility() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + final UidRangeParcel[] entireUser = { + new UidRangeParcel(PRI_USER_RANGE.getLower(), PRI_USER_RANGE.getUpper()) + }; + final UidRangeParcel[] exceptPkg0 = { + new UidRangeParcel(entireUser[0].start, entireUser[0].start + PKG_UIDS[0] - 1), + new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop) + }; + + final InOrder order = inOrder(mConnectivityManager); + + // Given lockdown is enabled with no package (legacy VPN), + vpn.setLockdown(true); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser)); + + // When a new VPN package is set the rules should change to cover that package. + vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE); + order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser)); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0)); + + // When that VPN package is unset, everything should be undone again in reverse. + vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE); + order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0)); + order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser)); + } + + @Test + public void testIsAlwaysOnPackageSupported() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + + ApplicationInfo appInfo = new ApplicationInfo(); + when(mPackageManager.getApplicationInfoAsUser(eq(PKGS[0]), anyInt(), eq(primaryUser.id))) + .thenReturn(appInfo); + + ServiceInfo svcInfo = new ServiceInfo(); + ResolveInfo resInfo = new ResolveInfo(); + resInfo.serviceInfo = svcInfo; + when(mPackageManager.queryIntentServicesAsUser(any(), eq(PackageManager.GET_META_DATA), + eq(primaryUser.id))) + .thenReturn(Collections.singletonList(resInfo)); + + // null package name should return false + assertFalse(vpn.isAlwaysOnPackageSupported(null)); + + // Pre-N apps are not supported + appInfo.targetSdkVersion = VERSION_CODES.M; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + + // N+ apps are supported by default + appInfo.targetSdkVersion = VERSION_CODES.N; + assertTrue(vpn.isAlwaysOnPackageSupported(PKGS[0])); + + // Apps that opt out explicitly are not supported + appInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT; + Bundle metaData = new Bundle(); + metaData.putBoolean(VpnService.SERVICE_META_DATA_SUPPORTS_ALWAYS_ON, false); + svcInfo.metaData = metaData; + assertFalse(vpn.isAlwaysOnPackageSupported(PKGS[0])); + } + + @Test + public void testNotificationShownForAlwaysOnApp() throws Exception { + final UserHandle userHandle = UserHandle.of(primaryUser.id); + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + final InOrder order = inOrder(mNotificationManager); + + // Don't show a notification for regular disconnected states. + vpn.updateState(DetailedState.DISCONNECTED, TAG); + order.verify(mNotificationManager, atLeastOnce()).cancel(anyString(), anyInt()); + + // Start showing a notification for disconnected once always-on. + vpn.setAlwaysOnPackage(PKGS[0], false, null); + order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); + + // Stop showing the notification once connected. + vpn.updateState(DetailedState.CONNECTED, TAG); + order.verify(mNotificationManager).cancel(anyString(), anyInt()); + + // Show the notification if we disconnect again. + vpn.updateState(DetailedState.DISCONNECTED, TAG); + order.verify(mNotificationManager).notify(anyString(), anyInt(), any()); + + // Notification should be cleared after unsetting always-on package. + vpn.setAlwaysOnPackage(null, false, null); + order.verify(mNotificationManager).cancel(anyString(), anyInt()); + } + + /** + * The profile name should NOT change between releases for backwards compatibility + * + * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST + * be updated to ensure backward compatibility. + */ + @Test + public void testGetProfileNameForPackage() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG; + assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG)); + } + + private Vpn createVpnAndSetupUidChecks(String... grantedOps) throws Exception { + return createVpnAndSetupUidChecks(primaryUser, grantedOps); + } + + private Vpn createVpnAndSetupUidChecks(UserInfo user, String... grantedOps) throws Exception { + final Vpn vpn = createVpn(user.id); + setMockedUsers(user); + + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(Process.myUid()); + + for (final String opStr : grantedOps) { + when(mAppOps.noteOpNoThrow(opStr, Process.myUid(), TEST_VPN_PKG, + null /* attributionTag */, null /* message */)) + .thenReturn(AppOpsManager.MODE_ALLOWED); + } + + return vpn; + } + + private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, String... checkedOps) { + assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile)); + + // The profile should always be stored, whether or not consent has been previously granted. + verify(mVpnProfileStore) + .put( + eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), + eq(mVpnProfile.encode())); + + for (final String checkedOpStr : checkedOps) { + verify(mAppOps).noteOpNoThrow(checkedOpStr, Process.myUid(), TEST_VPN_PKG, + null /* attributionTag */, null /* message */); + } + } + + @Test + public void testProvisionVpnProfileNoIpsecTunnels() throws Exception { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) + .thenReturn(false); + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + fail("Expected exception due to missing feature"); + } catch (UnsupportedOperationException expected) { + } + } + + @Test + public void testProvisionVpnProfilePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + checkProvisionVpnProfile( + vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + } + + @Test + public void testProvisionVpnProfileNotPreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller + // had neither. + checkProvisionVpnProfile(vpn, false /* expectedResult */, + AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN, AppOpsManager.OPSTR_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN); + + checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OPSTR_ACTIVATE_VPN); + } + + @Test + public void testProvisionVpnProfileTooLarge() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + final VpnProfile bigProfile = new VpnProfile(""); + bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile); + fail("Expected IAE due to profile size"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testProvisionVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testDeleteVpnProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + vpn.deleteVpnProfile(TEST_VPN_PKG); + + verify(mVpnProfileStore) + .remove(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + @Test + public void testDeleteVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.deleteVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testGetVpnProfilePrivileged() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(new VpnProfile("").encode()); + + vpn.getVpnProfilePrivileged(TEST_VPN_PKG); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + } + + @Test + public void testStartVpnProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + @Test + public void testStartVpnProfileVpnServicePreconsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + vpn.startVpnProfile(TEST_VPN_PKG); + + // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown. + verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), + TEST_VPN_PKG, null /* attributionTag */, null /* message */); + } + + @Test + public void testStartVpnProfileNotConsented() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected failure due to no user consent"); + } catch (SecurityException expected) { + } + + // Verify both appops were checked. + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + verify(mAppOps).noteOpNoThrow(AppOpsManager.OPSTR_ACTIVATE_VPN, Process.myUid(), + TEST_VPN_PKG, null /* attributionTag */, null /* message */); + + // Keystore should never have been accessed. + verify(mVpnProfileStore, never()).get(any()); + } + + @Test + public void testStartVpnProfileMissingProfile() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected failure due to missing profile"); + } catch (IllegalArgumentException expected) { + } + + verify(mVpnProfileStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG)); + verify(mAppOps) + .noteOpNoThrow( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(null) /* attributionTag */, + eq(null) /* message */); + } + + @Test + public void testStartVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.startVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testStopVpnProfileRestrictedUser() throws Exception { + final Vpn vpn = + createVpnAndSetupUidChecks( + restrictedProfileA, AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN); + + try { + vpn.stopVpnProfile(TEST_VPN_PKG); + fail("Expected SecurityException due to restricted user"); + } catch (SecurityException expected) { + } + } + + @Test + public void testSetPackageAuthorizationVpnService() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_SERVICE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationPlatformVpn() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_PLATFORM)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + } + + @Test + public void testSetPackageAuthorizationRevokeAuthorization() throws Exception { + final Vpn vpn = createVpnAndSetupUidChecks(); + + assertTrue(vpn.setPackageAuthorization(TEST_VPN_PKG, VpnManager.TYPE_VPN_NONE)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + verify(mAppOps) + .setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), + eq(Process.myUid()), + eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_IGNORED)); + } + + private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception { + final ArgumentCaptor<NetworkCallback> networkCallbackCaptor = + ArgumentCaptor.forClass(NetworkCallback.class); + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)) + .requestNetwork(any(), networkCallbackCaptor.capture()); + + // onAvailable() will trigger onDefaultNetworkChanged(), so NetdUtils#setInterfaceUp will be + // invoked. Set the return value of INetd#interfaceGetCfg to prevent NullPointerException. + final InterfaceConfigurationParcel config = new InterfaceConfigurationParcel(); + config.flags = new String[] {IF_STATE_DOWN}; + when(mNetd.interfaceGetCfg(anyString())).thenReturn(config); + final NetworkCallback cb = networkCallbackCaptor.getValue(); + cb.onAvailable(TEST_NETWORK); + return cb; + } + + private void verifyInterfaceSetCfgWithFlags(String flag) throws Exception { + // Add a timeout for waiting for interfaceSetCfg to be called. + verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetCfg(argThat( + config -> Arrays.asList(config.flags).contains(flag))); + } + + @Test + public void testStartPlatformVpnAuthenticationFailed() throws Exception { + final ArgumentCaptor<IkeSessionCallback> captor = + ArgumentCaptor.forClass(IkeSessionCallback.class); + final IkeProtocolException exception = mock(IkeProtocolException.class); + when(exception.getErrorType()) + .thenReturn(IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED); + + final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), (mVpnProfile)); + final NetworkCallback cb = triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + // Wait for createIkeSession() to be called before proceeding in order to ensure consistent + // state + verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)) + .createIkeSession(any(), any(), any(), any(), captor.capture(), any()); + final IkeSessionCallback ikeCb = captor.getValue(); + ikeCb.onClosedExceptionally(exception); + + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb)); + assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state); + } + + @Test + public void testStartPlatformVpnIllegalArgumentExceptionInSetup() throws Exception { + when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any())) + .thenThrow(new IllegalArgumentException()); + final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), mVpnProfile); + final NetworkCallback cb = triggerOnAvailableAndGetCallback(); + + verifyInterfaceSetCfgWithFlags(IF_STATE_UP); + + // Wait for createIkeSession() to be called before proceeding in order to ensure consistent + // state + verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS)).unregisterNetworkCallback(eq(cb)); + assertEquals(LegacyVpnInfo.STATE_FAILED, vpn.getLegacyVpnInfo().state); + } + + private void setAndVerifyAlwaysOnPackage(Vpn vpn, int uid, boolean lockdownEnabled) { + assertTrue(vpn.setAlwaysOnPackage(TEST_VPN_PKG, lockdownEnabled, null)); + + verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG))); + verify(mAppOps).setMode( + eq(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN), eq(uid), eq(TEST_VPN_PKG), + eq(AppOpsManager.MODE_ALLOWED)); + + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_APP), eq(TEST_VPN_PKG), eq(primaryUser.id)); + verify(mSystemServices).settingsSecurePutIntForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN), eq(lockdownEnabled ? 1 : 0), + eq(primaryUser.id)); + verify(mSystemServices).settingsSecurePutStringForUser( + eq(Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST), eq(""), eq(primaryUser.id)); + } + + @Test + public void testSetAndStartAlwaysOnVpn() throws Exception { + final Vpn vpn = createVpn(primaryUser.id); + setMockedUsers(primaryUser); + + // UID checks must return a different UID; otherwise it'll be treated as already prepared. + final int uid = Process.myUid() + 1; + when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt())) + .thenReturn(uid); + when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))) + .thenReturn(mVpnProfile.encode()); + + setAndVerifyAlwaysOnPackage(vpn, uid, false); + assertTrue(vpn.startAlwaysOnVpn()); + + // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in + // a subsequent CL. + } + + private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception { + setMockedUsers(primaryUser); + + // Dummy egress interface + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(EGRESS_IFACE); + + final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), + InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE); + lp.addRoute(defaultRoute); + + vpn.startLegacyVpn(vpnProfile, EGRESS_NETWORK, lp); + return vpn; + } + + @Test + public void testStartPlatformVpn() throws Exception { + startLegacyVpn(createVpn(primaryUser.id), mVpnProfile); + // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in + // a subsequent patch. + } + + @Test + public void testStartRacoonNumericAddress() throws Exception { + startRacoon("1.2.3.4", "1.2.3.4"); + } + + @Test + public void testStartRacoonHostname() throws Exception { + startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve + } + + private void assertTransportInfoMatches(NetworkCapabilities nc, int type) { + assertNotNull(nc); + VpnTransportInfo ti = (VpnTransportInfo) nc.getTransportInfo(); + assertNotNull(ti); + assertEquals(type, ti.getType()); + } + + public void startRacoon(final String serverAddr, final String expectedAddr) + throws Exception { + final ConditionVariable legacyRunnerReady = new ConditionVariable(); + final VpnProfile profile = new VpnProfile("testProfile" /* key */); + profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK; + profile.name = "testProfileName"; + profile.username = "userName"; + profile.password = "thePassword"; + profile.server = serverAddr; + profile.ipsecIdentifier = "id"; + profile.ipsecSecret = "secret"; + profile.l2tpSecret = "l2tpsecret"; + + when(mConnectivityManager.getAllNetworks()) + .thenReturn(new Network[] { new Network(101) }); + + when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(), + any(), any(), anyInt())).thenAnswer(invocation -> { + // The runner has registered an agent and is now ready. + legacyRunnerReady.open(); + return new Network(102); + }); + final Vpn vpn = startLegacyVpn(createVpn(primaryUser.id), profile); + final TestDeps deps = (TestDeps) vpn.mDeps; + try { + // udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK + assertArrayEquals( + new String[] { EGRESS_IFACE, expectedAddr, "udppsk", + profile.ipsecIdentifier, profile.ipsecSecret, "1701" }, + deps.racoonArgs.get(10, TimeUnit.SECONDS)); + // literal values are hardcoded in Vpn.java for mtpd args + assertArrayEquals( + new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret, + "name", profile.username, "password", profile.password, + "linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns", + "idle", "1800", "mtu", "1270", "mru", "1270" }, + deps.mtpdArgs.get(10, TimeUnit.SECONDS)); + + // Now wait for the runner to be ready before testing for the route. + ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class); + ArgumentCaptor<NetworkCapabilities> ncCaptor = + ArgumentCaptor.forClass(NetworkCapabilities.class); + verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(), + lpCaptor.capture(), ncCaptor.capture(), any(), any(), anyInt()); + + // In this test the expected address is always v4 so /32. + // Note that the interface needs to be specified because RouteInfo objects stored in + // LinkProperties objects always acquire the LinkProperties' interface. + final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"), + null, EGRESS_IFACE, RouteInfo.RTN_THROW); + final List<RouteInfo> actualRoutes = lpCaptor.getValue().getRoutes(); + assertTrue("Expected throw route (" + expectedRoute + ") not found in " + actualRoutes, + actualRoutes.contains(expectedRoute)); + + assertTransportInfoMatches(ncCaptor.getValue(), VpnManager.TYPE_VPN_LEGACY); + } finally { + // Now interrupt the thread, unblock the runner and clean up. + vpn.mVpnRunner.exitVpnRunner(); + deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier + vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup + } + } + + private static final class TestDeps extends Vpn.Dependencies { + public final CompletableFuture<String[]> racoonArgs = new CompletableFuture(); + public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture(); + public final File mStateFile; + + private final HashMap<String, Boolean> mRunningServices = new HashMap<>(); + + TestDeps() { + try { + mStateFile = File.createTempFile("vpnTest", ".tmp"); + mStateFile.deleteOnExit(); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isCallerSystem() { + return true; + } + + @Override + public void startService(final String serviceName) { + mRunningServices.put(serviceName, true); + } + + @Override + public void stopService(final String serviceName) { + mRunningServices.put(serviceName, false); + } + + @Override + public boolean isServiceRunning(final String serviceName) { + return mRunningServices.getOrDefault(serviceName, false); + } + + @Override + public boolean isServiceStopped(final String serviceName) { + return !isServiceRunning(serviceName); + } + + @Override + public File getStateFile() { + return mStateFile; + } + + @Override + public PendingIntent getIntentForStatusPanel(Context context) { + return null; + } + + @Override + public void sendArgumentsToDaemon( + final String daemon, final LocalSocket socket, final String[] arguments, + final Vpn.RetryScheduler interruptChecker) throws IOException { + if ("racoon".equals(daemon)) { + racoonArgs.complete(arguments); + } else if ("mtpd".equals(daemon)) { + writeStateFile(arguments); + mtpdArgs.complete(arguments); + } else { + throw new UnsupportedOperationException("Unsupported daemon : " + daemon); + } + } + + private void writeStateFile(final String[] arguments) throws IOException { + mStateFile.delete(); + mStateFile.createNewFile(); + mStateFile.deleteOnExit(); + final BufferedWriter writer = new BufferedWriter( + new FileWriter(mStateFile, false /* append */)); + writer.write(EGRESS_IFACE); + writer.write("\n"); + // addresses + writer.write("10.0.0.1/24\n"); + // routes + writer.write("192.168.6.0/24\n"); + // dns servers + writer.write("192.168.6.1\n"); + // search domains + writer.write("vpn.searchdomains.com\n"); + // endpoint - intentionally empty + writer.write("\n"); + writer.flush(); + writer.close(); + } + + @Override + @NonNull + public InetAddress resolve(final String endpoint) { + try { + // If a numeric IP address, return it. + return InetAddress.parseNumericAddress(endpoint); + } catch (IllegalArgumentException e) { + // Otherwise, return some token IP to test for. + return InetAddress.parseNumericAddress("5.6.7.8"); + } + } + + @Override + public boolean isInterfacePresent(final Vpn vpn, final String iface) { + return true; + } + } + + /** + * Mock some methods of vpn object. + */ + private Vpn createVpn(@UserIdInt int userId) { + final Context asUserContext = mock(Context.class, AdditionalAnswers.delegatesTo(mContext)); + doReturn(UserHandle.of(userId)).when(asUserContext).getUser(); + when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt())) + .thenReturn(asUserContext); + final TestLooper testLooper = new TestLooper(); + final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService, + mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator); + verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat( + provider -> provider.getName().contains("VpnNetworkProvider") + )); + return vpn; + } + + /** + * Populate {@link #mUserManager} with a list of fake users. + */ + private void setMockedUsers(UserInfo... users) { + final Map<Integer, UserInfo> userMap = new ArrayMap<>(); + for (UserInfo user : users) { + userMap.put(user.id, user); + } + + /** + * @see UserManagerService#getUsers(boolean) + */ + doAnswer(invocation -> { + final ArrayList<UserInfo> result = new ArrayList<>(users.length); + for (UserInfo ui : users) { + if (ui.isEnabled() && !ui.partial) { + result.add(ui); + } + } + return result; + }).when(mUserManager).getAliveUsers(); + + doAnswer(invocation -> { + final int id = (int) invocation.getArguments()[0]; + return userMap.get(id); + }).when(mUserManager).getUserInfo(anyInt()); + } + + /** + * Populate {@link #mPackageManager} with a fake packageName-to-UID mapping. + */ + private void setMockedPackages(final Map<String, Integer> packages) { + try { + doAnswer(invocation -> { + final String appName = (String) invocation.getArguments()[0]; + final int userId = (int) invocation.getArguments()[1]; + Integer appId = packages.get(appName); + if (appId == null) throw new PackageManager.NameNotFoundException(appName); + return UserHandle.getUid(userId, appId); + }).when(mPackageManager).getPackageUidAsUser(anyString(), anyInt()); + } catch (Exception e) { + } + } + + private void setMockedNetworks(final Map<Network, NetworkCapabilities> networks) { + doAnswer(invocation -> { + final Network network = (Network) invocation.getArguments()[0]; + return networks.get(network); + }).when(mConnectivityManager).getNetworkCapabilities(any()); + } + + // Need multiple copies of this, but Java's Stream objects can't be reused or + // duplicated. + private Stream<String> publicIpV4Routes() { + return Stream.of( + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", + "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", + "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", + "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", + "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15", "192.172.0.0/14", + "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8", "194.0.0.0/7", + "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"); + } + + private Stream<String> publicIpV6Routes() { + return Stream.of( + "::/1", "8000::/2", "c000::/3", "e000::/4", "f000::/5", "f800::/6", + "fe00::/8", "2605:ef80:e:af1d::/64"); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8b730af76951f9b9e622990a36bada88930efd76 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsAccessTest.java @@ -0,0 +1,189 @@ +/* + * 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 com.android.server.net; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.Manifest.permission; +import android.app.AppOpsManager; +import android.app.admin.DevicePolicyManagerInternal; +import android.content.Context; +import android.content.pm.PackageManager; +import android.telephony.TelephonyManager; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsAccessTest { + private static final String TEST_PKG = "com.example.test"; + private static final int TEST_UID = 12345; + + @Mock private Context mContext; + @Mock private DevicePolicyManagerInternal mDpmi; + @Mock private TelephonyManager mTm; + @Mock private AppOpsManager mAppOps; + + // Hold the real service so we can restore it when tearing down the test. + private DevicePolicyManagerInternal mSystemDpmi; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mSystemDpmi = LocalServices.getService(DevicePolicyManagerInternal.class); + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService(DevicePolicyManagerInternal.class, mDpmi); + + when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTm); + when(mContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOps); + } + + @After + public void tearDown() throws Exception { + LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class); + LocalServices.addService(DevicePolicyManagerInternal.class, mSystemDpmi); + } + + @Test + public void testCheckAccessLevel_hasCarrierPrivileges() throws Exception { + setHasCarrierPrivileges(true); + setIsDeviceOwner(false); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICE, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_isDeviceOwner() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(true); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICE, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_isProfileOwner() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.USER, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_hasAppOpsBitAllowed() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_ALLOWED, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_hasAppOpsBitDefault_grantedPermission() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, true); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_hasReadHistoryPermission() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(true); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(true); + assertEquals(NetworkStatsAccess.Level.DEVICESUMMARY, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_deniedAppOpsBit() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_ERRORED, true); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEFAULT, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + @Test + public void testCheckAccessLevel_deniedAppOpsBit_deniedPermission() throws Exception { + setHasCarrierPrivileges(false); + setIsDeviceOwner(false); + setIsProfileOwner(false); + setHasAppOpsPermission(AppOpsManager.MODE_DEFAULT, false); + setHasReadHistoryPermission(false); + assertEquals(NetworkStatsAccess.Level.DEFAULT, + NetworkStatsAccess.checkAccessLevel(mContext, TEST_UID, TEST_PKG)); + } + + private void setHasCarrierPrivileges(boolean hasPrivileges) { + when(mTm.checkCarrierPrivilegesForPackageAnyPhone(TEST_PKG)).thenReturn( + hasPrivileges ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS + : TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS); + } + + private void setIsDeviceOwner(boolean isOwner) { + when(mDpmi.isActiveDeviceOwner(TEST_UID)).thenReturn(isOwner); + } + + private void setIsProfileOwner(boolean isOwner) { + when(mDpmi.isActiveProfileOwner(TEST_UID)).thenReturn(isOwner); + } + + private void setHasAppOpsPermission(int appOpsMode, boolean hasPermission) { + when(mAppOps.noteOp(AppOpsManager.OP_GET_USAGE_STATS, TEST_UID, TEST_PKG)) + .thenReturn(appOpsMode); + when(mContext.checkCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS)).thenReturn( + hasPermission ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED); + } + + private void setHasReadHistoryPermission(boolean hasPermission) { + when(mContext.checkCallingOrSelfPermission(permission.READ_NETWORK_USAGE_HISTORY)) + .thenReturn(hasPermission ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a058a466a4ff056edc9644b880baa939777ab1ae --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsBaseTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net; + +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; + +import static org.junit.Assert.assertEquals; + +import android.net.NetworkStats; +import android.net.UnderlyingNetworkInfo; + +import java.util.Arrays; + +/** Superclass with utilities for NetworkStats(Service|Factory)Test */ +abstract class NetworkStatsBaseTest { + static final String TEST_IFACE = "test0"; + static final String TEST_IFACE2 = "test1"; + static final String TUN_IFACE = "test_nss_tun0"; + static final String TUN_IFACE2 = "test_nss_tun1"; + + static final int UID_RED = 1001; + static final int UID_BLUE = 1002; + static final int UID_GREEN = 1003; + static final int UID_VPN = 1004; + + void assertValues(NetworkStats stats, String iface, int uid, long rxBytes, + long rxPackets, long txBytes, long txPackets) { + assertValues( + stats, iface, uid, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, + rxBytes, rxPackets, txBytes, txPackets, 0); + } + + void assertValues(NetworkStats stats, String iface, int uid, int set, int tag, + int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets, long operations) { + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final int[] sets; + if (set == SET_ALL) { + sets = new int[] {SET_ALL, SET_DEFAULT, SET_FOREGROUND}; + } else { + sets = new int[] {set}; + } + + final int[] roamings; + if (roaming == ROAMING_ALL) { + roamings = new int[] {ROAMING_ALL, ROAMING_YES, ROAMING_NO}; + } else { + roamings = new int[] {roaming}; + } + + final int[] meterings; + if (metered == METERED_ALL) { + meterings = new int[] {METERED_ALL, METERED_YES, METERED_NO}; + } else { + meterings = new int[] {metered}; + } + + final int[] defaultNetworks; + if (defaultNetwork == DEFAULT_NETWORK_ALL) { + defaultNetworks = + new int[] {DEFAULT_NETWORK_ALL, DEFAULT_NETWORK_YES, DEFAULT_NETWORK_NO}; + } else { + defaultNetworks = new int[] {defaultNetwork}; + } + + for (int s : sets) { + for (int r : roamings) { + for (int m : meterings) { + for (int d : defaultNetworks) { + final int i = stats.findIndex(iface, uid, s, tag, m, r, d); + if (i != -1) { + entry.add(stats.getValues(i, null)); + } + } + } + } + } + + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } + + static UnderlyingNetworkInfo createVpnInfo(String[] underlyingIfaces) { + return createVpnInfo(TUN_IFACE, underlyingIfaces); + } + + static UnderlyingNetworkInfo createVpnInfo(String vpnIface, String[] underlyingIfaces) { + return new UnderlyingNetworkInfo(UID_VPN, vpnIface, Arrays.asList(underlyingIfaces)); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..505ff9b6a34bf2b255dea67d675f7cb971982189 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsCollectionTest.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 com.android.server.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.NetworkIdentity.OEM_NONE; +import static android.net.NetworkStats.SET_ALL; +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.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.os.Process.myUid; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.internal.net.NetworkUtilsInternal.multiplySafeByRational; +import static com.android.testutils.MiscAsserts.assertThrows; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.NetworkIdentity; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.Process; +import android.os.UserHandle; +import android.telephony.SubscriptionPlan; +import android.telephony.TelephonyManager; +import android.text.format.DateUtils; +import android.util.RecurrenceRule; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.tests.net.R; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link NetworkStatsCollection}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsCollectionTest { + + private static final String TEST_FILE = "test.bin"; + private static final String TEST_IMSI = "310260000000000"; + + private static final long TIME_A = 1326088800000L; // UTC: Monday 9th January 2012 06:00:00 AM + private static final long TIME_B = 1326110400000L; // UTC: Monday 9th January 2012 12:00:00 PM + private static final long TIME_C = 1326132000000L; // UTC: Monday 9th January 2012 06:00:00 PM + + private static Clock sOriginalClock; + + @Before + public void setUp() throws Exception { + sOriginalClock = RecurrenceRule.sClock; + // ignore any device overlay while testing + NetworkTemplate.forceAllNetworkTypes(); + } + + @After + public void tearDown() throws Exception { + RecurrenceRule.sClock = sOriginalClock; + NetworkTemplate.resetForceAllNetworkTypes(); + } + + private void setClock(Instant instant) { + RecurrenceRule.sClock = Clock.fixed(instant, ZoneId.systemDefault()); + } + + @Test + public void testReadLegacyNetwork() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_v1, testFile); + + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyNetwork(testFile); + + // verify that history read correctly + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE); + + // now export into a unified format + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + collection.write(bos); + + // clear structure completely + collection.reset(); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE); + + // and read back into structure, verifying that totals are same + collection.read(new ByteArrayInputStream(bos.toByteArray())); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 636016770L, 709306L, 88038768L, 518836L, NetworkStatsAccess.Level.DEVICE); + } + + @Test + public void testReadLegacyUid() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_uid_v4, testFile); + + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyUid(testFile, false); + + // verify that history read correctly + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE); + + // now export into a unified format + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + collection.write(bos); + + // clear structure completely + collection.reset(); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 0L, 0L, 0L, 0L, NetworkStatsAccess.Level.DEVICE); + + // and read back into structure, verifying that totals are same + collection.read(new ByteArrayInputStream(bos.toByteArray())); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), + 637076152L, 711413L, 88343717L, 521022L, NetworkStatsAccess.Level.DEVICE); + } + + @Test + public void testReadLegacyUidTags() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_uid_v4, testFile); + + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyUid(testFile, true); + + // verify that history read correctly + assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI), + 77017831L, 100995L, 35436758L, 92344L); + + // now export into a unified format + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + collection.write(bos); + + // clear structure completely + collection.reset(); + assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI), + 0L, 0L, 0L, 0L); + + // and read back into structure, verifying that totals are same + collection.read(new ByteArrayInputStream(bos.toByteArray())); + assertSummaryTotalIncludingTags(collection, buildTemplateMobileAll(TEST_IMSI), + 77017831L, 100995L, 35436758L, 92344L); + } + + @Test + public void testStartEndAtomicBuckets() throws Exception { + final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS); + + // record empty data straddling between buckets + final NetworkStats.Entry entry = new NetworkStats.Entry(); + entry.rxBytes = 32; + collection.recordData(null, UID_ALL, SET_DEFAULT, TAG_NONE, 30 * MINUTE_IN_MILLIS, + 90 * MINUTE_IN_MILLIS, entry); + + // assert that we report boundary in atomic buckets + assertEquals(0, collection.getStartMillis()); + assertEquals(2 * HOUR_IN_MILLIS, collection.getEndMillis()); + } + + @Test + public void testAccessLevels() throws Exception { + final NetworkStatsCollection collection = new NetworkStatsCollection(HOUR_IN_MILLIS); + final NetworkStats.Entry entry = new NetworkStats.Entry(); + final NetworkIdentitySet identSet = new NetworkIdentitySet(); + identSet.add(new NetworkIdentity(TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, + TEST_IMSI, null, false, true, true, OEM_NONE)); + + int myUid = Process.myUid(); + int otherUidInSameUser = Process.myUid() + 1; + int uidInDifferentUser = Process.myUid() + UserHandle.PER_USER_RANGE; + + // Record one entry for the current UID. + entry.rxBytes = 32; + collection.recordData(identSet, myUid, SET_DEFAULT, TAG_NONE, 0, 60 * MINUTE_IN_MILLIS, + entry); + + // Record one entry for another UID in this user. + entry.rxBytes = 64; + collection.recordData(identSet, otherUidInSameUser, SET_DEFAULT, TAG_NONE, 0, + 60 * MINUTE_IN_MILLIS, entry); + + // Record one entry for the system UID. + entry.rxBytes = 128; + collection.recordData(identSet, Process.SYSTEM_UID, SET_DEFAULT, TAG_NONE, 0, + 60 * MINUTE_IN_MILLIS, entry); + + // Record one entry for a UID in a different user. + entry.rxBytes = 256; + collection.recordData(identSet, uidInDifferentUser, SET_DEFAULT, TAG_NONE, 0, + 60 * MINUTE_IN_MILLIS, entry); + + // Verify the set of relevant UIDs for each access level. + assertArrayEquals(new int[] { myUid }, + collection.getRelevantUids(NetworkStatsAccess.Level.DEFAULT)); + assertArrayEquals(new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser }, + collection.getRelevantUids(NetworkStatsAccess.Level.USER)); + assertArrayEquals( + new int[] { Process.SYSTEM_UID, myUid, otherUidInSameUser, uidInDifferentUser }, + collection.getRelevantUids(NetworkStatsAccess.Level.DEVICE)); + + // Verify security check in getHistory. + assertNotNull(collection.getHistory(buildTemplateMobileAll(TEST_IMSI), null, myUid, SET_DEFAULT, + TAG_NONE, 0, 0L, 0L, NetworkStatsAccess.Level.DEFAULT, myUid)); + try { + collection.getHistory(buildTemplateMobileAll(TEST_IMSI), null, otherUidInSameUser, + SET_DEFAULT, TAG_NONE, 0, 0L, 0L, NetworkStatsAccess.Level.DEFAULT, myUid); + fail("Should have thrown SecurityException for accessing different UID"); + } catch (SecurityException e) { + // expected + } + + // Verify appropriate aggregation in getSummary. + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32, 0, 0, 0, + NetworkStatsAccess.Level.DEFAULT); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128, 0, 0, 0, + NetworkStatsAccess.Level.USER); + assertSummaryTotal(collection, buildTemplateMobileAll(TEST_IMSI), 32 + 64 + 128 + 256, 0, 0, + 0, NetworkStatsAccess.Level.DEVICE); + } + + @Test + public void testAugmentPlan() throws Exception { + final File testFile = + new File(InstrumentationRegistry.getContext().getFilesDir(), TEST_FILE); + stageFile(R.raw.netstats_v1, testFile); + + final NetworkStatsCollection emptyCollection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + final NetworkStatsCollection collection = new NetworkStatsCollection(30 * MINUTE_IN_MILLIS); + collection.readLegacyNetwork(testFile); + + // We're in the future, but not that far off + setClock(Instant.parse("2012-06-01T00:00:00.00Z")); + + // Test a bunch of plans that should result in no augmentation + final List<SubscriptionPlan> plans = new ArrayList<>(); + + // No plan + plans.add(null); + // No usage anchor + plans.add(SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")).build()); + // Usage anchor far in past + plans.add(SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")) + .setDataUsage(1000L, TIME_A - DateUtils.YEAR_IN_MILLIS).build()); + // Usage anchor far in future + plans.add(SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2011-01-14T00:00:00.00Z")) + .setDataUsage(1000L, TIME_A + DateUtils.YEAR_IN_MILLIS).build()); + // Usage anchor near but outside cycle + plans.add(SubscriptionPlan.Builder + .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"), + ZonedDateTime.parse("2012-01-09T15:00:00.00Z")) + .setDataUsage(1000L, TIME_C).build()); + + for (SubscriptionPlan plan : plans) { + int i; + NetworkStatsHistory history; + + // Empty collection should be untouched + history = getHistory(emptyCollection, plan, TIME_A, TIME_C); + assertEquals(0L, history.getTotalBytes()); + + // Normal collection should be untouched + history = getHistory(collection, plan, TIME_A, TIME_C); i = 0; + assertEntry(100647, 197, 23649, 185, history.getValues(i++, null)); + assertEntry(100647, 196, 23648, 185, history.getValues(i++, null)); + assertEntry(18323, 76, 15032, 76, history.getValues(i++, null)); + assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); + assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); + assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); + assertEntry(10747, 50, 16839, 55, history.getValues(i++, null)); + assertEntry(10747, 49, 16837, 54, history.getValues(i++, null)); + assertEntry(89191, 151, 18021, 140, history.getValues(i++, null)); + assertEntry(89190, 150, 18020, 139, history.getValues(i++, null)); + assertEntry(3821, 23, 4525, 26, history.getValues(i++, null)); + assertEntry(3820, 21, 4524, 26, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); + assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); + assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); + assertEquals(history.size(), i); + + // Slice from middle should be untouched + history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, + TIME_B + HOUR_IN_MILLIS); i = 0; + assertEntry(3821, 23, 4525, 26, history.getValues(i++, null)); + assertEntry(3820, 21, 4524, 26, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEquals(history.size(), i); + } + + // Lower anchor in the middle of plan + { + int i; + NetworkStatsHistory history; + + final SubscriptionPlan plan = SubscriptionPlan.Builder + .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"), + ZonedDateTime.parse("2012-01-09T15:00:00.00Z")) + .setDataUsage(200000L, TIME_B).build(); + + // Empty collection should be augmented + history = getHistory(emptyCollection, plan, TIME_A, TIME_C); + assertEquals(200000L, history.getTotalBytes()); + + // Normal collection should be augmented + history = getHistory(collection, plan, TIME_A, TIME_C); i = 0; + assertEntry(100647, 197, 23649, 185, history.getValues(i++, null)); + assertEntry(100647, 196, 23648, 185, history.getValues(i++, null)); + assertEntry(18323, 76, 15032, 76, history.getValues(i++, null)); + assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); + assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); + assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); + // Cycle point; start data normalization + assertEntry(7507, 0, 11763, 0, history.getValues(i++, null)); + assertEntry(7507, 0, 11762, 0, history.getValues(i++, null)); + assertEntry(62309, 0, 12589, 0, history.getValues(i++, null)); + assertEntry(62309, 0, 12588, 0, history.getValues(i++, null)); + assertEntry(2669, 0, 3161, 0, history.getValues(i++, null)); + assertEntry(2668, 0, 3160, 0, history.getValues(i++, null)); + // Anchor point; end data normalization + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); + assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); + assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); + // Cycle point + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); + assertEquals(history.size(), i); + + // Slice from middle should be augmented + history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, + TIME_B + HOUR_IN_MILLIS); i = 0; + assertEntry(2669, 0, 3161, 0, history.getValues(i++, null)); + assertEntry(2668, 0, 3160, 0, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEquals(history.size(), i); + } + + // Higher anchor in the middle of plan + { + int i; + NetworkStatsHistory history; + + final SubscriptionPlan plan = SubscriptionPlan.Builder + .createNonrecurring(ZonedDateTime.parse("2012-01-09T09:00:00.00Z"), + ZonedDateTime.parse("2012-01-09T15:00:00.00Z")) + .setDataUsage(400000L, TIME_B + MINUTE_IN_MILLIS).build(); + + // Empty collection should be augmented + history = getHistory(emptyCollection, plan, TIME_A, TIME_C); + assertEquals(400000L, history.getTotalBytes()); + + // Normal collection should be augmented + history = getHistory(collection, plan, TIME_A, TIME_C); i = 0; + assertEntry(100647, 197, 23649, 185, history.getValues(i++, null)); + assertEntry(100647, 196, 23648, 185, history.getValues(i++, null)); + assertEntry(18323, 76, 15032, 76, history.getValues(i++, null)); + assertEntry(18322, 75, 15031, 75, history.getValues(i++, null)); + assertEntry(527798, 761, 78570, 652, history.getValues(i++, null)); + assertEntry(527797, 760, 78570, 651, history.getValues(i++, null)); + // Cycle point; start data normalization + assertEntry(15015, 0, 23527, 0, history.getValues(i++, null)); + assertEntry(15015, 0, 23524, 0, history.getValues(i++, null)); + assertEntry(124619, 0, 25179, 0, history.getValues(i++, null)); + assertEntry(124618, 0, 25177, 0, history.getValues(i++, null)); + assertEntry(5338, 0, 6322, 0, history.getValues(i++, null)); + assertEntry(5337, 0, 6320, 0, history.getValues(i++, null)); + // Anchor point; end data normalization + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEntry(8289, 36, 6864, 39, history.getValues(i++, null)); + assertEntry(8289, 34, 6862, 37, history.getValues(i++, null)); + assertEntry(113914, 174, 18364, 157, history.getValues(i++, null)); + assertEntry(113913, 173, 18364, 157, history.getValues(i++, null)); + // Cycle point + assertEntry(11378, 49, 9261, 50, history.getValues(i++, null)); + assertEntry(11377, 48, 9261, 48, history.getValues(i++, null)); + assertEntry(201766, 328, 41808, 291, history.getValues(i++, null)); + assertEntry(201764, 328, 41807, 290, history.getValues(i++, null)); + assertEntry(106106, 219, 39918, 202, history.getValues(i++, null)); + assertEntry(106105, 216, 39916, 200, history.getValues(i++, null)); + + // Slice from middle should be augmented + history = getHistory(collection, plan, TIME_B - HOUR_IN_MILLIS, + TIME_B + HOUR_IN_MILLIS); i = 0; + assertEntry(5338, 0, 6322, 0, history.getValues(i++, null)); + assertEntry(5337, 0, 6320, 0, history.getValues(i++, null)); + assertEntry(91686, 159, 18576, 146, history.getValues(i++, null)); + assertEntry(91685, 159, 18574, 146, history.getValues(i++, null)); + assertEquals(history.size(), i); + } + } + + @Test + public void testAugmentPlanGigantic() throws Exception { + // We're in the future, but not that far off + setClock(Instant.parse("2012-06-01T00:00:00.00Z")); + + // Create a simple history with a ton of measured usage + final NetworkStatsCollection large = new NetworkStatsCollection(HOUR_IN_MILLIS); + final NetworkIdentitySet ident = new NetworkIdentitySet(); + ident.add(new NetworkIdentity(ConnectivityManager.TYPE_MOBILE, -1, TEST_IMSI, null, + false, true, true, OEM_NONE)); + large.recordData(ident, UID_ALL, SET_ALL, TAG_NONE, TIME_A, TIME_B, + new NetworkStats.Entry(12_730_893_164L, 1, 0, 0, 0)); + + // Verify untouched total + assertEquals(12_730_893_164L, getHistory(large, null, TIME_A, TIME_C).getTotalBytes()); + + // Verify anchor that might cause overflows + final SubscriptionPlan plan = SubscriptionPlan.Builder + .createRecurringMonthly(ZonedDateTime.parse("2012-01-09T00:00:00.00Z")) + .setDataUsage(4_939_212_390L, TIME_B).build(); + assertEquals(4_939_212_386L, getHistory(large, plan, TIME_A, TIME_C).getTotalBytes()); + } + + @Test + public void testRounding() throws Exception { + final NetworkStatsCollection coll = new NetworkStatsCollection(HOUR_IN_MILLIS); + + // Special values should remain unchanged + for (long time : new long[] { + Long.MIN_VALUE, Long.MAX_VALUE, SubscriptionPlan.TIME_UNKNOWN + }) { + assertEquals(time, coll.roundUp(time)); + assertEquals(time, coll.roundDown(time)); + } + + assertEquals(TIME_A, coll.roundUp(TIME_A)); + assertEquals(TIME_A, coll.roundDown(TIME_A)); + + assertEquals(TIME_A + HOUR_IN_MILLIS, coll.roundUp(TIME_A + 1)); + assertEquals(TIME_A, coll.roundDown(TIME_A + 1)); + + assertEquals(TIME_A, coll.roundUp(TIME_A - 1)); + assertEquals(TIME_A - HOUR_IN_MILLIS, coll.roundDown(TIME_A - 1)); + } + + @Test + public void testMultiplySafeRational() { + assertEquals(25, multiplySafeByRational(50, 1, 2)); + assertEquals(100, multiplySafeByRational(50, 2, 1)); + + assertEquals(-10, multiplySafeByRational(30, -1, 3)); + assertEquals(0, multiplySafeByRational(30, 0, 3)); + assertEquals(10, multiplySafeByRational(30, 1, 3)); + assertEquals(20, multiplySafeByRational(30, 2, 3)); + assertEquals(30, multiplySafeByRational(30, 3, 3)); + assertEquals(40, multiplySafeByRational(30, 4, 3)); + + assertEquals(100_000_000_000L, + multiplySafeByRational(300_000_000_000L, 10_000_000_000L, 30_000_000_000L)); + assertEquals(100_000_000_010L, + multiplySafeByRational(300_000_000_000L, 10_000_000_001L, 30_000_000_000L)); + assertEquals(823_202_048L, + multiplySafeByRational(4_939_212_288L, 2_121_815_528L, 12_730_893_165L)); + + assertThrows(ArithmeticException.class, () -> multiplySafeByRational(30, 3, 0)); + } + + /** + * Copy a {@link Resources#openRawResource(int)} into {@link File} for + * testing purposes. + */ + private void stageFile(int rawId, File file) throws Exception { + new File(file.getParent()).mkdirs(); + InputStream in = null; + OutputStream out = null; + try { + in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId); + out = new FileOutputStream(file); + Streams.copy(in, out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + } + } + + private static NetworkStatsHistory getHistory(NetworkStatsCollection collection, + SubscriptionPlan augmentPlan, long start, long end) { + return collection.getHistory(buildTemplateMobileAll(TEST_IMSI), augmentPlan, UID_ALL, + SET_ALL, TAG_NONE, FIELD_ALL, start, end, NetworkStatsAccess.Level.DEVICE, myUid()); + } + + private static void assertSummaryTotal(NetworkStatsCollection collection, + NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets, + @NetworkStatsAccess.Level int accessLevel) { + final NetworkStats.Entry actual = collection.getSummary( + template, Long.MIN_VALUE, Long.MAX_VALUE, accessLevel, myUid()) + .getTotal(null); + assertEntry(rxBytes, rxPackets, txBytes, txPackets, actual); + } + + private static void assertSummaryTotalIncludingTags(NetworkStatsCollection collection, + NetworkTemplate template, long rxBytes, long rxPackets, long txBytes, long txPackets) { + final NetworkStats.Entry actual = collection.getSummary( + template, Long.MIN_VALUE, Long.MAX_VALUE, NetworkStatsAccess.Level.DEVICE, myUid()) + .getTotalIncludingTags(null); + assertEntry(rxBytes, rxPackets, txBytes, txPackets, actual); + } + + private static void assertEntry(long rxBytes, long rxPackets, long txBytes, long txPackets, + NetworkStats.Entry actual) { + assertEntry(new NetworkStats.Entry(rxBytes, rxPackets, txBytes, txPackets, 0L), actual); + } + + private static void assertEntry(long rxBytes, long rxPackets, long txBytes, long txPackets, + NetworkStatsHistory.Entry actual) { + assertEntry(new NetworkStats.Entry(rxBytes, rxPackets, txBytes, txPackets, 0L), actual); + } + + private static void assertEntry(NetworkStats.Entry expected, + NetworkStatsHistory.Entry actual) { + assertEntry(expected, new NetworkStats.Entry(actual.rxBytes, actual.rxPackets, + actual.txBytes, actual.txPackets, 0L)); + } + + private static void assertEntry(NetworkStats.Entry expected, + NetworkStats.Entry actual) { + assertEquals("unexpected rxBytes", expected.rxBytes, actual.rxBytes); + assertEquals("unexpected rxPackets", expected.rxPackets, actual.rxPackets); + assertEquals("unexpected txBytes", expected.txBytes, actual.txBytes); + assertEquals("unexpected txPackets", expected.txPackets, actual.txPackets); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..93599f3c376d0a043a12440d34c05b2508e5e7bc --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java @@ -0,0 +1,579 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net; + +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; + +import static com.android.server.NetworkManagementSocketTagger.kernelToTag; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.content.res.Resources; +import android.net.NetworkStats; +import android.net.TrafficStats; +import android.net.UnderlyingNetworkInfo; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.frameworks.tests.net.R; +import com.android.internal.util.test.FsUtil; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.InputStream; +import java.io.OutputStream; + +/** Tests for {@link NetworkStatsFactory}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsFactoryTest extends NetworkStatsBaseTest { + private static final String CLAT_PREFIX = "v4-"; + + private File mTestProc; + private NetworkStatsFactory mFactory; + + @Before + public void setUp() throws Exception { + mTestProc = new File(InstrumentationRegistry.getContext().getFilesDir(), "proc"); + if (mTestProc.exists()) { + FsUtil.deleteContents(mTestProc); + } + + // The libandroid_servers which have the native method is not available to + // applications. So in order to have a test support native library, the native code + // related to networkStatsFactory is compiled to a minimal native library and loaded here. + System.loadLibrary("networkstatsfactorytestjni"); + mFactory = new NetworkStatsFactory(mTestProc, false); + mFactory.updateUnderlyingNetworkInfos(new UnderlyingNetworkInfo[0]); + } + + @After + public void tearDown() throws Exception { + mFactory = null; + + if (mTestProc.exists()) { + FsUtil.deleteContents(mTestProc); + } + } + + @Test + public void testNetworkStatsDetail() throws Exception { + final NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_typical); + + assertEquals(70, stats.size()); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 18621L, 2898L); + assertStatsEntry(stats, "wlan0", 10011, SET_DEFAULT, 0x0, 35777L, 5718L); + assertStatsEntry(stats, "wlan0", 10021, SET_DEFAULT, 0x7fffff01, 562386L, 49228L); + assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 227423L); + assertStatsEntry(stats, "rmnet2", 10001, SET_DEFAULT, 0x0, 1125899906842624L, 984L); + } + + @Test + public void testVpnRewriteTrafficThroughItself() throws Exception { + UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // + // VPN UID rewrites packets read from TUN back to TUN, plus some of its own traffic + + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_rewrite_through_self); + + assertValues(tunStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 2000L, 200L, 1000L, 100L, 0); + assertValues(tunStats, TUN_IFACE, UID_BLUE, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 1000L, 100L, 500L, 50L, 0); + assertValues(tunStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 0L, 0L, 1600L, 160L, 0); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 260L, 26L); + } + + @Test + public void testVpnWithClat() throws Exception { + final UnderlyingNetworkInfo[] underlyingNetworkInfos = new UnderlyingNetworkInfo[] { + createVpnInfo(new String[] {CLAT_PREFIX + TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + mFactory.noteStackedIface(CLAT_PREFIX + TEST_IFACE, TEST_IFACE); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over v4-WiFi, and clat + // added 20 bytes per packet of extra overhead + // + // For 1650 bytes sent over v4-WiFi, 4650 bytes were actually sent over WiFi, which is + // expected to be split as follows: + // UID_RED: 1000 bytes, 100 packets + // UID_BLUE: 500 bytes, 50 packets + // UID_VPN: 3150 bytes, 0 packets + // + // For 3300 bytes received over v4-WiFi, 9300 bytes were actually sent over WiFi, which is + // expected to be split as follows: + // UID_RED: 2000 bytes, 200 packets + // UID_BLUE: 1000 bytes, 100 packets + // UID_VPN: 6300 bytes, 0 packets + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_with_clat); + + assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_RED, 2000L, 200L, 1000, 100L); + assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, CLAT_PREFIX + TEST_IFACE, UID_VPN, 6300L, 0L, 3150L, 0L); + } + + @Test + public void testVpnWithOneUnderlyingIface() throws Exception { + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // VPN sent 1650 bytes (150 packets), and received 3300 (300 packets) over WiFi. + // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN. + // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes + // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN. + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 150L, 0L); + } + + @Test + public void testVpnWithOneUnderlyingIfaceAndOwnTraffic() throws Exception { + // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE). + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN. + // Additionally, the VPN sends 6000 bytes (600 packets) of its own traffic into the tun + // interface (passing that traffic to the VPN endpoint), and receives 5000 bytes (500 + // packets) from it. Including overhead that is 6600/5500 bytes. + // VPN sent 8250 bytes (750 packets), and received 8800 (800 packets) over WiFi. + // Of 8250 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 6750 bytes attributed to UID_VPN. + // Of 8800 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes + // attributed to UID_BLUE, and 5800 bytes attributed to UID_VPN. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_own_traffic); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 5800L, 500L, 6750L, 600L); + } + + @Test + public void testVpnWithOneUnderlyingIface_withCompression() throws Exception { + // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE). + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent/received by UID_RED over VPN. + // 3000 bytes (300 packets) were sent/received by UID_BLUE over VPN. + // VPN sent/received 1000 bytes (100 packets) over WiFi. + // Of 1000 bytes over WiFi, expect 250 bytes attributed UID_RED and 750 bytes to UID_BLUE, + // with nothing attributed to UID_VPN for both rx/tx traffic. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_compression); + + assertValues(tunStats, TEST_IFACE, UID_RED, 250L, 25L, 250L, 25L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 750L, 75L, 750L, 75L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L); + } + + @Test + public void testVpnWithTwoUnderlyingIfaces_packetDuplication() throws Exception { + // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and + // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set. + // Additionally, VPN is duplicating traffic across both WiFi and Cell. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent/received by UID_RED and UID_BLUE over VPN. + // VPN sent/received 4400 bytes (400 packets) over both WiFi and Cell (8800 bytes in total). + // Of 8800 bytes over WiFi/Cell, expect: + // - 500 bytes rx/tx each over WiFi/Cell attributed to both UID_RED and UID_BLUE. + // - 1200 bytes rx/tx each over WiFi/Cell for VPN_UID. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_duplication); + + assertValues(tunStats, TEST_IFACE, UID_RED, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 1200L, 100L, 1200L, 100L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE2, UID_BLUE, 500L, 50L, 500L, 50L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 1200L, 100L, 1200L, 100L); + } + + @Test + public void testConcurrentVpns() throws Exception { + // Assume two VPNs are connected on two different network interfaces. VPN1 is using + // TEST_IFACE and VPN2 is using TEST_IFACE2. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = new UnderlyingNetworkInfo[] { + createVpnInfo(TUN_IFACE, new String[] {TEST_IFACE}), + createVpnInfo(TUN_IFACE2, new String[] {TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 2000 bytes (200 packets) were received by UID_RED + // over VPN1. + // 700 bytes (70 packets) were sent, and 3000 bytes (300 packets) were received by UID_RED + // over VPN2. + // 500 bytes (50 packets) were sent, and 1000 bytes (100 packets) were received by UID_BLUE + // over VPN1. + // 250 bytes (25 packets) were sent, and 500 bytes (50 packets) were received by UID_BLUE + // over VPN2. + // VPN1 sent 1650 bytes (150 packets), and received 3300 (300 packets) over TEST_IFACE. + // Of 1650 bytes sent over WiFi, expect 1000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 150 bytes attributed to UID_VPN. + // Of 3300 bytes received over WiFi, expect 2000 bytes attributed to UID_RED, 1000 bytes + // attributed to UID_BLUE, and 300 bytes attributed to UID_VPN. + // VPN2 sent 1045 bytes (95 packets), and received 3850 (350 packets) over TEST_IFACE2. + // Of 1045 bytes sent over Cell, expect 700 bytes attributed to UID_RED, 250 bytes + // attributed to UID_BLUE, and 95 bytes attributed to UID_VPN. + // Of 3850 bytes received over Cell, expect 3000 bytes attributed to UID_RED, 500 bytes + // attributed to UID_BLUE, and 350 bytes attributed to UID_VPN. + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_one_underlying_two_vpn); + + assertValues(tunStats, TEST_IFACE, UID_RED, 2000L, 200L, 1000L, 100L); + assertValues(tunStats, TEST_IFACE, UID_BLUE, 1000L, 100L, 500L, 50L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 3000L, 300L, 700L, 70L); + assertValues(tunStats, TEST_IFACE2, UID_BLUE, 500L, 50L, 250L, 25L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 300L, 0L, 150L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 350L, 0L, 95L, 0L); + } + + @Test + public void testVpnWithTwoUnderlyingIfaces_splitTraffic() throws Exception { + // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and + // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set. + // Additionally, VPN is arbitrarily splitting traffic across WiFi and Cell. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent, and 500 bytes (50 packets) received by UID_RED over + // VPN. + // VPN sent 660 bytes (60 packets) over WiFi and 440 bytes (40 packets) over Cell. + // And, it received 330 bytes (30 packets) over WiFi and 220 bytes (20 packets) over Cell. + // For UID_RED, expect 600 bytes attributed over WiFi and 400 bytes over Cell for sent (tx) + // traffic. For received (rx) traffic, expect 300 bytes over WiFi and 200 bytes over Cell. + // + // For UID_VPN, expect 60 bytes attributed over WiFi and 40 bytes over Cell for tx traffic. + // And, 30 bytes over WiFi and 20 bytes over Cell for rx traffic. + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_split); + + assertValues(tunStats, TEST_IFACE, UID_RED, 300L, 30L, 600L, 60L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 30L, 0L, 60L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 200L, 20L, 400L, 40L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 20L, 0L, 40L, 0L); + } + + @Test + public void testVpnWithTwoUnderlyingIfaces_splitTrafficWithCompression() throws Exception { + // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and + // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set. + // Additionally, VPN is arbitrarily splitting compressed traffic across WiFi and Cell. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface: + // 1000 bytes (100 packets) were sent/received by UID_RED over VPN. + // VPN sent/received 600 bytes (60 packets) over WiFi and 200 bytes (20 packets) over Cell. + // For UID_RED, expect 600 bytes attributed over WiFi and 200 bytes over Cell for both + // rx/tx. + // UID_VPN gets nothing attributed to it (avoiding negative stats). + final NetworkStats tunStats = + parseDetailedStats(R.raw.xt_qtaguid_vpn_two_underlying_split_compression); + + assertValues(tunStats, TEST_IFACE, UID_RED, 600L, 60L, 600L, 60L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 200L, 20L, 200L, 20L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 0L, 0L, 0L, 0L); + } + + @Test + public void testVpnWithIncorrectUnderlyingIface() throws Exception { + // WiFi and Cell networks are connected and VPN is using Cell (which has TEST_IFACE2), + // but has declared only WiFi (TEST_IFACE) in its underlying network set. + final UnderlyingNetworkInfo[] underlyingNetworkInfos = + new UnderlyingNetworkInfo[] {createVpnInfo(new String[] {TEST_IFACE})}; + mFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos); + + // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption + // overhead per packet): + // 1000 bytes (100 packets) were sent/received by UID_RED over VPN. + // VPN sent/received 1100 bytes (100 packets) over Cell. + // Of 1100 bytes over Cell, expect all of it attributed to UID_VPN for both rx/tx traffic. + final NetworkStats tunStats = parseDetailedStats(R.raw.xt_qtaguid_vpn_incorrect_iface); + + assertValues(tunStats, TEST_IFACE, UID_RED, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE, UID_VPN, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_RED, 0L, 0L, 0L, 0L); + assertValues(tunStats, TEST_IFACE2, UID_VPN, 1100L, 100L, 1100L, 100L); + } + + @Test + public void testKernelTags() throws Exception { + assertEquals(0, kernelToTag("0x0000000000000000")); + assertEquals(0x32, kernelToTag("0x0000003200000000")); + assertEquals(2147483647, kernelToTag("0x7fffffff00000000")); + assertEquals(0, kernelToTag("0x0000000000000000")); + assertEquals(2147483136, kernelToTag("0x7FFFFE0000000000")); + + assertEquals(0, kernelToTag("0x0")); + assertEquals(0, kernelToTag("0xf00d")); + assertEquals(1, kernelToTag("0x100000000")); + assertEquals(14438007, kernelToTag("0xdc4e7700000000")); + assertEquals(TrafficStats.TAG_SYSTEM_DOWNLOAD, kernelToTag("0xffffff0100000000")); + } + + @Test + public void testNetworkStatsWithSet() throws Exception { + final NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_typical); + assertEquals(70, stats.size()); + assertStatsEntry(stats, "rmnet1", 10021, SET_DEFAULT, 0x30100000, 219110L, 578L, 227423L, + 676L); + assertStatsEntry(stats, "rmnet1", 10021, SET_FOREGROUND, 0x30100000, 742L, 3L, 1265L, 3L); + } + + @Test + public void testNetworkStatsSingle() throws Exception { + stageFile(R.raw.xt_qtaguid_iface_typical, file("net/xt_qtaguid/iface_stat_all")); + + final NetworkStats stats = mFactory.readNetworkStatsSummaryDev(); + assertEquals(6, stats.size()); + assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 2112L, 24L, 700L, 10L); + assertStatsEntry(stats, "test1", UID_ALL, SET_ALL, TAG_NONE, 6L, 8L, 10L, 12L); + assertStatsEntry(stats, "test2", UID_ALL, SET_ALL, TAG_NONE, 1L, 2L, 3L, 4L); + } + + @Test + public void testNetworkStatsXt() throws Exception { + stageFile(R.raw.xt_qtaguid_iface_fmt_typical, file("net/xt_qtaguid/iface_stat_fmt")); + + final NetworkStats stats = mFactory.readNetworkStatsSummaryXt(); + assertEquals(3, stats.size()); + assertStatsEntry(stats, "rmnet0", UID_ALL, SET_ALL, TAG_NONE, 6824L, 16L, 5692L, 10L); + assertStatsEntry(stats, "rmnet1", UID_ALL, SET_ALL, TAG_NONE, 11153922L, 8051L, 190226L, + 2468L); + assertStatsEntry(stats, "rmnet2", UID_ALL, SET_ALL, TAG_NONE, 4968L, 35L, 3081L, 39L); + } + + @Test + public void testDoubleClatAccountingSimple() throws Exception { + mFactory.noteStackedIface("v4-wlan0", "wlan0"); + + // xt_qtaguid_with_clat_simple is a synthetic file that simulates + // - 213 received 464xlat packets of size 200 bytes + // - 41 sent 464xlat packets of size 100 bytes + // - no other traffic on base interface for root uid. + NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_simple); + assertEquals(3, stats.size()); + + assertStatsEntry(stats, "v4-wlan0", 10060, SET_DEFAULT, 0x0, 46860L, 4920L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 0L, 0L); + } + + @Test + public void testDoubleClatAccounting() throws Exception { + mFactory.noteStackedIface("v4-wlan0", "wlan0"); + + NetworkStats stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat); + assertEquals(42, stats.size()); + + assertStatsEntry(stats, "v4-wlan0", 0, SET_DEFAULT, 0x0, 356L, 276L); + assertStatsEntry(stats, "v4-wlan0", 1000, SET_DEFAULT, 0x0, 30812L, 2310L); + assertStatsEntry(stats, "v4-wlan0", 10102, SET_DEFAULT, 0x0, 10022L, 3330L); + assertStatsEntry(stats, "v4-wlan0", 10060, SET_DEFAULT, 0x0, 9532772L, 254112L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, 0L, 0L); + assertStatsEntry(stats, "wlan0", 1000, SET_DEFAULT, 0x0, 6126L, 2013L); + assertStatsEntry(stats, "wlan0", 10013, SET_DEFAULT, 0x0, 0L, 144L); + assertStatsEntry(stats, "wlan0", 10018, SET_DEFAULT, 0x0, 5980263L, 167667L); + assertStatsEntry(stats, "wlan0", 10060, SET_DEFAULT, 0x0, 134356L, 8705L); + assertStatsEntry(stats, "wlan0", 10079, SET_DEFAULT, 0x0, 10926L, 1507L); + assertStatsEntry(stats, "wlan0", 10102, SET_DEFAULT, 0x0, 25038L, 8245L); + assertStatsEntry(stats, "wlan0", 10103, SET_DEFAULT, 0x0, 0L, 192L); + assertStatsEntry(stats, "dummy0", 0, SET_DEFAULT, 0x0, 0L, 168L); + assertStatsEntry(stats, "lo", 0, SET_DEFAULT, 0x0, 1288L, 1288L); + + assertNoStatsEntry(stats, "wlan0", 1029, SET_DEFAULT, 0x0); + } + + @Test + public void testDoubleClatAccounting100MBDownload() throws Exception { + // Downloading 100mb from an ipv4 only destination in a foreground activity + + long appRxBytesBefore = 328684029L; + long appRxBytesAfter = 439237478L; + assertEquals("App traffic should be ~100MB", 110553449, appRxBytesAfter - appRxBytesBefore); + + long rootRxBytes = 330187296L; + + mFactory.noteStackedIface("v4-wlan0", "wlan0"); + NetworkStats stats; + + // Stats snapshot before the download + stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_100mb_download_before); + assertStatsEntry(stats, "v4-wlan0", 10106, SET_FOREGROUND, 0x0, appRxBytesBefore, 5199872L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, rootRxBytes, 0L); + + // Stats snapshot after the download + stats = parseDetailedStats(R.raw.xt_qtaguid_with_clat_100mb_download_after); + assertStatsEntry(stats, "v4-wlan0", 10106, SET_FOREGROUND, 0x0, appRxBytesAfter, 7867488L); + assertStatsEntry(stats, "wlan0", 0, SET_DEFAULT, 0x0, rootRxBytes, 0L); + } + + /** + * Copy a {@link Resources#openRawResource(int)} into {@link File} for + * testing purposes. + */ + private void stageFile(int rawId, File file) throws Exception { + new File(file.getParent()).mkdirs(); + InputStream in = null; + OutputStream out = null; + try { + in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId); + out = new FileOutputStream(file); + Streams.copy(in, out); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + } + } + + private void stageLong(long value, File file) throws Exception { + new File(file.getParent()).mkdirs(); + FileWriter out = null; + try { + out = new FileWriter(file); + out.write(Long.toString(value)); + } finally { + IoUtils.closeQuietly(out); + } + } + + private File file(String path) throws Exception { + return new File(mTestProc, path); + } + + private NetworkStats parseDetailedStats(int resourceId) throws Exception { + stageFile(resourceId, file("net/xt_qtaguid/stats")); + return mFactory.readNetworkStatsDetail(); + } + + private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag, long rxBytes, long txBytes) { + final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO); + if (i < 0) { + fail(String.format("no NetworkStats for (iface: %s, uid: %d, set: %d, tag: %d)", + iface, uid, set, tag)); + } + final NetworkStats.Entry entry = stats.getValues(i, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + } + + private static void assertNoStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag) { + final int i = stats.findIndex(iface, uid, set, tag, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO); + if (i >= 0) { + fail("unexpected NetworkStats entry at " + i); + } + } + + private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag, long rxBytes, long rxPackets, long txBytes, long txPackets) { + assertStatsEntry(stats, iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, + rxBytes, rxPackets, txBytes, txPackets); + } + + private static void assertStatsEntry(NetworkStats stats, String iface, int uid, int set, + int tag, int metered, int roaming, int defaultNetwork, long rxBytes, long rxPackets, + long txBytes, long txPackets) { + final int i = stats.findIndex(iface, uid, set, tag, metered, roaming, defaultNetwork); + + if (i < 0) { + fail(String.format("no NetworkStats for (iface: %s, uid: %d, set: %d, tag: %d, metered:" + + " %d, roaming: %d, defaultNetwork: %d)", + iface, uid, set, tag, metered, roaming, defaultNetwork)); + } + final NetworkStats.Entry entry = stats.getValues(i, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9fa1c50423d92448e4cd2734ef33322c04f74720 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.NetworkIdentity.OEM_NONE; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.net.NetworkTemplate.buildTemplateWifiWildcard; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; + +import android.app.usage.NetworkStatsManager; +import android.net.DataUsageRequest; +import android.net.NetworkIdentity; +import android.net.NetworkStats; +import android.net.NetworkTemplate; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Messenger; +import android.os.Process; +import android.os.UserHandle; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.net.NetworkStatsServiceTest.LatchedHandler; +import com.android.testutils.HandlerUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; + +/** + * Tests for {@link NetworkStatsObservers}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsObserversTest { + private static final String TEST_IFACE = "test0"; + private static final String TEST_IFACE2 = "test1"; + private static final long TEST_START = 1194220800000L; + + private static final String IMSI_1 = "310004"; + private static final String IMSI_2 = "310260"; + private static final String TEST_SSID = "AndroidAP"; + + private static NetworkTemplate sTemplateWifi = buildTemplateWifiWildcard(); + private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1); + private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2); + + private static final int UID_RED = UserHandle.PER_USER_RANGE + 1; + private static final int UID_BLUE = UserHandle.PER_USER_RANGE + 2; + private static final int UID_GREEN = UserHandle.PER_USER_RANGE + 3; + private static final int UID_ANOTHER_USER = 2 * UserHandle.PER_USER_RANGE + 4; + + private static final long WAIT_TIMEOUT_MS = 500; + private static final long THRESHOLD_BYTES = 2 * MB_IN_BYTES; + private static final long BASE_BYTES = 7 * MB_IN_BYTES; + private static final int INVALID_TYPE = -1; + + private long mElapsedRealtime; + + private HandlerThread mObserverHandlerThread; + private Handler mObserverNoopHandler; + + private LatchedHandler mHandler; + + private NetworkStatsObservers mStatsObservers; + private Messenger mMessenger; + private ArrayMap<String, NetworkIdentitySet> mActiveIfaces; + private ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces; + + @Mock private IBinder mockBinder; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mObserverHandlerThread = new HandlerThread("HandlerThread"); + mObserverHandlerThread.start(); + final Looper observerLooper = mObserverHandlerThread.getLooper(); + mStatsObservers = new NetworkStatsObservers() { + @Override + protected Looper getHandlerLooperLocked() { + return observerLooper; + } + }; + + mHandler = new LatchedHandler(Looper.getMainLooper(), new ConditionVariable()); + mMessenger = new Messenger(mHandler); + + mActiveIfaces = new ArrayMap<>(); + mActiveUidIfaces = new ArrayMap<>(); + } + + @Test + public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception { + long thresholdTooLowBytes = 1L; + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdTooLowBytes); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + } + + @Test + public void testRegister_highThreshold_accepted() throws Exception { + long highThresholdBytes = 2 * THRESHOLD_BYTES; + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, highThresholdBytes); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request.template)); + assertEquals(highThresholdBytes, request.thresholdInBytes); + } + + @Test + public void testRegister_twoRequests_twoIds() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, THRESHOLD_BYTES); + + DataUsageRequest request1 = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request1.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request1.template)); + assertEquals(THRESHOLD_BYTES, request1.thresholdInBytes); + + DataUsageRequest request2 = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request2.requestId > request1.requestId); + assertTrue(Objects.equals(sTemplateWifi, request2.template)); + assertEquals(THRESHOLD_BYTES, request2.thresholdInBytes); + } + + @Test + public void testUnregister_unknownRequest_noop() throws Exception { + DataUsageRequest unknownRequest = new DataUsageRequest( + 123456 /* id */, sTemplateWifi, THRESHOLD_BYTES); + + mStatsObservers.unregister(unknownRequest, UID_RED); + } + + @Test + public void testUnregister_knownRequest_releasesCaller() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + + mStatsObservers.unregister(request, Process.SYSTEM_UID); + waitForObserverToIdle(); + + Mockito.verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + } + + @Test + public void testUnregister_knownRequest_invalidUid_doesNotUnregister() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_RED, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + Mockito.verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + + mStatsObservers.unregister(request, UID_BLUE); + waitForObserverToIdle(); + + Mockito.verifyZeroInteractions(mockBinder); + } + + private NetworkIdentitySet makeTestIdentSet() { + NetworkIdentitySet identSet = new NetworkIdentitySet(); + identSet.add(new NetworkIdentity( + TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_UNKNOWN, + IMSI_1, null /* networkId */, false /* roaming */, true /* metered */, + true /* defaultNetwork */, OEM_NONE)); + return identSet; + } + + @Test + public void testUpdateStats_initialSample_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L); + NetworkStats uidSnapshot = null; + + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + @Test + public void testUpdateStats_belowThreshold_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L); + NetworkStats uidSnapshot = null; + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES + 1024L, 10L, BASE_BYTES + 2048L, 20L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + + @Test + public void testUpdateStats_deviceAccess_notifies() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + Process.SYSTEM_UID, NetworkStatsAccess.Level.DEVICE); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = new NetworkStats(TEST_START, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES, 8L, BASE_BYTES, 16L); + NetworkStats uidSnapshot = null; + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + xtSnapshot = new NetworkStats(TEST_START + MINUTE_IN_MILLIS, 1 /* initialSize */) + .insertEntry(TEST_IFACE, BASE_BYTES + THRESHOLD_BYTES, 12L, + BASE_BYTES + THRESHOLD_BYTES, 22L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + } + + @Test + public void testUpdateStats_defaultAccess_notifiesSameUid() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_RED, NetworkStatsAccess.Level.DEFAULT); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + } + + @Test + public void testUpdateStats_defaultAccess_usageOtherUid_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_BLUE, NetworkStatsAccess.Level.DEFAULT); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + @Test + public void testUpdateStats_userAccess_usageSameUser_notifies() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_BLUE, NetworkStatsAccess.Level.USER); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, mHandler.lastMessageType); + } + + @Test + public void testUpdateStats_userAccess_usageAnotherUser_doesNotNotify() throws Exception { + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateImsi1, THRESHOLD_BYTES); + + DataUsageRequest request = mStatsObservers.register(inputRequest, mMessenger, mockBinder, + UID_RED, NetworkStatsAccess.Level.USER); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateImsi1, request.template)); + assertEquals(THRESHOLD_BYTES, request.thresholdInBytes); + + NetworkIdentitySet identSet = makeTestIdentSet(); + mActiveUidIfaces.put(TEST_IFACE, identSet); + + // Baseline + NetworkStats xtSnapshot = null; + NetworkStats uidSnapshot = new NetworkStats(TEST_START, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_YES, BASE_BYTES, 2L, BASE_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + + // Delta + uidSnapshot = new NetworkStats(TEST_START + 2 * MINUTE_IN_MILLIS, 2 /* initialSize */) + .insertEntry(TEST_IFACE, UID_ANOTHER_USER, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, BASE_BYTES + THRESHOLD_BYTES, 2L, + BASE_BYTES + THRESHOLD_BYTES, 2L, 0L); + mStatsObservers.updateStats( + xtSnapshot, uidSnapshot, mActiveIfaces, mActiveUidIfaces, TEST_START); + waitForObserverToIdle(); + } + + private void waitForObserverToIdle() { + HandlerUtils.waitForIdle(mObserverHandlerThread, WAIT_TIMEOUT_MS); + HandlerUtils.waitForIdle(mHandler, WAIT_TIMEOUT_MS); + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0ba5f7d8241e3c5d8addeeec988552593909a0ed --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java @@ -0,0 +1,1811 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net; + +import static android.content.Intent.ACTION_UID_REMOVED; +import static android.content.Intent.EXTRA_UID; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.NetworkIdentity.OEM_PAID; +import static android.net.NetworkIdentity.OEM_PRIVATE; +import static android.net.NetworkStats.DEFAULT_NETWORK_ALL; +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.DEFAULT_NETWORK_YES; +import static android.net.NetworkStats.IFACE_ALL; +import static android.net.NetworkStats.INTERFACES_ALL; +import static android.net.NetworkStats.METERED_ALL; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.METERED_YES; +import static android.net.NetworkStats.ROAMING_ALL; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.ROAMING_YES; +import static android.net.NetworkStats.SET_ALL; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.STATS_PER_UID; +import static android.net.NetworkStats.TAG_ALL; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStatsHistory.FIELD_ALL; +import static android.net.NetworkTemplate.MATCH_MOBILE_WILDCARD; +import static android.net.NetworkTemplate.NETWORK_TYPE_ALL; +import static android.net.NetworkTemplate.OEM_MANAGED_NO; +import static android.net.NetworkTemplate.OEM_MANAGED_YES; +import static android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT; +import static android.net.NetworkTemplate.buildTemplateMobileAll; +import static android.net.NetworkTemplate.buildTemplateMobileWithRatType; +import static android.net.NetworkTemplate.buildTemplateWifi; +import static android.net.NetworkTemplate.buildTemplateWifiWildcard; +import static android.net.TrafficStats.MB_IN_BYTES; +import static android.net.TrafficStats.UID_REMOVED; +import static android.net.TrafficStats.UID_TETHERING; +import static android.text.format.DateUtils.DAY_IN_MILLIS; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; +import static android.text.format.DateUtils.WEEK_IN_MILLIS; + +import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.AlarmManager; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.net.DataUsageRequest; +import android.net.INetworkManagementEventObserver; +import android.net.INetworkStatsSession; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkStateSnapshot; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.net.TelephonyNetworkSpecifier; +import android.net.UnderlyingNetworkInfo; +import android.net.netstats.provider.INetworkStatsProviderCallback; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.PowerManager; +import android.os.SimpleClock; +import android.provider.Settings; +import android.telephony.TelephonyManager; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.internal.util.test.FsUtil; +import com.android.server.net.NetworkStatsService.NetworkStatsSettings; +import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config; +import com.android.testutils.HandlerUtils; +import com.android.testutils.TestableNetworkStatsProviderBinder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Tests for {@link NetworkStatsService}. + * + * TODO: This test used to be really brittle because it used Easymock - it uses Mockito now, but + * still uses the Easymock structure, which could be simplified. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NetworkStatsServiceTest extends NetworkStatsBaseTest { + private static final String TAG = "NetworkStatsServiceTest"; + + private static final long TEST_START = 1194220800000L; + + private static final String IMSI_1 = "310004"; + private static final String IMSI_2 = "310260"; + private static final String TEST_SSID = "AndroidAP"; + + private static NetworkTemplate sTemplateWifi = buildTemplateWifi(TEST_SSID); + private static NetworkTemplate sTemplateCarrierWifi1 = + buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL, IMSI_1); + private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1); + private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2); + + private static final Network WIFI_NETWORK = new Network(100); + private static final Network MOBILE_NETWORK = new Network(101); + private static final Network VPN_NETWORK = new Network(102); + + private static final Network[] NETWORKS_WIFI = new Network[]{ WIFI_NETWORK }; + private static final Network[] NETWORKS_MOBILE = new Network[]{ MOBILE_NETWORK }; + + private static final long WAIT_TIMEOUT = 2 * 1000; // 2 secs + private static final int INVALID_TYPE = -1; + + private long mElapsedRealtime; + + private File mStatsDir; + private MockContext mServiceContext; + private @Mock TelephonyManager mTelephonyManager; + private @Mock INetworkManagementService mNetManager; + private @Mock NetworkStatsFactory mStatsFactory; + private @Mock NetworkStatsSettings mSettings; + private @Mock IBinder mBinder; + private @Mock AlarmManager mAlarmManager; + @Mock + private NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor; + private HandlerThread mHandlerThread; + + private NetworkStatsService mService; + private INetworkStatsSession mSession; + private INetworkManagementEventObserver mNetworkObserver; + private ContentObserver mContentObserver; + private Handler mHandler; + + private class MockContext extends BroadcastInterceptingContext { + private final Context mBaseContext; + + MockContext(Context base) { + super(base); + mBaseContext = base; + } + + @Override + public Object getSystemService(String name) { + if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager; + return mBaseContext.getSystemService(name); + } + } + + private final Clock mClock = new SimpleClock(ZoneOffset.UTC) { + @Override + public long millis() { + return currentTimeMillis(); + } + }; + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + final Context context = InstrumentationRegistry.getContext(); + mServiceContext = new MockContext(context); + mStatsDir = context.getFilesDir(); + if (mStatsDir.exists()) { + FsUtil.deleteContents(mStatsDir); + } + + PowerManager powerManager = (PowerManager) mServiceContext.getSystemService( + Context.POWER_SERVICE); + PowerManager.WakeLock wakeLock = + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + + mHandlerThread = new HandlerThread("HandlerThread"); + final NetworkStatsService.Dependencies deps = makeDependencies(); + mService = new NetworkStatsService(mServiceContext, mNetManager, mAlarmManager, wakeLock, + mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir, + getBaseDir(mStatsDir), deps); + + mElapsedRealtime = 0L; + + expectDefaultSettings(); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectSystemReady(); + mService.systemReady(); + // Verify that system ready fetches realtime stats + verify(mStatsFactory).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL); + // Wait for posting onChange() event to handler thread and verify that when system ready, + // start monitoring data usage per RAT type because the settings value is mock as false + // by default in expectSettings(). + waitForIdle(); + verify(mNetworkStatsSubscriptionsMonitor).start(); + reset(mNetworkStatsSubscriptionsMonitor); + + doReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS).when(mTelephonyManager) + .checkCarrierPrivilegesForPackageAnyPhone(anyString()); + + mSession = mService.openSession(); + assertNotNull("openSession() failed", mSession); + + // catch INetworkManagementEventObserver during systemReady() + ArgumentCaptor<INetworkManagementEventObserver> networkObserver = + ArgumentCaptor.forClass(INetworkManagementEventObserver.class); + verify(mNetManager).registerObserver(networkObserver.capture()); + mNetworkObserver = networkObserver.getValue(); + } + + @NonNull + private NetworkStatsService.Dependencies makeDependencies() { + return new NetworkStatsService.Dependencies() { + @Override + public HandlerThread makeHandlerThread() { + return mHandlerThread; + } + + @Override + public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor( + @NonNull Context context, @NonNull Looper looper, @NonNull Executor executor, + @NonNull NetworkStatsService service) { + + return mNetworkStatsSubscriptionsMonitor; + } + + @Override + public ContentObserver makeContentObserver(Handler handler, + NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) { + mHandler = handler; + return mContentObserver = super.makeContentObserver(handler, settings, monitor); + } + + }; + } + + @After + public void tearDown() throws Exception { + FsUtil.deleteContents(mStatsDir); + + mServiceContext = null; + mStatsDir = null; + + mNetManager = null; + mSettings = null; + + mSession.close(); + mService = null; + + mHandlerThread.quitSafely(); + } + + private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception { + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {snapshot}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + } + + private void incrementWifiStats(long durationMillis, String iface, + long rxb, long rxp, long txb, long txp) throws Exception { + incrementCurrentTime(durationMillis); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(iface, rxb, rxp, txb, txp)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + } + + @Test + public void testNetworkStatsCarrierWifi() throws Exception { + initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1)); + // verify service has empty history for carrier merged wifi and non-carrier wifi + assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0); + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + + // modify some number on wifi, and trigger poll event + incrementWifiStats(HOUR_IN_MILLIS, TEST_IFACE, 1024L, 1L, 2048L, 2L); + + // verify service recorded history + assertNetworkTotal(sTemplateCarrierWifi1, 1024L, 1L, 2048L, 2L, 0); + + // verify service recorded history for wifi with SSID filter + assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0); + + + // and bump forward again, with counters going higher. this is + // important, since polling should correctly subtract last snapshot. + incrementWifiStats(DAY_IN_MILLIS, TEST_IFACE, 4096L, 4L, 8192L, 8L); + + // verify service recorded history + assertNetworkTotal(sTemplateCarrierWifi1, 4096L, 4L, 8192L, 8L, 0); + // verify service recorded history for wifi with SSID filter + assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0); + } + + @Test + public void testNetworkStatsNonCarrierWifi() throws Exception { + initWifiStats(buildWifiState()); + + // verify service has empty history for wifi + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + // verify service has empty history for carrier merged wifi + assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0); + + // modify some number on wifi, and trigger poll event + incrementWifiStats(HOUR_IN_MILLIS, TEST_IFACE, 1024L, 1L, 2048L, 2L); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0); + // verify service has empty history for carrier wifi since current network is non carrier + // wifi + assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0); + + // and bump forward again, with counters going higher. this is + // important, since polling should correctly subtract last snapshot. + incrementWifiStats(DAY_IN_MILLIS, TEST_IFACE, 4096L, 4L, 8192L, 8L); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0); + // verify service has empty history for carrier wifi since current network is non carrier + // wifi + assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0); + } + + @Test + public void testStatsRebootPersist() throws Exception { + assertStatsFilesExist(false); + + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // verify service has empty history for wifi + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + + + // modify some number on wifi, and trigger poll event + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L)); + mService.setUidForeground(UID_RED, false); + mService.incrementOperationCount(UID_RED, 0xFAAD, 4); + mService.setUidForeground(UID_RED, true); + mService.incrementOperationCount(UID_RED, 0xFAAD, 6); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10); + assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 4); + assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 6); + assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0); + + + // graceful shutdown system, which should trigger persist of stats, and + // clear any values in memory. + expectDefaultSettings(); + mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN)); + assertStatsFilesExist(true); + + // boot through serviceReady() again + expectDefaultSettings(); + expectNetworkStatsUidDetail(buildEmptyStats()); + expectSystemReady(); + + mService.systemReady(); + + // after systemReady(), we should have historical stats loaded again + assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 1024L, 8L, 512L, 4L, 10); + assertUidTotal(sTemplateWifi, UID_RED, SET_DEFAULT, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 4); + assertUidTotal(sTemplateWifi, UID_RED, SET_FOREGROUND, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 512L, 4L, 256L, 2L, 6); + assertUidTotal(sTemplateWifi, UID_BLUE, 128L, 1L, 128L, 1L, 0); + + } + + // TODO: simulate reboot to test bucket resize + @Test + @Ignore + public void testStatsBucketResize() throws Exception { + NetworkStatsHistory history = null; + + assertStatsFilesExist(false); + + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // modify some number on wifi, and trigger poll event + incrementCurrentTime(2 * HOUR_IN_MILLIS); + expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 512L, 4L, 512L, 4L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL); + assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0); + assertEquals(HOUR_IN_MILLIS, history.getBucketDuration()); + assertEquals(2, history.size()); + + + // now change bucket duration setting and trigger another poll with + // exact same values, which should resize existing buckets. + expectSettings(0L, 30 * MINUTE_IN_MILLIS, WEEK_IN_MILLIS); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify identical stats, but spread across 4 buckets now + history = mSession.getHistoryForNetwork(sTemplateWifi, FIELD_ALL); + assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, 512L, 4L, 512L, 4L, 0); + assertEquals(30 * MINUTE_IN_MILLIS, history.getBucketDuration()); + assertEquals(4, history.size()); + + } + + @Test + public void testUidStatsAcrossNetworks() throws Exception { + // pretend first mobile network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some traffic on first network + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 10); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0); + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10); + assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0); + + + // now switch networks; this also tests that we're okay with interfaces + // disappearing, to verify we don't count backwards. + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + states = new NetworkStateSnapshot[] {buildMobile3gState(IMSI_2)}; + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 2048L, 16L, 512L, 4L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L)); + + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + forcePollAndWaitForIdle(); + + + // create traffic on second network + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 2176L, 17L, 1536L, 12L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1536L, 12L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 640L, 5L, 1024L, 8L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xFAAD, 128L, 1L, 1024L, 8L, 0L)); + mService.incrementOperationCount(UID_BLUE, 0xFAAD, 10); + + forcePollAndWaitForIdle(); + + // verify original history still intact + assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0); + assertUidTotal(sTemplateImsi1, UID_RED, 1536L, 12L, 512L, 4L, 10); + assertUidTotal(sTemplateImsi1, UID_BLUE, 512L, 4L, 0L, 0L, 0); + + // and verify new history also recorded under different template, which + // verifies that we didn't cross the streams. + assertNetworkTotal(sTemplateImsi2, 128L, 1L, 1024L, 8L, 0); + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateImsi2, UID_BLUE, 128L, 1L, 1024L, 8L, 10); + + } + + @Test + public void testUidRemovedIsMoved() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, + 4096L, 258L, 512L, 32L, 0L) + .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)); + mService.incrementOperationCount(UID_RED, 0xFAAD, 10); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 16L, 1L, 16L, 1L, 10); + assertUidTotal(sTemplateWifi, UID_BLUE, 4096L, 258L, 512L, 32L, 0); + assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0); + + + // now pretend two UIDs are uninstalled, which should migrate stats to + // special "removed" bucket. + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4128L, 258L, 544L, 34L)); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 16L, 1L, 16L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, + 4096L, 258L, 512L, 32L, 0L) + .insertEntry(TEST_IFACE, UID_GREEN, SET_DEFAULT, TAG_NONE, 16L, 1L, 16L, 1L, 0L)); + final Intent intent = new Intent(ACTION_UID_REMOVED); + intent.putExtra(EXTRA_UID, UID_BLUE); + mServiceContext.sendBroadcast(intent); + intent.putExtra(EXTRA_UID, UID_RED); + mServiceContext.sendBroadcast(intent); + + // existing uid and total should remain unchanged; but removed UID + // should be gone completely. + assertNetworkTotal(sTemplateWifi, 4128L, 258L, 544L, 34L, 0); + assertUidTotal(sTemplateWifi, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateWifi, UID_BLUE, 0L, 0L, 0L, 0L, 0); + assertUidTotal(sTemplateWifi, UID_GREEN, 16L, 1L, 16L, 1L, 0); + assertUidTotal(sTemplateWifi, UID_REMOVED, 4112L, 259L, 528L, 33L, 10); + + } + + @Test + public void testMobileStatsByRatType() throws Exception { + final NetworkTemplate template3g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS); + final NetworkTemplate template4g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_LTE); + final NetworkTemplate template5g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_NR); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)}; + + // 3G network comes online. + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS); + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L, 18L, 14L, 1L, 0L))); + forcePollAndWaitForIdle(); + + // Verify 3g templates gets stats. + assertUidTotal(sTemplateImsi1, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template4g, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0); + + // 4G network comes online. + incrementCurrentTime(MINUTE_IN_MILLIS); + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_LTE); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + // Append more traffic on existing 3g stats entry. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 16L, 22L, 17L, 2L, 0L)) + // Add entry that is new on 4g. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 33L, 27L, 8L, 10L, 1L))); + forcePollAndWaitForIdle(); + + // Verify ALL_MOBILE template gets all. 3g template counters do not increase. + assertUidTotal(sTemplateImsi1, UID_RED, 49L, 49L, 25L, 12L, 1); + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + // Verify 4g template counts appended stats on existing entry and newly created entry. + assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1); + // Verify 5g template doesn't get anything since no traffic is generated on 5g. + assertUidTotal(template5g, UID_RED, 0L, 0L, 0L, 0L, 0); + + // 5g network comes online. + incrementCurrentTime(MINUTE_IN_MILLIS); + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_NR); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + // Existing stats remains. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 16L, 22L, 17L, 2L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 33L, 27L, 8L, 10L, 1L)) + // Add some traffic on 5g. + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 5L, 13L, 31L, 9L, 2L))); + forcePollAndWaitForIdle(); + + // Verify ALL_MOBILE template gets all. + assertUidTotal(sTemplateImsi1, UID_RED, 54L, 62L, 56L, 21L, 3); + // 3g/4g template counters do not increase. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(template4g, UID_RED, 4L + 33L, 4L + 27L, 3L + 8L, 1L + 10L, 1); + // Verify 5g template gets the 5g count. + assertUidTotal(template5g, UID_RED, 5L, 13L, 31L, 9L, 2); + } + + @Test + public void testMobileStatsOemManaged() throws Exception { + final NetworkTemplate templateOemPaid = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_PAID, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemPrivate = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_PRIVATE, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemAll = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, + OEM_PAID | OEM_PRIVATE, SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemYes = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_YES, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + final NetworkTemplate templateOemNone = new NetworkTemplate(MATCH_MOBILE_WILDCARD, + /*subscriberId=*/null, /*matchSubscriberIds=*/null, /*networkId=*/null, + METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, NETWORK_TYPE_ALL, OEM_MANAGED_NO, + SUBSCRIBER_ID_MATCH_RULE_EXACT); + + // OEM_PAID network comes online. + NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{ + buildOemManagedMobileState(IMSI_1, false, + new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PAID})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 36L, 41L, 24L, 96L, 0L))); + forcePollAndWaitForIdle(); + + // OEM_PRIVATE network comes online. + states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, + new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 49L, 71L, 72L, 48L, 0L))); + forcePollAndWaitForIdle(); + + // OEM_PAID + OEM_PRIVATE network comes online. + states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, + new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE, + NetworkCapabilities.NET_CAPABILITY_OEM_PAID})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 57L, 86L, 83L, 93L, 0L))); + forcePollAndWaitForIdle(); + + // OEM_NONE network comes online. + states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, new int[]{})}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 29L, 73L, 34L, 31L, 0L))); + forcePollAndWaitForIdle(); + + // Verify OEM_PAID template gets only relevant stats. + assertUidTotal(templateOemPaid, UID_RED, 36L, 41L, 24L, 96L, 0); + + // Verify OEM_PRIVATE template gets only relevant stats. + assertUidTotal(templateOemPrivate, UID_RED, 49L, 71L, 72L, 48L, 0); + + // Verify OEM_PAID + OEM_PRIVATE template gets only relevant stats. + assertUidTotal(templateOemAll, UID_RED, 57L, 86L, 83L, 93L, 0); + + // Verify OEM_NONE sees only non-OEM managed stats. + assertUidTotal(templateOemNone, UID_RED, 29L, 73L, 34L, 31L, 0); + + // Verify OEM_MANAGED_YES sees all OEM managed stats. + assertUidTotal(templateOemYes, UID_RED, + 36L + 49L + 57L, + 41L + 71L + 86L, + 24L + 72L + 83L, + 96L + 48L + 93L, 0); + + // Verify ALL_MOBILE template gets both OEM managed and non-OEM managed stats. + assertUidTotal(sTemplateImsi1, UID_RED, + 36L + 49L + 57L + 29L, + 41L + 71L + 86L + 73L, + 24L + 72L + 83L + 34L, + 96L + 48L + 93L + 31L, 0); + } + + // TODO: support per IMSI state + private void setMobileRatTypeAndWaitForIdle(int ratType) { + when(mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(anyString())) + .thenReturn(ratType); + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + } + + @Test + public void testSummaryForAllUid() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some traffic for two apps + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1024L, 8L, 512L, 4L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateWifi, UID_RED, 50L, 5L, 50L, 5L, 1); + assertUidTotal(sTemplateWifi, UID_BLUE, 1024L, 8L, 512L, 4L, 0); + + + // now create more traffic in next hour, but only for one app + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 10L, 1L, 10L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, + 2048L, 16L, 1024L, 8L, 0L)); + forcePollAndWaitForIdle(); + + // first verify entire history present + NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(3, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 50L, 5L, 50L, 5L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 10L, 1L, 10L, 1L, 1); + assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 2048L, 16L, 1024L, 8L, 0); + + // now verify that recent history only contains one uid + final long currentTime = currentTimeMillis(); + stats = mSession.getSummaryForAllUid( + sTemplateWifi, currentTime - HOUR_IN_MILLIS, currentTime, true); + assertEquals(1, stats.size()); + assertValues(stats, IFACE_ALL, UID_BLUE, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1024L, 8L, 512L, 4L, 0); + } + + @Test + public void testDetailedUidStats() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + NetworkStats.Entry entry1 = new NetworkStats.Entry( + TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 50L, 5L, 50L, 5L, 0L); + NetworkStats.Entry entry2 = new NetworkStats.Entry( + TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 50L, 5L, 50L, 5L, 0L); + NetworkStats.Entry entry3 = new NetworkStats.Entry( + TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xBEEF, 1024L, 8L, 512L, 4L, 0L); + + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3) + .insertEntry(entry1) + .insertEntry(entry2) + .insertEntry(entry3)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + NetworkStats stats = mService.getDetailedUidStats(INTERFACES_ALL); + + assertEquals(3, stats.size()); + entry1.operations = 1; + assertEquals(entry1, stats.getValues(0, null)); + entry2.operations = 1; + assertEquals(entry2, stats.getValues(1, null)); + assertEquals(entry3, stats.getValues(2, null)); + } + + @Test + public void testDetailedUidStats_Filtered() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + + final String stackedIface = "stacked-test0"; + final LinkProperties stackedProp = new LinkProperties(); + stackedProp.setInterfaceName(stackedIface); + final NetworkStateSnapshot wifiState = buildWifiState(); + wifiState.getLinkProperties().addStackedLink(stackedProp); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {wifiState}; + + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + NetworkStats.Entry uidStats = new NetworkStats.Entry( + TEST_IFACE, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L); + // Stacked on matching interface + NetworkStats.Entry tetheredStats1 = new NetworkStats.Entry( + stackedIface, UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L); + // Different interface + NetworkStats.Entry tetheredStats2 = new NetworkStats.Entry( + "otherif", UID_BLUE, SET_DEFAULT, 0xF00D, 1024L, 8L, 512L, 4L, 0L); + + final String[] ifaceFilter = new String[] { TEST_IFACE }; + final String[] augmentedIfaceFilter = new String[] { stackedIface, TEST_IFACE }; + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + when(mStatsFactory.augmentWithStackedInterfaces(eq(ifaceFilter))) + .thenReturn(augmentedIfaceFilter); + when(mStatsFactory.readNetworkStatsDetail(eq(UID_ALL), any(), eq(TAG_ALL))) + .thenReturn(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(uidStats)); + when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)) + .thenReturn(new NetworkStats(getElapsedRealtime(), 2) + .insertEntry(tetheredStats1) + .insertEntry(tetheredStats2)); + + NetworkStats stats = mService.getDetailedUidStats(ifaceFilter); + + // mStatsFactory#readNetworkStatsDetail() has the following invocations: + // 1) NetworkStatsService#systemReady from #setUp. + // 2) mService#notifyNetworkStatus in the test above. + // + // Additionally, we should have one call from the above call to mService#getDetailedUidStats + // with the augmented ifaceFilter. + verify(mStatsFactory, times(2)).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL); + verify(mStatsFactory, times(1)).readNetworkStatsDetail( + eq(UID_ALL), + eq(augmentedIfaceFilter), + eq(TAG_ALL)); + assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE)); + assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface)); + assertEquals(2, stats.size()); + assertEquals(uidStats, stats.getValues(0, null)); + assertEquals(tetheredStats1, stats.getValues(1, null)); + } + + @Test + public void testForegroundBackground() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some initial traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1); + + + // now switch to foreground + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 64L, 1L, 64L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 32L, 2L, 32L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 1L, 1L, 1L, 1L, 0L)); + mService.setUidForeground(UID_RED, true); + mService.incrementOperationCount(UID_RED, 0xFAAD, 1); + + forcePollAndWaitForIdle(); + + // test that we combined correctly + assertUidTotal(sTemplateWifi, UID_RED, 160L, 4L, 160L, 4L, 2); + + // verify entire history present + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(4, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 32L, 2L, 32L, 2L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_FOREGROUND, 0xFAAD, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 1L, 1L, 1L, 1); + } + + @Test + public void testMetered() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = + new NetworkStateSnapshot[] {buildWifiState(true /* isMetered */, TEST_IFACE)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some initial traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + // Note that all traffic from NetworkManagementService is tagged as METERED_NO, ROAMING_NO + // and DEFAULT_NETWORK_YES, because these three properties aren't tracked at that layer. + // We layer them on top by inspecting the iface properties. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0L)); + mService.incrementOperationCount(UID_RED, 0xF00D, 1); + + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1); + // verify entire history present + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(2, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1); + } + + @Test + public void testRoaming() throws Exception { + // pretend that network comes online + expectDefaultSettings(); + NetworkStateSnapshot[] states = + new NetworkStateSnapshot[] {buildMobile3gState(IMSI_1, true /* isRoaming */)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + // Note that all traffic from NetworkManagementService is tagged as METERED_NO and + // ROAMING_NO, because metered and roaming isn't tracked at that layer. We layer it + // on top by inspecting the iface properties. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0L)); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0); + + // verify entire history present + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(2, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_ALL, ROAMING_YES, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 0); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_ALL, ROAMING_YES, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 0); + } + + @Test + public void testTethering() throws Exception { + // pretend first mobile network comes online + expectDefaultSettings(); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // create some tethering traffic + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST-TETHERING-OFFLOAD", provider); + assertNotNull(cb); + final long now = getElapsedRealtime(); + + // Traffic seen by kernel counters (includes software tethering). + final NetworkStats swIfaceStats = new NetworkStats(now, 1) + .insertEntry(TEST_IFACE, 1536L, 12L, 384L, 3L); + // Hardware tethering traffic, not seen by kernel counters. + final NetworkStats tetherHwIfaceStats = new NetworkStats(now, 1) + .insertEntry(new NetworkStats.Entry(TEST_IFACE, UID_ALL, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 512L, 4L, 128L, 1L, 0L)); + final NetworkStats tetherHwUidStats = new NetworkStats(now, 1) + .insertEntry(new NetworkStats.Entry(TEST_IFACE, UID_TETHERING, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 512L, 4L, 128L, 1L, 0L)); + cb.notifyStatsUpdated(0 /* unused */, tetherHwIfaceStats, tetherHwUidStats); + + // Fake some traffic done by apps on the device (as opposed to tethering), and record it + // into UID stats (as opposed to iface stats). + final NetworkStats localUidStats = new NetworkStats(now, 1) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 128L, 2L, 128L, 2L, 0L); + // Software per-uid tethering traffic. + final NetworkStats tetherSwUidStats = new NetworkStats(now, 1) + .insertEntry(TEST_IFACE, UID_TETHERING, SET_DEFAULT, TAG_NONE, 1408L, 10L, 256L, 1L, + 0L); + + expectNetworkStatsSummary(swIfaceStats); + expectNetworkStatsUidDetail(localUidStats, tetherSwUidStats); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateImsi1, 2048L, 16L, 512L, 4L, 0); + assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 0); + assertUidTotal(sTemplateImsi1, UID_TETHERING, 1920L, 14L, 384L, 2L, 0); + } + + @Test + public void testRegisterUsageCallback() throws Exception { + // pretend that wifi network comes online; service should ask about full + // network state, and poll any existing interfaces before updating. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // verify service has empty history for wifi + assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0); + long thresholdInBytes = 1L; // very small; should be overriden by framework + DataUsageRequest inputRequest = new DataUsageRequest( + DataUsageRequest.REQUEST_ID_UNSET, sTemplateWifi, thresholdInBytes); + + // Create a messenger that waits for callback activity + ConditionVariable cv = new ConditionVariable(false); + LatchedHandler latchedHandler = new LatchedHandler(Looper.getMainLooper(), cv); + Messenger messenger = new Messenger(latchedHandler); + + // Force poll + expectDefaultSettings(); + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // Register and verify request and that binder was called + DataUsageRequest request = + mService.registerUsageCallback(mServiceContext.getOpPackageName(), inputRequest, + messenger, mBinder); + assertTrue(request.requestId > 0); + assertTrue(Objects.equals(sTemplateWifi, request.template)); + long minThresholdInBytes = 2 * 1024 * 1024; // 2 MB + assertEquals(minThresholdInBytes, request.thresholdInBytes); + + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + + // Make sure that the caller binder gets connected + verify(mBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + + // modify some number on wifi, and trigger poll event + // not enough traffic to call data usage callback + incrementCurrentTime(HOUR_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 1024L, 1L, 2048L, 2L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0); + + // make sure callback has not being called + assertEquals(INVALID_TYPE, latchedHandler.lastMessageType); + + // and bump forward again, with counters going higher. this is + // important, since it will trigger the data usage callback + incrementCurrentTime(DAY_IN_MILLIS); + expectDefaultSettings(); + expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1) + .insertEntry(TEST_IFACE, 4096000L, 4L, 8192000L, 8L)); + expectNetworkStatsUidDetail(buildEmptyStats()); + forcePollAndWaitForIdle(); + + // verify service recorded history + assertNetworkTotal(sTemplateWifi, 4096000L, 4L, 8192000L, 8L, 0); + + + // Wait for the caller to ack receipt of CALLBACK_LIMIT_REACHED + assertTrue(cv.block(WAIT_TIMEOUT)); + assertEquals(NetworkStatsManager.CALLBACK_LIMIT_REACHED, latchedHandler.lastMessageType); + cv.close(); + + // Allow binder to disconnect + when(mBinder.unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt())).thenReturn(true); + + // Unregister request + mService.unregisterUsageRequest(request); + + // Wait for the caller to ack receipt of CALLBACK_RELEASED + assertTrue(cv.block(WAIT_TIMEOUT)); + assertEquals(NetworkStatsManager.CALLBACK_RELEASED, latchedHandler.lastMessageType); + + // Make sure that the caller binder gets disconnected + verify(mBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt()); + } + + @Test + public void testUnregisterUsageCallback_unknown_noop() throws Exception { + String callingPackage = "the.calling.package"; + long thresholdInBytes = 10 * 1024 * 1024; // 10 MB + DataUsageRequest unknownRequest = new DataUsageRequest( + 2 /* requestId */, sTemplateImsi1, thresholdInBytes); + + mService.unregisterUsageRequest(unknownRequest); + } + + @Test + public void testStatsProviderUpdateStats() throws Exception { + // Pretend that network comes online. + expectDefaultSettings(); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST", provider); + assertNotNull(cb); + + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Verifies that one requestStatsUpdate will be called during iface update. + provider.expectOnRequestStatsUpdate(0 /* unused */); + + // Create some initial traffic and report to the service. + incrementCurrentTime(HOUR_IN_MILLIS); + final NetworkStats expectedStats = new NetworkStats(0L, 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 128L, 2L, 128L, 2L, 1L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, + 0xF00D, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 64L, 1L, 64L, 1L, 1L)); + cb.notifyStatsUpdated(0 /* unused */, expectedStats, expectedStats); + + // Make another empty mutable stats object. This is necessary since the new NetworkStats + // object will be used to compare with the old one in NetworkStatsRecoder, two of them + // cannot be the same object. + expectNetworkStatsUidDetail(buildEmptyStats()); + + forcePollAndWaitForIdle(); + + // Verifies that one requestStatsUpdate and setAlert will be called during polling. + provider.expectOnRequestStatsUpdate(0 /* unused */); + provider.expectOnSetAlert(MB_IN_BYTES); + + // Verifies that service recorded history, does not verify uid tag part. + assertUidTotal(sTemplateWifi, UID_RED, 128L, 2L, 128L, 2L, 1); + + // Verifies that onStatsUpdated updates the stats accordingly. + final NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(2, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1L); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, 0xF00D, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1L); + + // Verifies that unregister the callback will remove the provider from service. + cb.unregister(); + forcePollAndWaitForIdle(); + provider.assertNoCallback(); + } + + @Test + public void testDualVilteProviderStats() throws Exception { + // Pretend that network comes online. + expectDefaultSettings(); + final int subId1 = 1; + final int subId2 = 2; + final NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{ + buildImsState(IMSI_1, subId1, TEST_IFACE), + buildImsState(IMSI_2, subId2, TEST_IFACE2)}; + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST", provider); + assertNotNull(cb); + + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Verifies that one requestStatsUpdate will be called during iface update. + provider.expectOnRequestStatsUpdate(0 /* unused */); + + // Create some initial traffic and report to the service. + incrementCurrentTime(HOUR_IN_MILLIS); + final String vtIface1 = NetworkStats.IFACE_VT + subId1; + final String vtIface2 = NetworkStats.IFACE_VT + subId2; + final NetworkStats expectedStats = new NetworkStats(0L, 1) + .addEntry(new NetworkStats.Entry(vtIface1, UID_RED, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 128L, 2L, 128L, 2L, 1L)) + .addEntry(new NetworkStats.Entry(vtIface2, UID_RED, SET_DEFAULT, + TAG_NONE, METERED_YES, ROAMING_NO, DEFAULT_NETWORK_YES, + 64L, 1L, 64L, 1L, 1L)); + cb.notifyStatsUpdated(0 /* unused */, expectedStats, expectedStats); + + // Make another empty mutable stats object. This is necessary since the new NetworkStats + // object will be used to compare with the old one in NetworkStatsRecoder, two of them + // cannot be the same object. + expectNetworkStatsUidDetail(buildEmptyStats()); + + forcePollAndWaitForIdle(); + + // Verifies that one requestStatsUpdate and setAlert will be called during polling. + provider.expectOnRequestStatsUpdate(0 /* unused */); + provider.expectOnSetAlert(MB_IN_BYTES); + + // Verifies that service recorded history, does not verify uid tag part. + assertUidTotal(sTemplateImsi1, UID_RED, 128L, 2L, 128L, 2L, 1); + + // Verifies that onStatsUpdated updates the stats accordingly. + NetworkStats stats = mSession.getSummaryForAllUid( + sTemplateImsi1, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(1, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 128L, 2L, 128L, 2L, 1L); + + stats = mSession.getSummaryForAllUid( + sTemplateImsi2, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertEquals(1, stats.size()); + assertValues(stats, IFACE_ALL, UID_RED, SET_DEFAULT, TAG_NONE, METERED_YES, ROAMING_NO, + DEFAULT_NETWORK_YES, 64L, 1L, 64L, 1L, 1L); + + // Verifies that unregister the callback will remove the provider from service. + cb.unregister(); + forcePollAndWaitForIdle(); + provider.assertNoCallback(); + } + + @Test + public void testStatsProviderSetAlert() throws Exception { + // Pretend that network comes online. + expectDefaultSettings(); + NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)}; + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Register custom provider and retrieve callback. + final TestableNetworkStatsProviderBinder provider = + new TestableNetworkStatsProviderBinder(); + final INetworkStatsProviderCallback cb = + mService.registerNetworkStatsProvider("TEST", provider); + assertNotNull(cb); + + // Simulates alert quota of the provider has been reached. + cb.notifyAlertReached(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + + // Verifies that polling is triggered by alert reached. + provider.expectOnRequestStatsUpdate(0 /* unused */); + // Verifies that global alert will be re-armed. + provider.expectOnSetAlert(MB_IN_BYTES); + } + + private void setCombineSubtypeEnabled(boolean enable) { + when(mSettings.getCombineSubtypeEnabled()).thenReturn(enable); + mHandler.post(() -> mContentObserver.onChange(false, Settings.Global + .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED))); + waitForIdle(); + if (enable) { + verify(mNetworkStatsSubscriptionsMonitor).stop(); + } else { + verify(mNetworkStatsSubscriptionsMonitor).start(); + } + } + + @Test + public void testDynamicWatchForNetworkRatTypeChanges() throws Exception { + // Build 3G template, type unknown template to get stats while network type is unknown + // and type all template to get the sum of all network type stats. + final NetworkTemplate template3g = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS); + final NetworkTemplate templateUnknown = + buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN); + final NetworkTemplate templateAll = + buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL); + final NetworkStateSnapshot[] states = + new NetworkStateSnapshot[]{buildMobile3gState(IMSI_1)}; + + expectNetworkStatsSummary(buildEmptyStats()); + expectNetworkStatsUidDetail(buildEmptyStats()); + + // 3G network comes online. + setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS); + mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L, 18L, 14L, 1L, 0L))); + forcePollAndWaitForIdle(); + + // Since CombineSubtypeEnabled is false by default in unit test, the generated traffic + // will be split by RAT type. Verify 3G templates gets stats, while template with unknown + // RAT type gets nothing, and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 0L, 0L, 0L, 0L, 0); + assertUidTotal(templateAll, UID_RED, 12L, 18L, 14L, 1L, 0); + + // Stop monitoring data usage per RAT type changes NetworkStatsService records data + // to {@link TelephonyManager#NETWORK_TYPE_UNKNOWN}. + setCombineSubtypeEnabled(true); + + // Call handleOnCollapsedRatTypeChanged manually to simulate the callback fired + // when stopping monitor, this is needed by NetworkStatsService to trigger + // handleNotifyNetworkStatus. + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + // Append more traffic on existing snapshot. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 12L + 4L, 18L + 4L, 14L + 3L, 1L + 1L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 35L, 29L, 7L, 11L, 1L))); + forcePollAndWaitForIdle(); + + // Verify 3G counters do not increase, while template with unknown RAT type gets new + // traffic and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1); + assertUidTotal(templateAll, UID_RED, 16L + 35L, 22L + 29L, 17L + 7L, 2L + 11L, 1); + + // Start monitoring data usage per RAT type changes and NetworkStatsService records data + // by a granular subtype representative of the actual subtype + setCombineSubtypeEnabled(false); + + mService.handleOnCollapsedRatTypeChanged(); + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + // Create some traffic. + incrementCurrentTime(MINUTE_IN_MILLIS); + // Append more traffic on existing snapshot. + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, + 22L, 26L, 19L, 5L, 0L)) + .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, + 35L, 29L, 7L, 11L, 1L))); + forcePollAndWaitForIdle(); + + // Verify traffic is split by RAT type, no increase on template with unknown RAT type + // and template with NETWORK_TYPE_ALL gets all stats. + assertUidTotal(template3g, UID_RED, 6L + 12L , 4L + 18L, 2L + 14L, 3L + 1L, 0); + assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1); + assertUidTotal(templateAll, UID_RED, 22L + 35L, 26L + 29L, 19L + 7L, 5L + 11L, 1); + } + + @Test + public void testOperationCount_nonDefault_traffic() throws Exception { + // Pretend mobile network comes online, but wifi is the default network. + expectDefaultSettings(); + NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{ + buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobile3gState(IMSI_1)}; + expectNetworkStatsUidDetail(buildEmptyStats()); + mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states), + new UnderlyingNetworkInfo[0]); + + // Create some traffic on mobile network. + incrementCurrentTime(HOUR_IN_MILLIS); + expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_NO, 2L, 1L, 3L, 4L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, + DEFAULT_NETWORK_YES, 1L, 3L, 2L, 1L, 0L) + .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 5L, 4L, 1L, 4L, 0L)); + // Increment operation count, which must have a specific tag. + mService.incrementOperationCount(UID_RED, 0xF00D, 2); + forcePollAndWaitForIdle(); + + // Verify mobile summary is not changed by the operation count. + final NetworkTemplate templateMobile = + buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL); + final NetworkStats statsMobile = mSession.getSummaryForAllUid( + templateMobile, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 3L, 4L, 5L, 5L, 0); + assertValues(statsMobile, IFACE_ALL, UID_RED, SET_ALL, 0xF00D, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 5L, 4L, 1L, 4L, 0); + + // Verify the operation count is blamed onto the default network. + // TODO: Blame onto the default network is not very reasonable. Consider blame onto the + // network that generates the traffic. + final NetworkTemplate templateWifi = buildTemplateWifiWildcard(); + final NetworkStats statsWifi = mSession.getSummaryForAllUid( + templateWifi, Long.MIN_VALUE, Long.MAX_VALUE, true); + assertValues(statsWifi, IFACE_ALL, UID_RED, SET_ALL, 0xF00D, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 2); + } + + private static File getBaseDir(File statsDir) { + File baseDir = new File(statsDir, "netstats"); + baseDir.mkdirs(); + return baseDir; + } + + private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets, + long txBytes, long txPackets, int operations) throws Exception { + assertNetworkTotal(template, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + txPackets, operations); + } + + private void assertNetworkTotal(NetworkTemplate template, long start, long end, long rxBytes, + long rxPackets, long txBytes, long txPackets, int operations) throws Exception { + // verify history API + final NetworkStatsHistory history = mSession.getHistoryForNetwork(template, FIELD_ALL); + assertValues(history, start, end, rxBytes, rxPackets, txBytes, txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForNetwork(template, start, end); + assertValues(stats, IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL, + DEFAULT_NETWORK_ALL, rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private void assertUidTotal(NetworkTemplate template, int uid, long rxBytes, long rxPackets, + long txBytes, long txPackets, int operations) throws Exception { + assertUidTotal(template, uid, SET_ALL, METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private void assertUidTotal(NetworkTemplate template, int uid, int set, int metered, + int roaming, int defaultNetwork, long rxBytes, long rxPackets, long txBytes, + long txPackets, int operations) throws Exception { + // verify history API + final NetworkStatsHistory history = mSession.getHistoryForUid( + template, uid, set, TAG_NONE, FIELD_ALL); + assertValues(history, Long.MIN_VALUE, Long.MAX_VALUE, rxBytes, rxPackets, txBytes, + txPackets, operations); + + // verify summary API + final NetworkStats stats = mSession.getSummaryForAllUid( + template, Long.MIN_VALUE, Long.MAX_VALUE, false); + assertValues(stats, IFACE_ALL, uid, set, TAG_NONE, metered, roaming, defaultNetwork, + rxBytes, rxPackets, txBytes, txPackets, operations); + } + + private void expectSystemReady() throws Exception { + expectNetworkStatsSummary(buildEmptyStats()); + } + + private String getActiveIface(NetworkStateSnapshot... states) throws Exception { + if (states == null || states.length == 0 || states[0].getLinkProperties() == null) { + return null; + } + return states[0].getLinkProperties().getInterfaceName(); + } + + private void expectNetworkStatsSummary(NetworkStats summary) throws Exception { + expectNetworkStatsSummaryDev(summary.clone()); + expectNetworkStatsSummaryXt(summary.clone()); + } + + private void expectNetworkStatsSummaryDev(NetworkStats summary) throws Exception { + when(mStatsFactory.readNetworkStatsSummaryDev()).thenReturn(summary); + } + + private void expectNetworkStatsSummaryXt(NetworkStats summary) throws Exception { + when(mStatsFactory.readNetworkStatsSummaryXt()).thenReturn(summary); + } + + private void expectNetworkStatsUidDetail(NetworkStats detail) throws Exception { + expectNetworkStatsUidDetail(detail, new NetworkStats(0L, 0)); + } + + private void expectNetworkStatsUidDetail(NetworkStats detail, NetworkStats tetherStats) + throws Exception { + when(mStatsFactory.readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL)) + .thenReturn(detail); + + // also include tethering details, since they are folded into UID + when(mNetManager.getNetworkStatsTethering(STATS_PER_UID)).thenReturn(tetherStats); + } + + private void expectDefaultSettings() throws Exception { + expectSettings(0L, HOUR_IN_MILLIS, WEEK_IN_MILLIS); + } + + private void expectSettings(long persistBytes, long bucketDuration, long deleteAge) + throws Exception { + when(mSettings.getPollInterval()).thenReturn(HOUR_IN_MILLIS); + when(mSettings.getPollDelay()).thenReturn(0L); + when(mSettings.getSampleEnabled()).thenReturn(true); + when(mSettings.getCombineSubtypeEnabled()).thenReturn(false); + + final Config config = new Config(bucketDuration, deleteAge, deleteAge); + when(mSettings.getDevConfig()).thenReturn(config); + when(mSettings.getXtConfig()).thenReturn(config); + when(mSettings.getUidConfig()).thenReturn(config); + when(mSettings.getUidTagConfig()).thenReturn(config); + + when(mSettings.getGlobalAlertBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getDevPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getXtPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getUidPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + when(mSettings.getUidTagPersistBytes(anyLong())).thenReturn(MB_IN_BYTES); + } + + private void assertStatsFilesExist(boolean exist) { + final File basePath = new File(mStatsDir, "netstats"); + if (exist) { + assertTrue(basePath.list().length > 0); + } else { + assertTrue(basePath.list().length == 0); + } + } + + private static void assertValues(NetworkStatsHistory stats, long start, long end, long rxBytes, + long rxPackets, long txBytes, long txPackets, int operations) { + final NetworkStatsHistory.Entry entry = stats.getValues(start, end, null); + assertEquals("unexpected rxBytes", rxBytes, entry.rxBytes); + assertEquals("unexpected rxPackets", rxPackets, entry.rxPackets); + assertEquals("unexpected txBytes", txBytes, entry.txBytes); + assertEquals("unexpected txPackets", txPackets, entry.txPackets); + assertEquals("unexpected operations", operations, entry.operations); + } + + private static NetworkStateSnapshot buildWifiState() { + return buildWifiState(false, TEST_IFACE, null); + } + + private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface) { + return buildWifiState(isMetered, iface, null); + } + + private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface, + String subscriberId) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(iface); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !isMetered); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + capabilities.setSSID(TEST_SSID); + return new NetworkStateSnapshot(WIFI_NETWORK, capabilities, prop, subscriberId, TYPE_WIFI); + } + + private static NetworkStateSnapshot buildMobile3gState(String subscriberId) { + return buildMobile3gState(subscriberId, false /* isRoaming */); + } + + private static NetworkStateSnapshot buildMobile3gState(String subscriberId, boolean isRoaming) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(TEST_IFACE); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + return new NetworkStateSnapshot( + MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE); + } + + private NetworkStats buildEmptyStats() { + return new NetworkStats(getElapsedRealtime(), 0); + } + + private static NetworkStateSnapshot buildOemManagedMobileState( + String subscriberId, boolean isRoaming, int[] oemNetCapabilities) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(TEST_IFACE); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, !isRoaming); + for (int nc : oemNetCapabilities) { + capabilities.setCapability(nc, true); + } + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + return new NetworkStateSnapshot(MOBILE_NETWORK, capabilities, prop, subscriberId, + TYPE_MOBILE); + } + + private static NetworkStateSnapshot buildImsState( + String subscriberId, int subId, String ifaceName) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(ifaceName); + final NetworkCapabilities capabilities = new NetworkCapabilities(); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true); + capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_IMS, true); + capabilities.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + capabilities.setNetworkSpecifier(new TelephonyNetworkSpecifier(subId)); + return new NetworkStateSnapshot( + MOBILE_NETWORK, capabilities, prop, subscriberId, TYPE_MOBILE); + } + + private long getElapsedRealtime() { + return mElapsedRealtime; + } + + private long startTimeMillis() { + return TEST_START; + } + + private long currentTimeMillis() { + return startTimeMillis() + mElapsedRealtime; + } + + private void incrementCurrentTime(long duration) { + mElapsedRealtime += duration; + } + + private void forcePollAndWaitForIdle() { + mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL)); + waitForIdle(); + } + + private void waitForIdle() { + HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT); + } + + static class LatchedHandler extends Handler { + private final ConditionVariable mCv; + int lastMessageType = INVALID_TYPE; + + LatchedHandler(Looper looper, ConditionVariable cv) { + super(looper); + mCv = cv; + } + + @Override + public void handleMessage(Message msg) { + lastMessageType = msg.what; + mCv.open(); + super.handleMessage(msg); + } + } +} diff --git a/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6d2c7dc39ffd017ba9b2996298dfc88d65f9f272 --- /dev/null +++ b/tests/unit/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.net; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.NetworkTemplate; +import android.os.test.TestLooper; +import android.telephony.NetworkRegistrationInfo; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.internal.util.CollectionUtils; +import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@RunWith(JUnit4.class) +public final class NetworkStatsSubscriptionsMonitorTest { + private static final int TEST_SUBID1 = 3; + private static final int TEST_SUBID2 = 5; + private static final String TEST_IMSI1 = "466921234567890"; + private static final String TEST_IMSI2 = "466920987654321"; + private static final String TEST_IMSI3 = "466929999999999"; + + @Mock private Context mContext; + @Mock private SubscriptionManager mSubscriptionManager; + @Mock private TelephonyManager mTelephonyManager; + @Mock private NetworkStatsSubscriptionsMonitor.Delegate mDelegate; + private final List<Integer> mTestSubList = new ArrayList<>(); + + private final Executor mExecutor = Executors.newSingleThreadExecutor(); + private NetworkStatsSubscriptionsMonitor mMonitor; + private TestLooper mTestLooper = new TestLooper(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + when(mContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE))) + .thenReturn(mSubscriptionManager); + when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE))) + .thenReturn(mTelephonyManager); + + mMonitor = new NetworkStatsSubscriptionsMonitor(mContext, mTestLooper.getLooper(), + mExecutor, mDelegate); + } + + @Test + public void testStartStop() { + // Verify that addOnSubscriptionsChangedListener() is never called before start(). + verify(mSubscriptionManager, never()) + .addOnSubscriptionsChangedListener(mExecutor, mMonitor); + mMonitor.start(); + verify(mSubscriptionManager).addOnSubscriptionsChangedListener(mExecutor, mMonitor); + + // Verify that removeOnSubscriptionsChangedListener() is never called before stop() + verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(mMonitor); + mMonitor.stop(); + verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mMonitor); + } + + @NonNull + private static int[] convertArrayListToIntArray(@NonNull List<Integer> arrayList) { + final int[] list = new int[arrayList.size()]; + for (int i = 0; i < arrayList.size(); i++) { + list[i] = arrayList.get(i); + } + return list; + } + + private void setRatTypeForSub(List<RatTypeListener> listeners, + int subId, int type) { + final ServiceState serviceState = mock(ServiceState.class); + when(serviceState.getDataNetworkType()).thenReturn(type); + final RatTypeListener match = CollectionUtils + .find(listeners, it -> it.getSubId() == subId); + if (match == null) { + fail("Could not find listener with subId: " + subId); + } + match.onServiceStateChanged(serviceState); + } + + private void addTestSub(int subId, String subscriberId) { + // add SubId to TestSubList. + if (mTestSubList.contains(subId)) fail("The subscriber list already contains this ID"); + + mTestSubList.add(subId); + + final int[] subList = convertArrayListToIntArray(mTestSubList); + when(mSubscriptionManager.getCompleteActiveSubscriptionIdList()).thenReturn(subList); + when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId); + mMonitor.onSubscriptionsChanged(); + } + + private void updateSubscriberIdForTestSub(int subId, @Nullable final String subscriberId) { + when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId); + mMonitor.onSubscriptionsChanged(); + } + + private void removeTestSub(int subId) { + // Remove subId from TestSubList. + mTestSubList.removeIf(it -> it == subId); + final int[] subList = convertArrayListToIntArray(mTestSubList); + when(mSubscriptionManager.getCompleteActiveSubscriptionIdList()).thenReturn(subList); + mMonitor.onSubscriptionsChanged(); + } + + private void assertRatTypeChangedForSub(String subscriberId, int ratType) { + assertEquals(ratType, mMonitor.getRatTypeForSubscriberId(subscriberId)); + final ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class); + // Verify callback with the subscriberId and the RAT type should be as expected. + // It will fail if get a callback with an unexpected RAT type. + verify(mDelegate).onCollapsedRatTypeChanged(eq(subscriberId), typeCaptor.capture()); + final int type = typeCaptor.getValue(); + assertEquals(ratType, type); + } + + private void assertRatTypeNotChangedForSub(String subscriberId, int ratType) { + assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType); + // Should never get callback with any RAT type. + verify(mDelegate, never()).onCollapsedRatTypeChanged(eq(subscriberId), anyInt()); + } + + @Test + public void testSubChangedAndRatTypeChanged() { + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. + addTestSub(TEST_SUBID1, TEST_IMSI1); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Insert sim2. + addTestSub(TEST_SUBID2, TEST_IMSI2); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + verify(mTelephonyManager, times(2)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + reset(mDelegate); + + // Set RAT type of sim1 to UMTS. + // Verify RAT type of sim1 after subscription gets onCollapsedRatTypeChanged() callback + // and others remain untouched. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim2 to LTE. + // Verify RAT type of sim2 after subscription gets onCollapsedRatTypeChanged() callback + // and others remain untouched. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID2, + TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Remove sim2 and verify that callbacks are fired and RAT type is correct for sim2. + // while the other two remain untouched. + removeTestSub(TEST_SUBID2); + verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim1 to UNKNOWN. Then stop monitoring subscription changes + // and verify that the listener for sim1 is removed. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + mMonitor.stop(); + verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + } + + + @Test + public void test5g() { + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. Also capture listener for later use. + addTestSub(TEST_SUBID1, TEST_IMSI1); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + final RatTypeListener listener = CollectionUtils + .find(ratTypeListenerCaptor.getAllValues(), it -> it.getSubId() == TEST_SUBID1); + assertNotNull(listener); + + // Set RAT type to 5G NSA (non-standalone) mode, verify the monitor outputs + // NETWORK_TYPE_5G_NSA. + final ServiceState serviceState = mock(ServiceState.class); + when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_LTE); + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED); + listener.onServiceStateChanged(serviceState); + assertRatTypeChangedForSub(TEST_IMSI1, NetworkTemplate.NETWORK_TYPE_5G_NSA); + reset(mDelegate); + + // Set RAT type to LTE without NR connected, the RAT type should be downgraded to LTE. + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_NONE); + listener.onServiceStateChanged(serviceState); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_LTE); + reset(mDelegate); + + // Verify NR connected with other RAT type does not take effect. + when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_UMTS); + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_CONNECTED); + listener.onServiceStateChanged(serviceState); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + + // Set RAT type to 5G standalone mode, the RAT type should be NR. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_NR); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_NR); + reset(mDelegate); + + // Set NR state to none in standalone mode does not change anything. + when(serviceState.getDataNetworkType()).thenReturn(TelephonyManager.NETWORK_TYPE_NR); + when(serviceState.getNrState()).thenReturn(NetworkRegistrationInfo.NR_STATE_NONE); + listener.onServiceStateChanged(serviceState); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_NR); + } + + @Test + public void testSubscriberIdUnavailable() { + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + + mMonitor.start(); + // Insert sim1, set subscriberId to null which is normal in SIM PIN locked case. + // Verify RAT type is NETWORK_TYPE_UNKNOWN and service will not perform listener + // registration. + addTestSub(TEST_SUBID1, null); + verify(mTelephonyManager, never()).listen(any(), anyInt()); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Set IMSI for sim1, verify the listener will be registered. + updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI1); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + reset(mTelephonyManager); + when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager); + + // Set RAT type of sim1 to UMTS. Verify RAT type of sim1 is changed. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + + // Set IMSI to null again to simulate somehow IMSI is not available, such as + // modem crash. Verify service should unregister listener. + updateSubscriberIdForTestSub(TEST_SUBID1, null); + verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor.getValue()), + eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + clearInvocations(mTelephonyManager); + + // Simulate somehow IMSI is back. Verify service will register with + // another listener and fire callback accordingly. + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor2 = + ArgumentCaptor.forClass(RatTypeListener.class); + updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI1); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor2.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + clearInvocations(mTelephonyManager); + + // Set RAT type of sim1 to LTE. Verify RAT type of sim1 still works. + setRatTypeForSub(ratTypeListenerCaptor2.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_LTE); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_LTE); + reset(mDelegate); + + mMonitor.stop(); + verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor2.getValue()), + eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + } + + /** + * Verify that when IMSI suddenly changed for a given subId, the service will register a new + * listener and unregister the old one, and report changes on updated IMSI. This is for modem + * feature that may be enabled for certain carrier, which changes to use a different IMSI while + * roaming on certain networks for multi-IMSI SIM cards, but the subId stays the same. + */ + @Test + public void testSubscriberIdChanged() { + mMonitor.start(); + // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback + // before changing RAT type. + addTestSub(TEST_SUBID1, TEST_IMSI1); + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor = + ArgumentCaptor.forClass(RatTypeListener.class); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + + // Set RAT type of sim1 to UMTS. + // Verify RAT type of sim1 changes accordingly. + setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + clearInvocations(mTelephonyManager); + + // Simulate IMSI of sim1 changed to IMSI2. Verify the service will register with + // another listener and remove the old one. The RAT type of new IMSI stays at + // NETWORK_TYPE_UNKNOWN until received initial callback from telephony. + final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor2 = + ArgumentCaptor.forClass(RatTypeListener.class); + updateSubscriberIdForTestSub(TEST_SUBID1, TEST_IMSI2); + verify(mTelephonyManager, times(1)).listen(ratTypeListenerCaptor2.capture(), + eq(PhoneStateListener.LISTEN_SERVICE_STATE)); + verify(mTelephonyManager, times(1)).listen(eq(ratTypeListenerCaptor.getValue()), + eq(PhoneStateListener.LISTEN_NONE)); + assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN); + reset(mDelegate); + + // Set RAT type of sim1 to UMTS for new listener to simulate the initial callback received + // from telephony after registration. Verify RAT type of sim1 changes with IMSI2 + // accordingly. + setRatTypeForSub(ratTypeListenerCaptor2.getAllValues(), TEST_SUBID1, + TelephonyManager.NETWORK_TYPE_UMTS); + assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN); + assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UMTS); + reset(mDelegate); + } +} diff --git a/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ebbc0ef625480ff47429cfdbb1b71f80dba305ed --- /dev/null +++ b/tests/unit/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.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.server.net.ipmemorystore; + +import static org.junit.Assert.assertEquals; + +import android.net.ipmemorystore.NetworkAttributes; +import android.net.networkstack.aidl.quirks.IPv6ProvisioningLossQuirk; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.reflect.Field; +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Arrays; + +/** Unit tests for {@link NetworkAttributes}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class NetworkAttributesTest { + private static final String WEIGHT_FIELD_NAME_PREFIX = "WEIGHT_"; + private static final float EPSILON = 0.0001f; + + // This is running two tests to make sure the total weight is the sum of all weights. To be + // sure this is not fireproof, but you'd kind of need to do it on purpose to pass. + @Test + public void testTotalWeight() throws IllegalAccessException, UnknownHostException { + // Make sure that TOTAL_WEIGHT is equal to the sum of the fields starting with WEIGHT_ + float sum = 0f; + final Field[] fieldList = NetworkAttributes.class.getDeclaredFields(); + for (final Field field : fieldList) { + if (!field.getName().startsWith(WEIGHT_FIELD_NAME_PREFIX)) continue; + field.setAccessible(true); + sum += (float) field.get(null); + } + assertEquals(sum, NetworkAttributes.TOTAL_WEIGHT, EPSILON); + + final IPv6ProvisioningLossQuirk ipv6ProvisioningLossQuirk = + new IPv6ProvisioningLossQuirk(3, System.currentTimeMillis() + 7_200_000); + // Use directly the constructor with all attributes, and make sure that when compared + // to itself the score is a clean 1.0f. + final NetworkAttributes na = + new NetworkAttributes( + (Inet4Address) Inet4Address.getByAddress(new byte[] {1, 2, 3, 4}), + System.currentTimeMillis() + 7_200_000, + "some hint", + Arrays.asList(Inet4Address.getByAddress(new byte[] {5, 6, 7, 8}), + Inet4Address.getByAddress(new byte[] {9, 0, 1, 2})), + 98, ipv6ProvisioningLossQuirk); + assertEquals(1.0f, na.getNetworkGroupSamenessConfidence(na), EPSILON); + } +} diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..1c1ba9e12553ceb6691e18955278ac761b2ebcec --- /dev/null +++ b/tests/unit/jni/Android.bp @@ -0,0 +1,28 @@ +package { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library_shared { + name: "libnetworkstatsfactorytestjni", + + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + + srcs: [ + ":lib_networkStatsFactory_native", + "test_onload.cpp", + ], + + shared_libs: [ + "libbpf_android", + "liblog", + "libnativehelper", + "libnetdbpf", + "libnetdutils", + ], +} diff --git a/tests/unit/jni/test_onload.cpp b/tests/unit/jni/test_onload.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5194ddb0d8823bf8685d2d6434a6d704447ab29e --- /dev/null +++ b/tests/unit/jni/test_onload.cpp @@ -0,0 +1,44 @@ +/* + * 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. + */ + +/* + * this is a mini native libaray for NetworkStatsFactoryTest to run properly. It + * load all the native method related to NetworkStatsFactory when test run + */ +#include <nativehelper/JNIHelp.h> +#include "jni.h" +#include "utils/Log.h" +#include "utils/misc.h" + +namespace android { +int register_android_server_net_NetworkStatsFactory(JNIEnv* env); +}; + +using namespace android; + +extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) +{ + JNIEnv* env = NULL; + jint result = -1; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { + ALOGE("GetEnv failed!"); + return result; + } + ALOG_ASSERT(env, "Could not retrieve the env!"); + register_android_server_net_NetworkStatsFactory(env); + return JNI_VERSION_1_4; +} diff --git a/tests/unit/res/raw/history_v1 b/tests/unit/res/raw/history_v1 new file mode 100644 index 0000000000000000000000000000000000000000..de79491c032e13c800c647e165cc2523db015197 Binary files /dev/null and b/tests/unit/res/raw/history_v1 differ diff --git a/tests/unit/res/raw/net_dev_typical b/tests/unit/res/raw/net_dev_typical new file mode 100644 index 0000000000000000000000000000000000000000..290bf03eb9b42df6a1df42c87d3e0f3b41e8c8d3 --- /dev/null +++ b/tests/unit/res/raw/net_dev_typical @@ -0,0 +1,8 @@ +Inter-| Receive | Transmit + face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed + lo: 8308 116 0 0 0 0 0 0 8308 116 0 0 0 0 0 0 +rmnet0: 1507570 2205 0 0 0 0 0 0 489339 2237 0 0 0 0 0 0 + ifb0: 52454 151 0 151 0 0 0 0 0 0 0 0 0 0 0 0 + ifb1: 52454 151 0 151 0 0 0 0 0 0 0 0 0 0 0 0 + sit0: 0 0 0 0 0 0 0 0 0 0 148 0 0 0 0 0 +ip6tnl0: 0 0 0 0 0 0 0 0 0 0 151 151 0 0 0 0 diff --git a/tests/unit/res/raw/netstats_uid_v4 b/tests/unit/res/raw/netstats_uid_v4 new file mode 100644 index 0000000000000000000000000000000000000000..e75fc1ca5c2e6c01a907792cc88faeddce61f1e3 Binary files /dev/null and b/tests/unit/res/raw/netstats_uid_v4 differ diff --git a/tests/unit/res/raw/netstats_v1 b/tests/unit/res/raw/netstats_v1 new file mode 100644 index 0000000000000000000000000000000000000000..e80860a6b9599fd1588a36b77b95a8e90a003efc Binary files /dev/null and b/tests/unit/res/raw/netstats_v1 differ diff --git a/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical b/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical new file mode 100644 index 0000000000000000000000000000000000000000..656d5bb82da4086cdcb5b052243420340527e862 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_iface_fmt_typical @@ -0,0 +1,4 @@ +ifname total_skb_rx_bytes total_skb_rx_packets total_skb_tx_bytes total_skb_tx_packets +rmnet2 4968 35 3081 39 +rmnet1 11153922 8051 190226 2468 +rmnet0 6824 16 5692 10 diff --git a/tests/unit/res/raw/xt_qtaguid_iface_typical b/tests/unit/res/raw/xt_qtaguid_iface_typical new file mode 100644 index 0000000000000000000000000000000000000000..610723aef2f201d67a86bceda9f403fd7ef90d15 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_iface_typical @@ -0,0 +1,6 @@ +rmnet3 1 0 0 0 0 20822 501 1149991 815 +rmnet2 1 0 0 0 0 1594 15 1313 15 +rmnet1 1 0 0 0 0 207398 458 166918 565 +rmnet0 1 0 0 0 0 2112 24 700 10 +test1 1 1 2 3 4 5 6 7 8 +test2 0 1 2 3 4 5 6 7 8 diff --git a/tests/unit/res/raw/xt_qtaguid_typical b/tests/unit/res/raw/xt_qtaguid_typical new file mode 100644 index 0000000000000000000000000000000000000000..c1b0d259955c34e9ad795cbc92cde00601050468 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_typical @@ -0,0 +1,71 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 wlan0 0x0 0 0 18621 96 2898 44 312 6 15897 58 2412 32 312 6 1010 16 1576 22 +3 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 wlan0 0x0 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +5 wlan0 0x0 1000 1 1949 13 1078 14 0 0 1600 10 349 3 0 0 600 10 478 4 +6 wlan0 0x0 10005 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +7 wlan0 0x0 10005 1 32081 38 5315 50 32081 38 0 0 0 0 5315 50 0 0 0 0 +8 wlan0 0x0 10011 0 35777 53 5718 57 0 0 0 0 35777 53 0 0 0 0 5718 57 +9 wlan0 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +10 wlan0 0x0 10014 0 0 0 1098 13 0 0 0 0 0 0 0 0 0 0 1098 13 +11 wlan0 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +12 wlan0 0x0 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549 +13 wlan0 0x0 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6 +15 wlan0 0x0 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +16 wlan0 0x7fffff0100000000 10021 0 562386 573 49228 549 0 0 0 0 562386 573 0 0 0 0 49228 549 +17 wlan0 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +18 wlan0 0x7fffff0100000000 10031 0 3425 5 586 6 0 0 0 0 3425 5 0 0 0 0 586 6 +19 wlan0 0x7fffff0100000000 10031 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +20 rmnet2 0x0 0 0 547 5 118 2 40 1 243 1 264 3 0 0 62 1 56 1 +21 rmnet2 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 rmnet2 0x0 10001 0 1125899906842624 5 984 11 632 5 0 0 0 0 984 11 0 0 0 0 +23 rmnet2 0x0 10001 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +24 rmnet1 0x0 0 0 26736 174 7098 130 7210 97 18382 64 1144 13 2932 64 4054 64 112 2 +25 rmnet1 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 rmnet1 0x0 1000 0 75774 77 18038 78 75335 72 439 5 0 0 17668 73 370 5 0 0 +27 rmnet1 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +28 rmnet1 0x0 10007 0 269945 578 111632 586 269945 578 0 0 0 0 111632 586 0 0 0 0 +29 rmnet1 0x0 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +30 rmnet1 0x0 10011 0 1741256 6918 769778 7019 1741256 6918 0 0 0 0 769778 7019 0 0 0 0 +31 rmnet1 0x0 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +32 rmnet1 0x0 10014 0 0 0 786 12 0 0 0 0 0 0 786 12 0 0 0 0 +33 rmnet1 0x0 10014 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +34 rmnet1 0x0 10021 0 433533 1454 393420 1604 433533 1454 0 0 0 0 393420 1604 0 0 0 0 +35 rmnet1 0x0 10021 1 21215 33 10278 33 21215 33 0 0 0 0 10278 33 0 0 0 0 +36 rmnet1 0x0 10036 0 6310 25 3284 29 6310 25 0 0 0 0 3284 29 0 0 0 0 +37 rmnet1 0x0 10036 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +38 rmnet1 0x0 10047 0 34264 47 3936 34 34264 47 0 0 0 0 3936 34 0 0 0 0 +39 rmnet1 0x0 10047 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +40 rmnet1 0x4e7700000000 10011 0 9187 27 4248 33 9187 27 0 0 0 0 4248 33 0 0 0 0 +41 rmnet1 0x4e7700000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 rmnet1 0x1000000000000000 10007 0 2109 4 791 4 2109 4 0 0 0 0 791 4 0 0 0 0 +43 rmnet1 0x1000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +44 rmnet1 0x1000000400000000 10007 0 9811 22 6286 22 9811 22 0 0 0 0 6286 22 0 0 0 0 +45 rmnet1 0x1000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +46 rmnet1 0x1010000000000000 10021 0 164833 426 135392 527 164833 426 0 0 0 0 135392 527 0 0 0 0 +47 rmnet1 0x1010000000000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +48 rmnet1 0x1144000400000000 10011 0 10112 18 3334 17 10112 18 0 0 0 0 3334 17 0 0 0 0 +49 rmnet1 0x1144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +50 rmnet1 0x1244000400000000 10011 0 1300 3 848 2 1300 3 0 0 0 0 848 2 0 0 0 0 +51 rmnet1 0x1244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +52 rmnet1 0x3000000000000000 10007 0 10389 14 1521 12 10389 14 0 0 0 0 1521 12 0 0 0 0 +53 rmnet1 0x3000000000000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +54 rmnet1 0x3000000400000000 10007 0 238070 380 93938 404 238070 380 0 0 0 0 93938 404 0 0 0 0 +55 rmnet1 0x3000000400000000 10007 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +56 rmnet1 0x3010000000000000 10021 0 219110 578 227423 676 219110 578 0 0 0 0 227423 676 0 0 0 0 +57 rmnet1 0x3010000000000000 10021 1 742 3 1265 3 742 3 0 0 0 0 1265 3 0 0 0 0 +58 rmnet1 0x3020000000000000 10021 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +59 rmnet1 0x3020000000000000 10021 1 20473 30 9013 30 20473 30 0 0 0 0 9013 30 0 0 0 0 +60 rmnet1 0x3144000400000000 10011 0 43963 92 34414 116 43963 92 0 0 0 0 34414 116 0 0 0 0 +61 rmnet1 0x3144000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +62 rmnet1 0x3244000400000000 10011 0 3486 8 1520 9 3486 8 0 0 0 0 1520 9 0 0 0 0 +63 rmnet1 0x3244000400000000 10011 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +64 rmnet1 0x7fffff0100000000 10021 0 29102 56 8865 60 29102 56 0 0 0 0 8865 60 0 0 0 0 +65 rmnet1 0x7fffff0100000000 10021 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +66 rmnet1 0x7fffff0300000000 1000 0 995 13 14145 14 995 13 0 0 0 0 14145 14 0 0 0 0 +67 rmnet1 0x7fffff0300000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +68 rmnet0 0x0 0 0 4312 49 1288 23 0 0 0 0 4312 49 0 0 0 0 1288 23 +69 rmnet0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +70 rmnet0 0x0 10080 0 22266 30 20976 30 0 0 0 0 22266 30 0 0 0 0 20976 30 +71 rmnet0 0x0 10080 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface b/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface new file mode 100644 index 0000000000000000000000000000000000000000..fc92715253ede4c0d99596e03446f747242c1842 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_incorrect_iface @@ -0,0 +1,3 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test1 0x0 1004 0 1100 100 1100 100 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying new file mode 100644 index 0000000000000000000000000000000000000000..1ef18894b669836da5769754cc9c53cb3c0c4e0f --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying @@ -0,0 +1,5 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +5 test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression new file mode 100644 index 0000000000000000000000000000000000000000..6d6bf550bbfa27132de6aad22b1bb59ae6e886d5 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_compression @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 3000 300 3000 300 0 0 0 0 0 0 0 0 0 0 0 0 +4 test0 0x0 1004 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic new file mode 100644 index 0000000000000000000000000000000000000000..2c2e5d2555f6c3e782eb9a298e4fed8a03b95f25 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_own_traffic @@ -0,0 +1,6 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test_nss_tun0 0x0 1004 0 5000 500 6000 600 0 0 0 0 0 0 0 0 0 0 0 0 +5 test0 0x0 1004 0 8800 800 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 1004 1 0 0 8250 750 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn new file mode 100644 index 0000000000000000000000000000000000000000..eb0513b10049cef228d964819947e0faf062b7be --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_one_underlying_two_vpn @@ -0,0 +1,9 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test_nss_tun1 0x0 1001 0 3000 300 700 70 0 0 0 0 0 0 0 0 0 0 0 0 +5 test_nss_tun1 0x0 1002 0 500 50 250 25 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +7 test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0 +8 test1 0x0 1004 0 3850 350 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +9 test1 0x0 1004 1 0 0 1045 95 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self b/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self new file mode 100644 index 0000000000000000000000000000000000000000..afcdd71990264c24f1e7e93c12812b1aff1a8841 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_rewrite_through_self @@ -0,0 +1,6 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 test_nss_tun0 0x0 1004 0 0 0 1600 160 0 0 0 0 0 0 0 0 0 0 0 0 +5 test0 0x0 1004 1 0 0 1760 176 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication new file mode 100644 index 0000000000000000000000000000000000000000..d7c7eb9f4ae8bc9e6affc55728bb55cd49903880 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_duplication @@ -0,0 +1,5 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +4 test0 0x0 1004 0 2200 200 2200 200 0 0 0 0 0 0 0 0 0 0 0 0 +5 test1 0x0 1004 0 2200 200 2200 200 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split new file mode 100644 index 0000000000000000000000000000000000000000..38a3dce4a83443d2ec6105acd58495ebb2cf2222 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 500 50 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test0 0x0 1004 0 330 30 660 60 0 0 0 0 0 0 0 0 0 0 0 0 +4 test1 0x0 1004 0 220 20 440 40 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression new file mode 100644 index 0000000000000000000000000000000000000000..d35244b3b4f29d60e41ebe0f81313d3990446430 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_two_underlying_split_compression @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 1000 100 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test0 0x0 1004 0 600 60 600 60 0 0 0 0 0 0 0 0 0 0 0 0 +4 test1 0x0 1004 0 200 20 200 20 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_vpn_with_clat b/tests/unit/res/raw/xt_qtaguid_vpn_with_clat new file mode 100644 index 0000000000000000000000000000000000000000..0d893d515ca23d61ce86081a070fb3fbd3ab8fe4 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_vpn_with_clat @@ -0,0 +1,8 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 test_nss_tun0 0x0 1001 0 2000 200 1000 100 0 0 0 0 0 0 0 0 0 0 0 0 +3 test_nss_tun0 0x0 1002 0 1000 100 500 50 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-test0 0x0 1004 0 3300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +5 v4-test0 0x0 1004 1 0 0 1650 150 0 0 0 0 0 0 0 0 0 0 0 0 +6 test0 0x0 0 0 9300 300 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +7 test0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +8 test0 0x0 1029 0 0 0 4650 150 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat b/tests/unit/res/raw/xt_qtaguid_with_clat new file mode 100644 index 0000000000000000000000000000000000000000..f04b32f08332d983d04e07258a47e77ed740e86d --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat @@ -0,0 +1,43 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 v4-wlan0 0x0 0 0 256 5 196 4 256 5 0 0 0 0 196 4 0 0 0 0 +3 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-wlan0 0x0 1000 0 30312 25 1770 27 30236 24 76 1 0 0 1694 26 76 1 0 0 +5 v4-wlan0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 v4-wlan0 0x0 10060 0 9398432 6717 169412 4235 9398432 6717 0 0 0 0 169412 4235 0 0 0 0 +7 v4-wlan0 0x0 10060 1 1448660 1041 31192 753 1448660 1041 0 0 0 0 31192 753 0 0 0 0 +8 v4-wlan0 0x0 10102 0 9702 16 2870 23 9702 16 0 0 0 0 2870 23 0 0 0 0 +9 v4-wlan0 0x0 10102 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +10 wlan0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +11 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +12 wlan0 0x0 1000 0 6126 13 2013 16 5934 11 192 2 0 0 1821 14 192 2 0 0 +13 wlan0 0x0 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 10013 0 0 0 144 2 0 0 0 0 0 0 144 2 0 0 0 0 +15 wlan0 0x0 10013 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +16 wlan0 0x0 10018 0 5980263 4715 167667 1922 5972583 4709 0 0 7680 6 167667 1922 0 0 0 0 +17 wlan0 0x0 10018 1 43995 37 2766 27 43995 37 0 0 0 0 2766 27 0 0 0 0 +18 wlan0 0x0 10060 0 134356 133 8705 74 134356 133 0 0 0 0 8705 74 0 0 0 0 +19 wlan0 0x0 10060 1 294709 326 26448 256 294709 326 0 0 0 0 26448 256 0 0 0 0 +20 wlan0 0x0 10079 0 10926 13 1507 13 10926 13 0 0 0 0 1507 13 0 0 0 0 +21 wlan0 0x0 10079 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 wlan0 0x0 10102 0 25038 42 8245 57 25038 42 0 0 0 0 8245 57 0 0 0 0 +23 wlan0 0x0 10102 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +24 wlan0 0x0 10103 0 0 0 192 2 0 0 0 0 0 0 0 0 192 2 0 0 +25 wlan0 0x0 10103 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 wlan0 0x1000040700000000 10018 0 831 6 655 5 831 6 0 0 0 0 655 5 0 0 0 0 +27 wlan0 0x1000040700000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +28 wlan0 0x1000040b00000000 10018 0 1714 8 1561 7 1714 8 0 0 0 0 1561 7 0 0 0 0 +29 wlan0 0x1000040b00000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +30 wlan0 0x1000120300000000 10018 0 8243 11 2234 12 8243 11 0 0 0 0 2234 12 0 0 0 0 +31 wlan0 0x1000120300000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +32 wlan0 0x1000180300000000 10018 0 56368 49 4790 39 56368 49 0 0 0 0 4790 39 0 0 0 0 +33 wlan0 0x1000180300000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +34 wlan0 0x1000300000000000 10018 0 9488 17 18813 25 1808 11 0 0 7680 6 18813 25 0 0 0 0 +35 wlan0 0x1000300000000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +36 wlan0 0x3000180400000000 10018 0 131262 103 7416 103 131262 103 0 0 0 0 7416 103 0 0 0 0 +37 wlan0 0x3000180400000000 10018 1 43995 37 2766 27 43995 37 0 0 0 0 2766 27 0 0 0 0 +38 wlan0 0xffffff0100000000 10018 0 5771986 4518 131190 1725 5771986 4518 0 0 0 0 131190 1725 0 0 0 0 +39 wlan0 0xffffff0100000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +40 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3 +41 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 lo 0x0 0 0 1288 16 1288 16 0 0 532 8 756 8 0 0 532 8 756 8 +43 lo 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after new file mode 100644 index 0000000000000000000000000000000000000000..12d98ca29f5711e49effb6aa1b03d00c54912bcf --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_after @@ -0,0 +1,189 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 r_rmnet_data0 0x0 0 0 0 0 392 6 0 0 0 0 0 0 0 0 0 0 392 6 +3 r_rmnet_data0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-wlan0 0x0 0 0 58952 2072 2888 65 264 6 0 0 58688 2066 132 3 0 0 2756 62 +5 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 v4-wlan0 0x0 10034 0 6192 11 1445 11 6192 11 0 0 0 0 1445 11 0 0 0 0 +7 v4-wlan0 0x0 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +8 v4-wlan0 0x0 10057 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +9 v4-wlan0 0x0 10057 1 728 7 392 7 0 0 728 7 0 0 0 0 392 7 0 0 +10 v4-wlan0 0x0 10106 0 2232 18 2232 18 0 0 2232 18 0 0 0 0 2232 18 0 0 +11 v4-wlan0 0x0 10106 1 432952718 314238 5442288 121260 432950238 314218 2480 20 0 0 5433900 121029 8388 231 0 0 +12 wlan0 0x0 0 0 330187296 250652 0 0 329106990 236273 226202 1255 854104 13124 0 0 0 0 0 0 +13 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 1000 0 77113 272 56151 575 77113 272 0 0 0 0 19191 190 36960 385 0 0 +15 wlan0 0x0 1000 1 20227 80 8356 72 18539 74 1688 6 0 0 7562 66 794 6 0 0 +16 wlan0 0x0 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +17 wlan0 0x0 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +18 wlan0 0x0 10015 0 4390 7 14824 252 4390 7 0 0 0 0 14824 252 0 0 0 0 +19 wlan0 0x0 10015 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +20 wlan0 0x0 10018 0 4928 11 1741 14 4928 11 0 0 0 0 1741 14 0 0 0 0 +21 wlan0 0x0 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 wlan0 0x0 10020 0 21163552 34395 2351650 15326 21162947 34390 605 5 0 0 2351045 15321 605 5 0 0 +23 wlan0 0x0 10020 1 13835740 12938 1548795 6365 13833754 12920 1986 18 0 0 1546809 6347 1986 18 0 0 +24 wlan0 0x0 10023 0 13405 40 5042 44 13405 40 0 0 0 0 5042 44 0 0 0 0 +25 wlan0 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 wlan0 0x0 10034 0 436394741 342648 6237981 80442 436394741 342648 0 0 0 0 6237981 80442 0 0 0 0 +27 wlan0 0x0 10034 1 64860872 51297 1335539 15546 64860872 51297 0 0 0 0 1335539 15546 0 0 0 0 +28 wlan0 0x0 10044 0 17614444 14774 521004 5694 17329882 14432 284562 342 0 0 419974 5408 101030 286 0 0 +29 wlan0 0x0 10044 1 17701 33 3100 28 17701 33 0 0 0 0 3100 28 0 0 0 0 +30 wlan0 0x0 10057 0 12312074 9339 436098 5450 12248060 9263 64014 76 0 0 414224 5388 21874 62 0 0 +31 wlan0 0x0 10057 1 1332953195 954797 31849632 457698 1331933207 953569 1019988 1228 0 0 31702284 456899 147348 799 0 0 +32 wlan0 0x0 10060 0 32972 200 433705 380 32972 200 0 0 0 0 433705 380 0 0 0 0 +33 wlan0 0x0 10060 1 32106 66 37789 87 32106 66 0 0 0 0 37789 87 0 0 0 0 +34 wlan0 0x0 10061 0 7675 23 2509 22 7675 23 0 0 0 0 2509 22 0 0 0 0 +35 wlan0 0x0 10061 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +36 wlan0 0x0 10074 0 38355 82 10447 97 38355 82 0 0 0 0 10447 97 0 0 0 0 +37 wlan0 0x0 10074 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +38 wlan0 0x0 10078 0 49013 79 7167 69 49013 79 0 0 0 0 7167 69 0 0 0 0 +39 wlan0 0x0 10078 1 5872 8 1236 10 5872 8 0 0 0 0 1236 10 0 0 0 0 +40 wlan0 0x0 10082 0 8301 13 1981 15 8301 13 0 0 0 0 1981 15 0 0 0 0 +41 wlan0 0x0 10082 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 wlan0 0x0 10086 0 7001 14 1579 15 7001 14 0 0 0 0 1579 15 0 0 0 0 +43 wlan0 0x0 10086 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +44 wlan0 0x0 10090 0 24327795 20224 920502 14661 24327795 20224 0 0 0 0 920502 14661 0 0 0 0 +45 wlan0 0x0 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +46 wlan0 0x0 10092 0 36849 78 12449 81 36849 78 0 0 0 0 12449 81 0 0 0 0 +47 wlan0 0x0 10092 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +48 wlan0 0x0 10095 0 131962 223 37069 241 131962 223 0 0 0 0 37069 241 0 0 0 0 +49 wlan0 0x0 10095 1 12949 21 3930 21 12949 21 0 0 0 0 3930 21 0 0 0 0 +50 wlan0 0x0 10106 0 30899554 22679 632476 12296 30895334 22645 4220 34 0 0 628256 12262 4220 34 0 0 +51 wlan0 0x0 10106 1 88923475 64963 1606962 35612 88917201 64886 3586 29 2688 48 1602032 35535 4930 77 0 0 +52 wlan0 0x40700000000 10020 0 705732 10589 404428 5504 705732 10589 0 0 0 0 404428 5504 0 0 0 0 +53 wlan0 0x40700000000 10020 1 2376 36 1296 18 2376 36 0 0 0 0 1296 18 0 0 0 0 +54 wlan0 0x40800000000 10020 0 34624 146 122525 160 34624 146 0 0 0 0 122525 160 0 0 0 0 +55 wlan0 0x40800000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +56 wlan0 0x40b00000000 10020 0 22411 85 7364 57 22411 85 0 0 0 0 7364 57 0 0 0 0 +57 wlan0 0x40b00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +58 wlan0 0x120300000000 10020 0 76641 241 32783 169 76641 241 0 0 0 0 32783 169 0 0 0 0 +59 wlan0 0x120300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +60 wlan0 0x130100000000 10020 0 73101 287 23236 203 73101 287 0 0 0 0 23236 203 0 0 0 0 +61 wlan0 0x130100000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +62 wlan0 0x180300000000 10020 0 330648 399 24736 232 330648 399 0 0 0 0 24736 232 0 0 0 0 +63 wlan0 0x180300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +64 wlan0 0x180400000000 10020 0 21865 59 5022 42 21865 59 0 0 0 0 5022 42 0 0 0 0 +65 wlan0 0x180400000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +66 wlan0 0x300000000000 10020 0 15984 65 26927 57 15984 65 0 0 0 0 26927 57 0 0 0 0 +67 wlan0 0x300000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +68 wlan0 0x1065fff00000000 10020 0 131871 599 93783 445 131871 599 0 0 0 0 93783 445 0 0 0 0 +69 wlan0 0x1065fff00000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +70 wlan0 0x1b24f4600000000 10034 0 15445 42 23329 45 15445 42 0 0 0 0 23329 45 0 0 0 0 +71 wlan0 0x1b24f4600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +72 wlan0 0x1000010000000000 10020 0 5542 9 1364 10 5542 9 0 0 0 0 1364 10 0 0 0 0 +73 wlan0 0x1000010000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +74 wlan0 0x1000040100000000 10020 0 47196 184 213319 257 47196 184 0 0 0 0 213319 257 0 0 0 0 +75 wlan0 0x1000040100000000 10020 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +76 wlan0 0x1000040700000000 10020 0 11599 50 10786 47 11599 50 0 0 0 0 10786 47 0 0 0 0 +77 wlan0 0x1000040700000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +78 wlan0 0x1000040800000000 10020 0 21902 145 174139 166 21902 145 0 0 0 0 174139 166 0 0 0 0 +79 wlan0 0x1000040800000000 10020 1 8568 88 105743 90 8568 88 0 0 0 0 105743 90 0 0 0 0 +80 wlan0 0x1000100300000000 10020 0 55213 118 194551 199 55213 118 0 0 0 0 194551 199 0 0 0 0 +81 wlan0 0x1000100300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +82 wlan0 0x1000120300000000 10020 0 50826 74 21153 70 50826 74 0 0 0 0 21153 70 0 0 0 0 +83 wlan0 0x1000120300000000 10020 1 72 1 175 2 72 1 0 0 0 0 175 2 0 0 0 0 +84 wlan0 0x1000180300000000 10020 0 744198 657 65437 592 744198 657 0 0 0 0 65437 592 0 0 0 0 +85 wlan0 0x1000180300000000 10020 1 144719 132 10989 108 144719 132 0 0 0 0 10989 108 0 0 0 0 +86 wlan0 0x1000180600000000 10020 0 4599 8 1928 10 4599 8 0 0 0 0 1928 10 0 0 0 0 +87 wlan0 0x1000180600000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +88 wlan0 0x1000250000000000 10020 0 57740 98 13076 88 57740 98 0 0 0 0 13076 88 0 0 0 0 +89 wlan0 0x1000250000000000 10020 1 328 3 414 4 207 2 121 1 0 0 293 3 121 1 0 0 +90 wlan0 0x1000300000000000 10020 0 7675 30 31331 32 7675 30 0 0 0 0 31331 32 0 0 0 0 +91 wlan0 0x1000300000000000 10020 1 30173 97 101335 100 30173 97 0 0 0 0 101335 100 0 0 0 0 +92 wlan0 0x1000310200000000 10020 0 1681 9 2194 9 1681 9 0 0 0 0 2194 9 0 0 0 0 +93 wlan0 0x1000310200000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +94 wlan0 0x1000360000000000 10020 0 5606 20 2831 20 5606 20 0 0 0 0 2831 20 0 0 0 0 +95 wlan0 0x1000360000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +96 wlan0 0x11065fff00000000 10020 0 18363 91 83367 104 18363 91 0 0 0 0 83367 104 0 0 0 0 +97 wlan0 0x11065fff00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +98 wlan0 0x3000009600000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +99 wlan0 0x3000009600000000 10020 1 6163 18 2424 18 6163 18 0 0 0 0 2424 18 0 0 0 0 +100 wlan0 0x3000009800000000 10020 0 23337 46 8723 39 23337 46 0 0 0 0 8723 39 0 0 0 0 +101 wlan0 0x3000009800000000 10020 1 33744 93 72437 89 33744 93 0 0 0 0 72437 89 0 0 0 0 +102 wlan0 0x3000020000000000 10020 0 4124 11 8969 19 4124 11 0 0 0 0 8969 19 0 0 0 0 +103 wlan0 0x3000020000000000 10020 1 5993 11 3815 14 5993 11 0 0 0 0 3815 14 0 0 0 0 +104 wlan0 0x3000040100000000 10020 0 113809 342 135666 308 113809 342 0 0 0 0 135666 308 0 0 0 0 +105 wlan0 0x3000040100000000 10020 1 142508 642 500579 637 142508 642 0 0 0 0 500579 637 0 0 0 0 +106 wlan0 0x3000040700000000 10020 0 365815 5119 213340 2733 365815 5119 0 0 0 0 213340 2733 0 0 0 0 +107 wlan0 0x3000040700000000 10020 1 30747 130 18408 100 30747 130 0 0 0 0 18408 100 0 0 0 0 +108 wlan0 0x3000040800000000 10020 0 34672 112 68623 92 34672 112 0 0 0 0 68623 92 0 0 0 0 +109 wlan0 0x3000040800000000 10020 1 78443 199 140944 192 78443 199 0 0 0 0 140944 192 0 0 0 0 +110 wlan0 0x3000040b00000000 10020 0 14949 33 4017 26 14949 33 0 0 0 0 4017 26 0 0 0 0 +111 wlan0 0x3000040b00000000 10020 1 996 15 576 8 996 15 0 0 0 0 576 8 0 0 0 0 +112 wlan0 0x3000090000000000 10020 0 11826 67 7309 52 11826 67 0 0 0 0 7309 52 0 0 0 0 +113 wlan0 0x3000090000000000 10020 1 24805 41 4785 41 24805 41 0 0 0 0 4785 41 0 0 0 0 +114 wlan0 0x3000100300000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +115 wlan0 0x3000100300000000 10020 1 3112 10 1628 10 3112 10 0 0 0 0 1628 10 0 0 0 0 +116 wlan0 0x3000120300000000 10020 0 38249 107 20374 85 38249 107 0 0 0 0 20374 85 0 0 0 0 +117 wlan0 0x3000120300000000 10020 1 122581 174 36792 143 122581 174 0 0 0 0 36792 143 0 0 0 0 +118 wlan0 0x3000130100000000 10020 0 2700 41 1524 21 2700 41 0 0 0 0 1524 21 0 0 0 0 +119 wlan0 0x3000130100000000 10020 1 22515 59 8366 52 22515 59 0 0 0 0 8366 52 0 0 0 0 +120 wlan0 0x3000180200000000 10020 0 6411 18 14511 20 6411 18 0 0 0 0 14511 20 0 0 0 0 +121 wlan0 0x3000180200000000 10020 1 336 5 319 4 336 5 0 0 0 0 319 4 0 0 0 0 +122 wlan0 0x3000180300000000 10020 0 129301 136 17622 97 129301 136 0 0 0 0 17622 97 0 0 0 0 +123 wlan0 0x3000180300000000 10020 1 464787 429 41703 336 464787 429 0 0 0 0 41703 336 0 0 0 0 +124 wlan0 0x3000180400000000 10020 0 11014 39 2787 25 11014 39 0 0 0 0 2787 25 0 0 0 0 +125 wlan0 0x3000180400000000 10020 1 144040 139 7540 80 144040 139 0 0 0 0 7540 80 0 0 0 0 +126 wlan0 0x3000210100000000 10020 0 10278 44 4579 33 10278 44 0 0 0 0 4579 33 0 0 0 0 +127 wlan0 0x3000210100000000 10020 1 31151 73 14159 47 31151 73 0 0 0 0 14159 47 0 0 0 0 +128 wlan0 0x3000250000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +129 wlan0 0x3000250000000000 10020 1 76614 143 17711 130 76080 137 534 6 0 0 17177 124 534 6 0 0 +130 wlan0 0x3000260100000000 10020 0 9426 26 3535 20 9426 26 0 0 0 0 3535 20 0 0 0 0 +131 wlan0 0x3000260100000000 10020 1 468 7 288 4 468 7 0 0 0 0 288 4 0 0 0 0 +132 wlan0 0x3000300000000000 10020 0 7241 29 12055 26 7241 29 0 0 0 0 12055 26 0 0 0 0 +133 wlan0 0x3000300000000000 10020 1 3273 23 11232 21 3273 23 0 0 0 0 11232 21 0 0 0 0 +134 wlan0 0x3000310000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +135 wlan0 0x3000310000000000 10020 1 53425 64 8721 62 53425 64 0 0 0 0 8721 62 0 0 0 0 +136 wlan0 0x3000310500000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +137 wlan0 0x3000310500000000 10020 1 9929 16 3879 18 9929 16 0 0 0 0 3879 18 0 0 0 0 +138 wlan0 0x3000320100000000 10020 0 6844 14 3745 13 6844 14 0 0 0 0 3745 13 0 0 0 0 +139 wlan0 0x3000320100000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +140 wlan0 0x3000360000000000 10020 0 8855 43 4749 31 8855 43 0 0 0 0 4749 31 0 0 0 0 +141 wlan0 0x3000360000000000 10020 1 5597 19 2456 19 5597 19 0 0 0 0 2456 19 0 0 0 0 +142 wlan0 0x3010000000000000 10090 0 605140 527 38435 429 605140 527 0 0 0 0 38435 429 0 0 0 0 +143 wlan0 0x3010000000000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +144 wlan0 0x31065fff00000000 10020 0 22011 67 29665 64 22011 67 0 0 0 0 29665 64 0 0 0 0 +145 wlan0 0x31065fff00000000 10020 1 10695 34 18347 35 10695 34 0 0 0 0 18347 35 0 0 0 0 +146 wlan0 0x32e544f900000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +147 wlan0 0x32e544f900000000 10034 1 40143 54 7299 61 40143 54 0 0 0 0 7299 61 0 0 0 0 +148 wlan0 0x58872a4400000000 10018 0 4928 11 1669 13 4928 11 0 0 0 0 1669 13 0 0 0 0 +149 wlan0 0x58872a4400000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +150 wlan0 0x5caeaa7b00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +151 wlan0 0x5caeaa7b00000000 10034 1 74971 73 7103 75 74971 73 0 0 0 0 7103 75 0 0 0 0 +152 wlan0 0x9e00923800000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +153 wlan0 0x9e00923800000000 10034 1 72385 98 13072 110 72385 98 0 0 0 0 13072 110 0 0 0 0 +154 wlan0 0xb972bdd400000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +155 wlan0 0xb972bdd400000000 10034 1 15282 24 3034 27 15282 24 0 0 0 0 3034 27 0 0 0 0 +156 wlan0 0xc7c9f7ba00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +157 wlan0 0xc7c9f7ba00000000 10034 1 194915 185 13316 138 194915 185 0 0 0 0 13316 138 0 0 0 0 +158 wlan0 0xc9395b2600000000 10034 0 6991 13 6215 14 6991 13 0 0 0 0 6215 14 0 0 0 0 +159 wlan0 0xc9395b2600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +160 wlan0 0xdaddf21100000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +161 wlan0 0xdaddf21100000000 10034 1 928676 849 81570 799 928676 849 0 0 0 0 81570 799 0 0 0 0 +162 wlan0 0xe8d195d100000000 10020 0 516 8 288 4 516 8 0 0 0 0 288 4 0 0 0 0 +163 wlan0 0xe8d195d100000000 10020 1 5905 15 2622 15 5905 15 0 0 0 0 2622 15 0 0 0 0 +164 wlan0 0xe8d195d100000000 10034 0 236640 524 312523 555 236640 524 0 0 0 0 312523 555 0 0 0 0 +165 wlan0 0xe8d195d100000000 10034 1 319028 539 188776 553 319028 539 0 0 0 0 188776 553 0 0 0 0 +166 wlan0 0xffffff0100000000 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +167 wlan0 0xffffff0100000000 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +168 wlan0 0xffffff0100000000 10020 0 17874405 14068 223987 3065 17874405 14068 0 0 0 0 223987 3065 0 0 0 0 +169 wlan0 0xffffff0100000000 10020 1 11011258 8672 177693 2407 11011258 8672 0 0 0 0 177693 2407 0 0 0 0 +170 wlan0 0xffffff0100000000 10034 0 436062595 341880 5843990 79630 436062595 341880 0 0 0 0 5843990 79630 0 0 0 0 +171 wlan0 0xffffff0100000000 10034 1 63201220 49447 1005882 13713 63201220 49447 0 0 0 0 1005882 13713 0 0 0 0 +172 wlan0 0xffffff0100000000 10044 0 17159287 13702 356212 4778 17159287 13702 0 0 0 0 356212 4778 0 0 0 0 +173 wlan0 0xffffff0100000000 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +174 wlan0 0xffffff0100000000 10078 0 10439 17 1665 15 10439 17 0 0 0 0 1665 15 0 0 0 0 +175 wlan0 0xffffff0100000000 10078 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +176 wlan0 0xffffff0100000000 10090 0 23722655 19697 881995 14231 23722655 19697 0 0 0 0 881995 14231 0 0 0 0 +177 wlan0 0xffffff0100000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +178 wlan0 0xffffff0500000000 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +179 wlan0 0xffffff0500000000 1000 1 1592 5 314 1 0 0 1592 5 0 0 0 0 314 1 0 0 +180 wlan0 0xffffff0600000000 1000 0 0 0 36960 385 0 0 0 0 0 0 0 0 36960 385 0 0 +181 wlan0 0xffffff0600000000 1000 1 96 1 480 5 0 0 96 1 0 0 0 0 480 5 0 0 +182 wlan0 0xffffff0700000000 1000 0 38732 229 16567 163 38732 229 0 0 0 0 16567 163 0 0 0 0 +183 wlan0 0xffffff0700000000 1000 1 18539 74 7562 66 18539 74 0 0 0 0 7562 66 0 0 0 0 +184 wlan0 0xffffff0900000000 1000 0 38381 43 2624 27 38381 43 0 0 0 0 2624 27 0 0 0 0 +185 wlan0 0xffffff0900000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +186 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3 +187 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +188 wlan0 0x0 1029 0 0 0 8524052 130894 0 0 0 0 0 0 7871216 121284 108568 1325 544268 8285 +189 wlan0 0x0 1029 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before new file mode 100644 index 0000000000000000000000000000000000000000..ce4bcc3a3b43bcb2c34e908acff88c65e566a412 --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat_100mb_download_before @@ -0,0 +1,187 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 r_rmnet_data0 0x0 0 0 0 0 392 6 0 0 0 0 0 0 0 0 0 0 392 6 +3 r_rmnet_data0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 v4-wlan0 0x0 0 0 58848 2070 2836 64 160 4 0 0 58688 2066 80 2 0 0 2756 62 +5 v4-wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +6 v4-wlan0 0x0 10034 0 6192 11 1445 11 6192 11 0 0 0 0 1445 11 0 0 0 0 +7 v4-wlan0 0x0 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +8 v4-wlan0 0x0 10057 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +9 v4-wlan0 0x0 10057 1 728 7 392 7 0 0 728 7 0 0 0 0 392 7 0 0 +10 v4-wlan0 0x0 10106 0 1488 12 1488 12 0 0 1488 12 0 0 0 0 1488 12 0 0 +11 v4-wlan0 0x0 10106 1 323981189 235142 3509032 84542 323979453 235128 1736 14 0 0 3502676 84363 6356 179 0 0 +12 wlan0 0x0 0 0 330187296 250652 0 0 329106990 236273 226202 1255 854104 13124 0 0 0 0 0 0 +13 wlan0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +14 wlan0 0x0 1000 0 77113 272 56151 575 77113 272 0 0 0 0 19191 190 36960 385 0 0 +15 wlan0 0x0 1000 1 20227 80 8356 72 18539 74 1688 6 0 0 7562 66 794 6 0 0 +16 wlan0 0x0 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +17 wlan0 0x0 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +18 wlan0 0x0 10015 0 4390 7 14824 252 4390 7 0 0 0 0 14824 252 0 0 0 0 +19 wlan0 0x0 10015 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +20 wlan0 0x0 10018 0 4928 11 1741 14 4928 11 0 0 0 0 1741 14 0 0 0 0 +21 wlan0 0x0 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +22 wlan0 0x0 10020 0 21141412 34316 2329881 15262 21140807 34311 605 5 0 0 2329276 15257 605 5 0 0 +23 wlan0 0x0 10020 1 13835740 12938 1548555 6362 13833754 12920 1986 18 0 0 1546569 6344 1986 18 0 0 +24 wlan0 0x0 10023 0 13405 40 5042 44 13405 40 0 0 0 0 5042 44 0 0 0 0 +25 wlan0 0x0 10023 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +26 wlan0 0x0 10034 0 436394741 342648 6237981 80442 436394741 342648 0 0 0 0 6237981 80442 0 0 0 0 +27 wlan0 0x0 10034 1 64860872 51297 1335539 15546 64860872 51297 0 0 0 0 1335539 15546 0 0 0 0 +28 wlan0 0x0 10044 0 17614444 14774 521004 5694 17329882 14432 284562 342 0 0 419974 5408 101030 286 0 0 +29 wlan0 0x0 10044 1 17701 33 3100 28 17701 33 0 0 0 0 3100 28 0 0 0 0 +30 wlan0 0x0 10057 0 12311735 9335 435954 5448 12247721 9259 64014 76 0 0 414080 5386 21874 62 0 0 +31 wlan0 0x0 10057 1 1332953195 954797 31849632 457698 1331933207 953569 1019988 1228 0 0 31702284 456899 147348 799 0 0 +32 wlan0 0x0 10060 0 32972 200 433705 380 32972 200 0 0 0 0 433705 380 0 0 0 0 +33 wlan0 0x0 10060 1 32106 66 37789 87 32106 66 0 0 0 0 37789 87 0 0 0 0 +34 wlan0 0x0 10061 0 7675 23 2509 22 7675 23 0 0 0 0 2509 22 0 0 0 0 +35 wlan0 0x0 10061 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +36 wlan0 0x0 10074 0 38355 82 10447 97 38355 82 0 0 0 0 10447 97 0 0 0 0 +37 wlan0 0x0 10074 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +38 wlan0 0x0 10078 0 49013 79 7167 69 49013 79 0 0 0 0 7167 69 0 0 0 0 +39 wlan0 0x0 10078 1 5872 8 1236 10 5872 8 0 0 0 0 1236 10 0 0 0 0 +40 wlan0 0x0 10082 0 8301 13 1981 15 8301 13 0 0 0 0 1981 15 0 0 0 0 +41 wlan0 0x0 10082 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +42 wlan0 0x0 10086 0 7001 14 1579 15 7001 14 0 0 0 0 1579 15 0 0 0 0 +43 wlan0 0x0 10086 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +44 wlan0 0x0 10090 0 24327795 20224 920502 14661 24327795 20224 0 0 0 0 920502 14661 0 0 0 0 +45 wlan0 0x0 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +46 wlan0 0x0 10092 0 36849 78 12449 81 36849 78 0 0 0 0 12449 81 0 0 0 0 +47 wlan0 0x0 10092 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +48 wlan0 0x0 10095 0 131962 223 37069 241 131962 223 0 0 0 0 37069 241 0 0 0 0 +49 wlan0 0x0 10095 1 12949 21 3930 21 12949 21 0 0 0 0 3930 21 0 0 0 0 +50 wlan0 0x0 10106 0 30899554 22679 632476 12296 30895334 22645 4220 34 0 0 628256 12262 4220 34 0 0 +51 wlan0 0x0 10106 1 88922349 64952 1605126 35599 88916075 64875 3586 29 2688 48 1600196 35522 4930 77 0 0 +52 wlan0 0x40700000000 10020 0 705732 10589 404428 5504 705732 10589 0 0 0 0 404428 5504 0 0 0 0 +53 wlan0 0x40700000000 10020 1 2376 36 1296 18 2376 36 0 0 0 0 1296 18 0 0 0 0 +54 wlan0 0x40800000000 10020 0 34624 146 122525 160 34624 146 0 0 0 0 122525 160 0 0 0 0 +55 wlan0 0x40800000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +56 wlan0 0x40b00000000 10020 0 22411 85 7364 57 22411 85 0 0 0 0 7364 57 0 0 0 0 +57 wlan0 0x40b00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +58 wlan0 0x120300000000 10020 0 76641 241 32783 169 76641 241 0 0 0 0 32783 169 0 0 0 0 +59 wlan0 0x120300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +60 wlan0 0x130100000000 10020 0 73101 287 23236 203 73101 287 0 0 0 0 23236 203 0 0 0 0 +61 wlan0 0x130100000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +62 wlan0 0x180300000000 10020 0 330648 399 24736 232 330648 399 0 0 0 0 24736 232 0 0 0 0 +63 wlan0 0x180300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +64 wlan0 0x180400000000 10020 0 21865 59 5022 42 21865 59 0 0 0 0 5022 42 0 0 0 0 +65 wlan0 0x180400000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +66 wlan0 0x300000000000 10020 0 15984 65 26927 57 15984 65 0 0 0 0 26927 57 0 0 0 0 +67 wlan0 0x300000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +68 wlan0 0x1065fff00000000 10020 0 131871 599 93783 445 131871 599 0 0 0 0 93783 445 0 0 0 0 +69 wlan0 0x1065fff00000000 10020 1 264 4 144 2 264 4 0 0 0 0 144 2 0 0 0 0 +70 wlan0 0x1b24f4600000000 10034 0 15445 42 23329 45 15445 42 0 0 0 0 23329 45 0 0 0 0 +71 wlan0 0x1b24f4600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +72 wlan0 0x1000010000000000 10020 0 5542 9 1364 10 5542 9 0 0 0 0 1364 10 0 0 0 0 +73 wlan0 0x1000010000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +74 wlan0 0x1000040100000000 10020 0 47196 184 213319 257 47196 184 0 0 0 0 213319 257 0 0 0 0 +75 wlan0 0x1000040100000000 10020 1 60 1 103 1 60 1 0 0 0 0 103 1 0 0 0 0 +76 wlan0 0x1000040700000000 10020 0 11599 50 10786 47 11599 50 0 0 0 0 10786 47 0 0 0 0 +77 wlan0 0x1000040700000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +78 wlan0 0x1000040800000000 10020 0 21902 145 174139 166 21902 145 0 0 0 0 174139 166 0 0 0 0 +79 wlan0 0x1000040800000000 10020 1 8568 88 105743 90 8568 88 0 0 0 0 105743 90 0 0 0 0 +80 wlan0 0x1000100300000000 10020 0 55213 118 194551 199 55213 118 0 0 0 0 194551 199 0 0 0 0 +81 wlan0 0x1000100300000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +82 wlan0 0x1000120300000000 10020 0 50826 74 21153 70 50826 74 0 0 0 0 21153 70 0 0 0 0 +83 wlan0 0x1000120300000000 10020 1 72 1 175 2 72 1 0 0 0 0 175 2 0 0 0 0 +84 wlan0 0x1000180300000000 10020 0 744198 657 65437 592 744198 657 0 0 0 0 65437 592 0 0 0 0 +85 wlan0 0x1000180300000000 10020 1 144719 132 10989 108 144719 132 0 0 0 0 10989 108 0 0 0 0 +86 wlan0 0x1000180600000000 10020 0 4599 8 1928 10 4599 8 0 0 0 0 1928 10 0 0 0 0 +87 wlan0 0x1000180600000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +88 wlan0 0x1000250000000000 10020 0 57740 98 13076 88 57740 98 0 0 0 0 13076 88 0 0 0 0 +89 wlan0 0x1000250000000000 10020 1 328 3 414 4 207 2 121 1 0 0 293 3 121 1 0 0 +90 wlan0 0x1000300000000000 10020 0 7675 30 31331 32 7675 30 0 0 0 0 31331 32 0 0 0 0 +91 wlan0 0x1000300000000000 10020 1 30173 97 101335 100 30173 97 0 0 0 0 101335 100 0 0 0 0 +92 wlan0 0x1000310200000000 10020 0 1681 9 2194 9 1681 9 0 0 0 0 2194 9 0 0 0 0 +93 wlan0 0x1000310200000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +94 wlan0 0x1000360000000000 10020 0 5606 20 2831 20 5606 20 0 0 0 0 2831 20 0 0 0 0 +95 wlan0 0x1000360000000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +96 wlan0 0x11065fff00000000 10020 0 18363 91 83367 104 18363 91 0 0 0 0 83367 104 0 0 0 0 +97 wlan0 0x11065fff00000000 10020 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +98 wlan0 0x3000009600000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +99 wlan0 0x3000009600000000 10020 1 6163 18 2424 18 6163 18 0 0 0 0 2424 18 0 0 0 0 +100 wlan0 0x3000009800000000 10020 0 23337 46 8723 39 23337 46 0 0 0 0 8723 39 0 0 0 0 +101 wlan0 0x3000009800000000 10020 1 33744 93 72437 89 33744 93 0 0 0 0 72437 89 0 0 0 0 +102 wlan0 0x3000020000000000 10020 0 4124 11 8969 19 4124 11 0 0 0 0 8969 19 0 0 0 0 +103 wlan0 0x3000020000000000 10020 1 5993 11 3815 14 5993 11 0 0 0 0 3815 14 0 0 0 0 +104 wlan0 0x3000040100000000 10020 0 106718 322 121557 287 106718 322 0 0 0 0 121557 287 0 0 0 0 +105 wlan0 0x3000040100000000 10020 1 142508 642 500579 637 142508 642 0 0 0 0 500579 637 0 0 0 0 +106 wlan0 0x3000040700000000 10020 0 365419 5113 213124 2730 365419 5113 0 0 0 0 213124 2730 0 0 0 0 +107 wlan0 0x3000040700000000 10020 1 30747 130 18408 100 30747 130 0 0 0 0 18408 100 0 0 0 0 +108 wlan0 0x3000040800000000 10020 0 34672 112 68623 92 34672 112 0 0 0 0 68623 92 0 0 0 0 +109 wlan0 0x3000040800000000 10020 1 78443 199 140944 192 78443 199 0 0 0 0 140944 192 0 0 0 0 +110 wlan0 0x3000040b00000000 10020 0 14949 33 4017 26 14949 33 0 0 0 0 4017 26 0 0 0 0 +111 wlan0 0x3000040b00000000 10020 1 996 15 576 8 996 15 0 0 0 0 576 8 0 0 0 0 +112 wlan0 0x3000090000000000 10020 0 4017 28 3610 25 4017 28 0 0 0 0 3610 25 0 0 0 0 +113 wlan0 0x3000090000000000 10020 1 24805 41 4545 38 24805 41 0 0 0 0 4545 38 0 0 0 0 +114 wlan0 0x3000100300000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +115 wlan0 0x3000100300000000 10020 1 3112 10 1628 10 3112 10 0 0 0 0 1628 10 0 0 0 0 +116 wlan0 0x3000120300000000 10020 0 38249 107 20374 85 38249 107 0 0 0 0 20374 85 0 0 0 0 +117 wlan0 0x3000120300000000 10020 1 122581 174 36792 143 122581 174 0 0 0 0 36792 143 0 0 0 0 +118 wlan0 0x3000130100000000 10020 0 2700 41 1524 21 2700 41 0 0 0 0 1524 21 0 0 0 0 +119 wlan0 0x3000130100000000 10020 1 22515 59 8366 52 22515 59 0 0 0 0 8366 52 0 0 0 0 +120 wlan0 0x3000180200000000 10020 0 6411 18 14511 20 6411 18 0 0 0 0 14511 20 0 0 0 0 +121 wlan0 0x3000180200000000 10020 1 336 5 319 4 336 5 0 0 0 0 319 4 0 0 0 0 +122 wlan0 0x3000180300000000 10020 0 129301 136 17622 97 129301 136 0 0 0 0 17622 97 0 0 0 0 +123 wlan0 0x3000180300000000 10020 1 464787 429 41703 336 464787 429 0 0 0 0 41703 336 0 0 0 0 +124 wlan0 0x3000180400000000 10020 0 11014 39 2787 25 11014 39 0 0 0 0 2787 25 0 0 0 0 +125 wlan0 0x3000180400000000 10020 1 144040 139 7540 80 144040 139 0 0 0 0 7540 80 0 0 0 0 +126 wlan0 0x3000210100000000 10020 0 10278 44 4579 33 10278 44 0 0 0 0 4579 33 0 0 0 0 +127 wlan0 0x3000210100000000 10020 1 31151 73 14159 47 31151 73 0 0 0 0 14159 47 0 0 0 0 +128 wlan0 0x3000250000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +129 wlan0 0x3000250000000000 10020 1 76614 143 17711 130 76080 137 534 6 0 0 17177 124 534 6 0 0 +130 wlan0 0x3000260100000000 10020 0 9426 26 3535 20 9426 26 0 0 0 0 3535 20 0 0 0 0 +131 wlan0 0x3000260100000000 10020 1 468 7 288 4 468 7 0 0 0 0 288 4 0 0 0 0 +132 wlan0 0x3000300000000000 10020 0 7241 29 12055 26 7241 29 0 0 0 0 12055 26 0 0 0 0 +133 wlan0 0x3000300000000000 10020 1 3273 23 11232 21 3273 23 0 0 0 0 11232 21 0 0 0 0 +134 wlan0 0x3000310000000000 10020 0 132 2 72 1 132 2 0 0 0 0 72 1 0 0 0 0 +135 wlan0 0x3000310000000000 10020 1 53425 64 8721 62 53425 64 0 0 0 0 8721 62 0 0 0 0 +136 wlan0 0x3000310500000000 10020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +137 wlan0 0x3000310500000000 10020 1 9929 16 3879 18 9929 16 0 0 0 0 3879 18 0 0 0 0 +138 wlan0 0x3000360000000000 10020 0 8855 43 4749 31 8855 43 0 0 0 0 4749 31 0 0 0 0 +139 wlan0 0x3000360000000000 10020 1 5597 19 2456 19 5597 19 0 0 0 0 2456 19 0 0 0 0 +140 wlan0 0x3010000000000000 10090 0 605140 527 38435 429 605140 527 0 0 0 0 38435 429 0 0 0 0 +141 wlan0 0x3010000000000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +142 wlan0 0x31065fff00000000 10020 0 22011 67 29665 64 22011 67 0 0 0 0 29665 64 0 0 0 0 +143 wlan0 0x31065fff00000000 10020 1 10695 34 18347 35 10695 34 0 0 0 0 18347 35 0 0 0 0 +144 wlan0 0x32e544f900000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +145 wlan0 0x32e544f900000000 10034 1 40143 54 7299 61 40143 54 0 0 0 0 7299 61 0 0 0 0 +146 wlan0 0x58872a4400000000 10018 0 4928 11 1669 13 4928 11 0 0 0 0 1669 13 0 0 0 0 +147 wlan0 0x58872a4400000000 10018 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +148 wlan0 0x5caeaa7b00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +149 wlan0 0x5caeaa7b00000000 10034 1 74971 73 7103 75 74971 73 0 0 0 0 7103 75 0 0 0 0 +150 wlan0 0x9e00923800000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +151 wlan0 0x9e00923800000000 10034 1 72385 98 13072 110 72385 98 0 0 0 0 13072 110 0 0 0 0 +152 wlan0 0xb972bdd400000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +153 wlan0 0xb972bdd400000000 10034 1 15282 24 3034 27 15282 24 0 0 0 0 3034 27 0 0 0 0 +154 wlan0 0xc7c9f7ba00000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +155 wlan0 0xc7c9f7ba00000000 10034 1 194915 185 13316 138 194915 185 0 0 0 0 13316 138 0 0 0 0 +156 wlan0 0xc9395b2600000000 10034 0 6991 13 6215 14 6991 13 0 0 0 0 6215 14 0 0 0 0 +157 wlan0 0xc9395b2600000000 10034 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +158 wlan0 0xdaddf21100000000 10034 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +159 wlan0 0xdaddf21100000000 10034 1 928676 849 81570 799 928676 849 0 0 0 0 81570 799 0 0 0 0 +160 wlan0 0xe8d195d100000000 10020 0 516 8 288 4 516 8 0 0 0 0 288 4 0 0 0 0 +161 wlan0 0xe8d195d100000000 10020 1 5905 15 2622 15 5905 15 0 0 0 0 2622 15 0 0 0 0 +162 wlan0 0xe8d195d100000000 10034 0 236640 524 312523 555 236640 524 0 0 0 0 312523 555 0 0 0 0 +163 wlan0 0xe8d195d100000000 10034 1 319028 539 188776 553 319028 539 0 0 0 0 188776 553 0 0 0 0 +164 wlan0 0xffffff0100000000 10006 0 80755 92 9122 99 80755 92 0 0 0 0 9122 99 0 0 0 0 +165 wlan0 0xffffff0100000000 10006 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +166 wlan0 0xffffff0100000000 10020 0 17874405 14068 223987 3065 17874405 14068 0 0 0 0 223987 3065 0 0 0 0 +167 wlan0 0xffffff0100000000 10020 1 11011258 8672 177693 2407 11011258 8672 0 0 0 0 177693 2407 0 0 0 0 +168 wlan0 0xffffff0100000000 10034 0 436062595 341880 5843990 79630 436062595 341880 0 0 0 0 5843990 79630 0 0 0 0 +169 wlan0 0xffffff0100000000 10034 1 63201220 49447 1005882 13713 63201220 49447 0 0 0 0 1005882 13713 0 0 0 0 +170 wlan0 0xffffff0100000000 10044 0 17159287 13702 356212 4778 17159287 13702 0 0 0 0 356212 4778 0 0 0 0 +171 wlan0 0xffffff0100000000 10044 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +172 wlan0 0xffffff0100000000 10078 0 10439 17 1665 15 10439 17 0 0 0 0 1665 15 0 0 0 0 +173 wlan0 0xffffff0100000000 10078 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +174 wlan0 0xffffff0100000000 10090 0 23722655 19697 881995 14231 23722655 19697 0 0 0 0 881995 14231 0 0 0 0 +175 wlan0 0xffffff0100000000 10090 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +176 wlan0 0xffffff0500000000 1000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +177 wlan0 0xffffff0500000000 1000 1 1592 5 314 1 0 0 1592 5 0 0 0 0 314 1 0 0 +178 wlan0 0xffffff0600000000 1000 0 0 0 36960 385 0 0 0 0 0 0 0 0 36960 385 0 0 +179 wlan0 0xffffff0600000000 1000 1 96 1 480 5 0 0 96 1 0 0 0 0 480 5 0 0 +180 wlan0 0xffffff0700000000 1000 0 38732 229 16567 163 38732 229 0 0 0 0 16567 163 0 0 0 0 +181 wlan0 0xffffff0700000000 1000 1 18539 74 7562 66 18539 74 0 0 0 0 7562 66 0 0 0 0 +182 wlan0 0xffffff0900000000 1000 0 38381 43 2624 27 38381 43 0 0 0 0 2624 27 0 0 0 0 +183 wlan0 0xffffff0900000000 1000 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +184 dummy0 0x0 0 0 0 0 168 3 0 0 0 0 0 0 0 0 0 0 168 3 +185 dummy0 0x0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +186 wlan0 0x0 1029 0 0 0 5855801 94173 0 0 0 0 0 0 5208040 84634 103637 1256 544124 8283 +187 wlan0 0x0 1029 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 diff --git a/tests/unit/res/raw/xt_qtaguid_with_clat_simple b/tests/unit/res/raw/xt_qtaguid_with_clat_simple new file mode 100644 index 0000000000000000000000000000000000000000..a1d6d411bad81ffc037599a27cc3fe86377712ea --- /dev/null +++ b/tests/unit/res/raw/xt_qtaguid_with_clat_simple @@ -0,0 +1,4 @@ +idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets +2 v4-wlan0 0x0 10060 0 42600 213 4100 41 42600 213 0 0 0 0 4100 41 0 0 0 0 +3 v4-wlan0 0x0 10060 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +4 wlan0 0x0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0