Merge "Revert "Prioritize non-slices over slices"" into main
diff --git a/DnsResolver/Android.bp b/DnsResolver/Android.bp
new file mode 100644
index 0000000..d133034
--- /dev/null
+++ b/DnsResolver/Android.bp
@@ -0,0 +1,83 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+ name: "libcom.android.tethering.dns_helper",
+ version_script: "libcom.android.tethering.dns_helper.map.txt",
+ stubs: {
+ versions: [
+ "1",
+ ],
+ symbol_file: "libcom.android.tethering.dns_helper.map.txt",
+ },
+ defaults: ["netd_defaults"],
+ header_libs: [
+ "bpf_connectivity_headers",
+ "libcutils_headers",
+ ],
+ srcs: [
+ "DnsBpfHelper.cpp",
+ "DnsHelper.cpp",
+ ],
+ static_libs: [
+ "libmodules-utils-build",
+ ],
+ shared_libs: [
+ "libbase",
+ ],
+ export_include_dirs: ["include"],
+ header_abi_checker: {
+ enabled: true,
+ symbol_file: "libcom.android.tethering.dns_helper.map.txt",
+ },
+ sanitize: {
+ cfi: true,
+ },
+ apex_available: ["com.android.tethering"],
+ min_sdk_version: "30",
+}
+
+cc_test {
+ name: "dns_helper_unit_test",
+ defaults: ["netd_defaults"],
+ test_suites: ["general-tests", "mts-tethering"],
+ test_config_template: ":net_native_test_config_template",
+ header_libs: [
+ "bpf_connectivity_headers",
+ ],
+ srcs: [
+ "DnsBpfHelperTest.cpp",
+ ],
+ static_libs: [
+ "libcom.android.tethering.dns_helper",
+ ],
+ shared_libs: [
+ "libbase",
+ "libcutils",
+ ],
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+}
diff --git a/DnsResolver/DnsBpfHelper.cpp b/DnsResolver/DnsBpfHelper.cpp
new file mode 100644
index 0000000..37c46ca
--- /dev/null
+++ b/DnsResolver/DnsBpfHelper.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "DnsBpfHelper"
+
+#include "DnsBpfHelper.h"
+
+#include <android-base/logging.h>
+#include <android-modules-utils/sdk_level.h>
+
+namespace android {
+namespace net {
+
+#define RETURN_IF_RESULT_NOT_OK(result) \
+ do { \
+ if (!result.ok()) { \
+ LOG(ERROR) << "L" << __LINE__ << " " << __func__ << ": " << strerror(result.error().code()); \
+ return result.error(); \
+ } \
+ } while (0)
+
+base::Result<void> DnsBpfHelper::init() {
+ if (!android::modules::sdklevel::IsAtLeastT()) {
+ LOG(ERROR) << __func__ << ": Unsupported before Android T.";
+ return base::Error(EOPNOTSUPP);
+ }
+
+ RETURN_IF_RESULT_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
+ RETURN_IF_RESULT_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
+ RETURN_IF_RESULT_NOT_OK(mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH));
+ return {};
+}
+
+base::Result<bool> DnsBpfHelper::isUidNetworkingBlocked(uid_t uid, bool metered) {
+ if (is_system_uid(uid)) return false;
+ if (!mConfigurationMap.isValid() || !mUidOwnerMap.isValid()) {
+ LOG(ERROR) << __func__
+ << ": BPF maps are not ready. Forgot to call ADnsHelper_init?";
+ return base::Error(EUNATCH);
+ }
+
+ auto enabledRules = mConfigurationMap.readValue(UID_RULES_CONFIGURATION_KEY);
+ RETURN_IF_RESULT_NOT_OK(enabledRules);
+
+ auto value = mUidOwnerMap.readValue(uid);
+ uint32_t uidRules = value.ok() ? value.value().rule : 0;
+
+ // For doze mode, battery saver, low power standby.
+ if (isBlockedByUidRules(enabledRules.value(), uidRules)) return true;
+
+ // For data saver.
+ if (!metered) return false;
+
+ // The background data setting (PENALTY_BOX_MATCH) and unrestricted data usage setting
+ // (HAPPY_BOX_MATCH) for individual apps override the system wide Data Saver setting.
+ if (uidRules & PENALTY_BOX_MATCH) return true;
+ if (uidRules & HAPPY_BOX_MATCH) return false;
+
+ auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
+ RETURN_IF_RESULT_NOT_OK(dataSaverSetting);
+ return dataSaverSetting.value();
+}
+
+} // namespace net
+} // namespace android
diff --git a/DnsResolver/DnsBpfHelper.h b/DnsResolver/DnsBpfHelper.h
new file mode 100644
index 0000000..f1c3992
--- /dev/null
+++ b/DnsResolver/DnsBpfHelper.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+
+#include "bpf/BpfMap.h"
+#include "netd.h"
+
+namespace android {
+namespace net {
+
+class DnsBpfHelper {
+ public:
+ DnsBpfHelper() = default;
+ DnsBpfHelper(const DnsBpfHelper&) = delete;
+ DnsBpfHelper& operator=(const DnsBpfHelper&) = delete;
+
+ base::Result<void> init();
+ base::Result<bool> isUidNetworkingBlocked(uid_t uid, bool metered);
+
+ private:
+ android::bpf::BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
+ android::bpf::BpfMapRO<uint32_t, UidOwnerValue> mUidOwnerMap;
+ android::bpf::BpfMapRO<uint32_t, bool> mDataSaverEnabledMap;
+
+ // For testing
+ friend class DnsBpfHelperTest;
+};
+
+} // namespace net
+} // namespace android
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/DnsResolver/DnsBpfHelperTest.cpp
new file mode 100644
index 0000000..67b5b95
--- /dev/null
+++ b/DnsResolver/DnsBpfHelperTest.cpp
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <private/android_filesystem_config.h>
+
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#include "DnsBpfHelper.h"
+
+using namespace android::bpf; // NOLINT(google-build-using-namespace): exempted
+
+namespace android {
+namespace net {
+
+constexpr int TEST_MAP_SIZE = 2;
+
+#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+
+class DnsBpfHelperTest : public ::testing::Test {
+ protected:
+ DnsBpfHelper mDnsBpfHelper;
+ BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
+ BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
+ BpfMap<uint32_t, bool> mFakeDataSaverEnabledMap;
+
+ void SetUp() {
+ mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
+ ASSERT_VALID(mFakeConfigurationMap);
+
+ mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+ ASSERT_VALID(mFakeUidOwnerMap);
+
+ mFakeDataSaverEnabledMap.resetMap(BPF_MAP_TYPE_ARRAY, DATA_SAVER_ENABLED_MAP_SIZE);
+ ASSERT_VALID(mFakeDataSaverEnabledMap);
+
+ mDnsBpfHelper.mConfigurationMap = mFakeConfigurationMap;
+ ASSERT_VALID(mDnsBpfHelper.mConfigurationMap);
+ mDnsBpfHelper.mUidOwnerMap = mFakeUidOwnerMap;
+ ASSERT_VALID(mDnsBpfHelper.mUidOwnerMap);
+ mDnsBpfHelper.mDataSaverEnabledMap = mFakeDataSaverEnabledMap;
+ ASSERT_VALID(mDnsBpfHelper.mDataSaverEnabledMap);
+ }
+
+ void ResetAllMaps() {
+ mDnsBpfHelper.mConfigurationMap.reset();
+ mDnsBpfHelper.mUidOwnerMap.reset();
+ mDnsBpfHelper.mDataSaverEnabledMap.reset();
+ }
+};
+
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked) {
+ struct TestConfig {
+ const uid_t uid;
+ const uint32_t enabledRules;
+ const uint32_t uidRules;
+ const int expectedResult;
+ std::string toString() const {
+ return fmt::format(
+ "uid: {}, enabledRules: {}, uidRules: {}, expectedResult: {}",
+ uid, enabledRules, uidRules, expectedResult);
+ }
+ } testConfigs[] = {
+ // clang-format off
+ // No rule enabled:
+ // uid, enabledRules, uidRules, expectedResult
+ {AID_APP_START, NO_MATCH, NO_MATCH, false},
+
+ // An allowlist rule:
+ {AID_APP_START, NO_MATCH, DOZABLE_MATCH, false},
+ {AID_APP_START, DOZABLE_MATCH, NO_MATCH, true},
+ {AID_APP_START, DOZABLE_MATCH, DOZABLE_MATCH, false},
+ // A denylist rule
+ {AID_APP_START, NO_MATCH, STANDBY_MATCH, false},
+ {AID_APP_START, STANDBY_MATCH, NO_MATCH, false},
+ {AID_APP_START, STANDBY_MATCH, STANDBY_MATCH, true},
+
+ // Multiple rules enabled:
+ // Match only part of the enabled allowlist rules.
+ {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH, true},
+ {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, POWERSAVE_MATCH, true},
+ // Match all of the enabled allowlist rules.
+ {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH|POWERSAVE_MATCH, false},
+ // Match allowlist.
+ {AID_APP_START, DOZABLE_MATCH|STANDBY_MATCH, DOZABLE_MATCH, false},
+ // Match no rule.
+ {AID_APP_START, DOZABLE_MATCH|STANDBY_MATCH, NO_MATCH, true},
+ {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, NO_MATCH, true},
+
+ // System UID: always unblocked.
+ {AID_SYSTEM, NO_MATCH, NO_MATCH, false},
+ {AID_SYSTEM, NO_MATCH, DOZABLE_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH, NO_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH, DOZABLE_MATCH, false},
+ {AID_SYSTEM, NO_MATCH, STANDBY_MATCH, false},
+ {AID_SYSTEM, STANDBY_MATCH, NO_MATCH, false},
+ {AID_SYSTEM, STANDBY_MATCH, STANDBY_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH|POWERSAVE_MATCH, POWERSAVE_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH|POWERSAVE_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH|STANDBY_MATCH, DOZABLE_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH|STANDBY_MATCH, NO_MATCH, false},
+ {AID_SYSTEM, DOZABLE_MATCH|POWERSAVE_MATCH, NO_MATCH, false},
+ // clang-format on
+ };
+
+ for (const auto& config : testConfigs) {
+ SCOPED_TRACE(config.toString());
+
+ // Setup maps.
+ EXPECT_RESULT_OK(mFakeConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY,
+ config.enabledRules, BPF_EXIST));
+ EXPECT_RESULT_OK(mFakeUidOwnerMap.writeValue(config.uid, {.iif = 0, .rule = config.uidRules},
+ BPF_ANY));
+
+ // Verify the function.
+ auto result = mDnsBpfHelper.isUidNetworkingBlocked(config.uid, /*metered=*/false);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(config.expectedResult, result.value());
+ }
+}
+
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked_uninitialized) {
+ ResetAllMaps();
+
+ auto result = mDnsBpfHelper.isUidNetworkingBlocked(AID_APP_START, /*metered=*/false);
+ EXPECT_FALSE(result.ok());
+ EXPECT_EQ(EUNATCH, result.error().code());
+
+ result = mDnsBpfHelper.isUidNetworkingBlocked(AID_SYSTEM, /*metered=*/false);
+ EXPECT_TRUE(result.ok());
+ EXPECT_FALSE(result.value());
+}
+
+// Verify DataSaver on metered network.
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked_metered) {
+ struct TestConfig {
+ const uint32_t enabledRules; // Settings in configuration map.
+ const bool dataSaverEnabled; // Settings in data saver enabled map.
+ const uint32_t uidRules; // Settings in uid owner map.
+ const int blocked; // Whether the UID is expected to be networking blocked or not.
+ std::string toString() const {
+ return fmt::format(
+ ", enabledRules: {}, dataSaverEnabled: {}, uidRules: {}, expect blocked: {}",
+ enabledRules, dataSaverEnabled, uidRules, blocked);
+ }
+ } testConfigs[]{
+ // clang-format off
+ // enabledRules, dataSaverEnabled, uidRules, blocked
+ {NO_MATCH, false, NO_MATCH, false},
+ {NO_MATCH, false, PENALTY_BOX_MATCH, true},
+ {NO_MATCH, false, HAPPY_BOX_MATCH, false},
+ {NO_MATCH, false, PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+ {NO_MATCH, true, NO_MATCH, true},
+ {NO_MATCH, true, PENALTY_BOX_MATCH, true},
+ {NO_MATCH, true, HAPPY_BOX_MATCH, false},
+ {NO_MATCH, true, PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+ {STANDBY_MATCH, false, STANDBY_MATCH, true},
+ {STANDBY_MATCH, false, STANDBY_MATCH|PENALTY_BOX_MATCH, true},
+ {STANDBY_MATCH, false, STANDBY_MATCH|HAPPY_BOX_MATCH, true},
+ {STANDBY_MATCH, false, STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+ {STANDBY_MATCH, true, STANDBY_MATCH, true},
+ {STANDBY_MATCH, true, STANDBY_MATCH|PENALTY_BOX_MATCH, true},
+ {STANDBY_MATCH, true, STANDBY_MATCH|HAPPY_BOX_MATCH, true},
+ {STANDBY_MATCH, true, STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+ // clang-format on
+ };
+
+ for (const auto& config : testConfigs) {
+ SCOPED_TRACE(config.toString());
+
+ // Setup maps.
+ EXPECT_RESULT_OK(mFakeConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY,
+ config.enabledRules, BPF_EXIST));
+ EXPECT_RESULT_OK(mFakeDataSaverEnabledMap.writeValue(DATA_SAVER_ENABLED_KEY,
+ config.dataSaverEnabled, BPF_EXIST));
+ EXPECT_RESULT_OK(mFakeUidOwnerMap.writeValue(AID_APP_START, {.iif = 0, .rule = config.uidRules},
+ BPF_ANY));
+
+ // Verify the function.
+ auto result = mDnsBpfHelper.isUidNetworkingBlocked(AID_APP_START, /*metered=*/true);
+ EXPECT_RESULT_OK(result);
+ EXPECT_EQ(config.blocked, result.value());
+ }
+}
+
+} // namespace net
+} // namespace android
diff --git a/DnsResolver/DnsHelper.cpp b/DnsResolver/DnsHelper.cpp
new file mode 100644
index 0000000..3372908
--- /dev/null
+++ b/DnsResolver/DnsHelper.cpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <errno.h>
+
+#include "DnsBpfHelper.h"
+#include "DnsHelperPublic.h"
+
+static android::net::DnsBpfHelper sDnsBpfHelper;
+
+int ADnsHelper_init() {
+ auto result = sDnsBpfHelper.init();
+ if (!result.ok()) return -result.error().code();
+
+ return 0;
+}
+
+int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered) {
+ auto result = sDnsBpfHelper.isUidNetworkingBlocked(uid, metered);
+ if (!result.ok()) return -result.error().code();
+
+ // bool -> int conversion.
+ return result.value();
+}
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
new file mode 100644
index 0000000..7c9fc9e
--- /dev/null
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+__BEGIN_DECLS
+
+/*
+ * Perform any required initialization - including opening any required BPF maps. This function
+ * needs to be called before using other functions of this library.
+ *
+ * Returns 0 on success, a negative POSIX error code (see errno.h) on other failures.
+ */
+int ADnsHelper_init();
+
+/*
+ * The function reads bpf maps and returns whether the given uid has blocked networking or not. The
+ * function is supported starting from Android T.
+ *
+ * |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID.
+ * |metered| indicates whether the uid is currently using a billing network.
+ *
+ * Returns 0(false)/1(true) on success, a negative POSIX error code (see errno.h) on other failures.
+ */
+int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered);
+
+__END_DECLS
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/DnsResolver/libcom.android.tethering.dns_helper.map.txt
new file mode 100644
index 0000000..3c965a2
--- /dev/null
+++ b/DnsResolver/libcom.android.tethering.dns_helper.map.txt
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# This lists the entry points visible to applications that use the
+# libcom.android.tethering.dns_helper library. Other entry points present in
+# the library won't be usable.
+
+LIBCOM_ANDROID_TETHERING_DNS_HELPER {
+ global:
+ ADnsHelper_init; # apex
+ ADnsHelper_isUidNetworkingBlocked; # apex
+ local:
+ *;
+};
diff --git a/OWNERS b/OWNERS
index 649efda..b2176cc 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 31808
set noparent
file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
diff --git a/TEST_MAPPING b/TEST_MAPPING
index fafd3bb..ab3ed66 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -5,6 +5,9 @@
"options": [
{
"exclude-annotation": "com.android.testutils.NetworkStackModuleTest"
+ },
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
}
]
},
@@ -97,9 +100,6 @@
"name": "TetheringIntegrationTests"
},
{
- "name": "traffic_controller_unit_test"
- },
- {
"name": "libnetworkstats_test"
},
{
@@ -128,8 +128,7 @@
"keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
},
{
- "name": "traffic_controller_unit_test",
- "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
+ "name": "dns_helper_unit_test"
},
{
"name": "FrameworksNetDeflakeTest"
@@ -260,10 +259,12 @@
"name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
- "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
- },
- {
- "name": "traffic_controller_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+ "name": "ConnectivityCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ }
+ ]
},
{
"name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 4478b1e..414e50a 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -81,7 +81,10 @@
"framework-tethering.impl",
],
manifest: "AndroidManifestBase.xml",
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
// build tethering static library, used to compile both variants of the tethering.
@@ -95,6 +98,7 @@
],
static_libs: [
"NetworkStackApiCurrentShims",
+ "net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
lint: { strict_updatability_linting: true },
@@ -109,6 +113,7 @@
],
static_libs: [
"NetworkStackApiStableShims",
+ "net-utils-device-common-struct",
],
apex_available: ["com.android.tethering"],
lint: { strict_updatability_linting: true },
@@ -213,7 +218,10 @@
use_embedded_native_libs: true,
privapp_allowlist: ":privapp_allowlist_com.android.tethering",
apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
sdk {
@@ -224,6 +232,7 @@
"com.android.tethering",
],
native_shared_libs: [
+ "libcom.android.tethering.dns_helper",
"libnetd_updatable",
],
}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 9757daa..de9017a 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -80,9 +80,11 @@
first: {
jni_libs: [
"libservice-connectivity",
+ "libservice-thread-jni",
"libandroid_net_connectivity_com_android_net_module_util_jni",
],
native_shared_libs: [
+ "libcom.android.tethering.dns_helper",
"libcom.android.tethering.connectivity_native",
"libnetd_updatable",
],
@@ -96,6 +98,8 @@
},
binaries: [
"clatd",
+ "ethtool",
+ "netbpfload",
"ot-daemon",
],
canned_fs_config: "canned_fs_config",
diff --git a/Tethering/apex/canned_fs_config b/Tethering/apex/canned_fs_config
index 5a03347..1f5fcfa 100644
--- a/Tethering/apex/canned_fs_config
+++ b/Tethering/apex/canned_fs_config
@@ -1,2 +1,3 @@
/bin/for-system 0 1000 0750
/bin/for-system/clatd 1029 1029 06755
+/bin/netbpfload 0 0 0750
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index a280046..4d1e7ef 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -167,7 +167,6 @@
@Override
public boolean addIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
- if (!isInitialized()) return false;
// RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
if (rule.sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
@@ -185,7 +184,6 @@
@Override
public boolean removeIpv6UpstreamRule(@NonNull final Ipv6UpstreamRule rule) {
- if (!isInitialized()) return false;
// RFC7421_PREFIX_LENGTH = 64 which is the most commonly used IPv6 subnet prefix length.
if (rule.sourcePrefix.getPrefixLength() != RFC7421_PREFIX_LENGTH) return false;
@@ -200,8 +198,6 @@
@Override
public boolean addIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
- if (!isInitialized()) return false;
-
final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
final Tether6Value value = rule.makeTether6Value();
@@ -217,8 +213,6 @@
@Override
public boolean removeIpv6DownstreamRule(@NonNull final Ipv6DownstreamRule rule) {
- if (!isInitialized()) return false;
-
try {
mBpfDownstream6Map.deleteEntry(rule.makeTetherDownstream6Key());
} catch (ErrnoException e) {
@@ -234,8 +228,6 @@
@Override
@Nullable
public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
- if (!isInitialized()) return null;
-
final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
try {
// The reported tether stats are total data usage for all currently-active upstream
@@ -250,8 +242,6 @@
@Override
public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
- if (!isInitialized()) return false;
-
// The common case is an update, where the stats already exist,
// hence we read first, even though writing with BPF_NOEXIST
// first would make the code simpler.
@@ -307,8 +297,6 @@
@Override
@Nullable
public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
- if (!isInitialized()) return null;
-
// getAndClearTetherOffloadStats is called after all offload rules have already been
// deleted for the given upstream interface. Before starting to do cleanup stuff in this
// function, use synchronizeKernelRCU to make sure that all the current running eBPF
@@ -354,8 +342,6 @@
@Override
public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
@NonNull Tether4Value value) {
- if (!isInitialized()) return false;
-
try {
if (downstream) {
mBpfDownstream4Map.insertEntry(key, value);
@@ -379,8 +365,6 @@
@Override
public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
- if (!isInitialized()) return false;
-
try {
if (downstream) {
if (!mBpfDownstream4Map.deleteEntry(key)) return false; // Rule did not exist
@@ -413,8 +397,6 @@
@Override
public void tetherOffloadRuleForEach(boolean downstream,
@NonNull ThrowingBiConsumer<Tether4Key, Tether4Value> action) {
- if (!isInitialized()) return;
-
try {
if (downstream) {
mBpfDownstream4Map.forEach(action);
@@ -428,8 +410,6 @@
@Override
public boolean attachProgram(String iface, boolean downstream, boolean ipv4) {
- if (!isInitialized()) return false;
-
try {
BpfUtils.attachProgram(iface, downstream, ipv4);
} catch (IOException e) {
@@ -441,8 +421,6 @@
@Override
public boolean detachProgram(String iface, boolean ipv4) {
- if (!isInitialized()) return false;
-
try {
BpfUtils.detachProgram(iface, ipv4);
} catch (IOException e) {
@@ -460,8 +438,6 @@
@Override
public boolean addDevMap(int ifIndex) {
- if (!isInitialized()) return false;
-
try {
mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex));
} catch (ErrnoException e) {
@@ -473,8 +449,6 @@
@Override
public boolean removeDevMap(int ifIndex) {
- if (!isInitialized()) return false;
-
try {
mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex));
} catch (ErrnoException e) {
diff --git a/Tethering/common/TetheringLib/api/lint-baseline.txt b/Tethering/common/TetheringLib/api/lint-baseline.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/lint-baseline.txt
diff --git a/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt b/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..1d09598
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/module-lib-lint-baseline.txt
@@ -0,0 +1,23 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.TetheringManager#requestLatestTetheringEntitlementResult(int, boolean, java.util.concurrent.Executor, android.net.TetheringManager.OnTetheringEntitlementResultListener):
+ Method 'requestLatestTetheringEntitlementResult' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(android.net.TetheringManager.TetheringRequest, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+ Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(int, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+ Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopAllTethering():
+ Method 'stopAllTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopTethering(int):
+ Method 'stopTethering' documentation mentions permissions already declared by @RequiresPermission
+
+
+SdkConstant: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+
+
+Todo: android.net.TetheringConstants:
+ Documentation mentions 'TODO'
diff --git a/Tethering/common/TetheringLib/api/system-lint-baseline.txt b/Tethering/common/TetheringLib/api/system-lint-baseline.txt
new file mode 100644
index 0000000..e678ce1
--- /dev/null
+++ b/Tethering/common/TetheringLib/api/system-lint-baseline.txt
@@ -0,0 +1,17 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.TetheringManager#requestLatestTetheringEntitlementResult(int, boolean, java.util.concurrent.Executor, android.net.TetheringManager.OnTetheringEntitlementResultListener):
+ Method 'requestLatestTetheringEntitlementResult' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#startTethering(android.net.TetheringManager.TetheringRequest, java.util.concurrent.Executor, android.net.TetheringManager.StartTetheringCallback):
+ Method 'startTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopAllTethering():
+ Method 'stopAllTethering' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.TetheringManager#stopTethering(int):
+ Method 'stopTethering' documentation mentions permissions already declared by @RequiresPermission
+
+
+SdkConstant: android.net.TetheringManager#ACTION_TETHER_STATE_CHANGED:
+ Field 'ACTION_TETHER_STATE_CHANGED' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp
index ed80128..fd40d41 100644
--- a/Tethering/jni/onload.cpp
+++ b/Tethering/jni/onload.cpp
@@ -25,7 +25,6 @@
int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name);
int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name);
int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env);
-int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env);
int register_com_android_networkstack_tethering_util_TetheringUtils(JNIEnv* env);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
diff --git a/Tethering/lint-baseline.xml b/Tethering/lint-baseline.xml
new file mode 100644
index 0000000..37511c6
--- /dev/null
+++ b/Tethering/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.netstats.provider.NetworkStatsProvider#notifyWarningReached`"
+ errorLine1=" mStatsProvider.notifyWarningReached();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/Tethering/src/com/android/networkstack/tethering/OffloadController.java"
+ line="293"
+ column="44"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index a851410..c065cd6 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -33,6 +33,7 @@
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM;
import static com.android.networkstack.tethering.UpstreamNetworkState.isVcnInterface;
import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER;
@@ -44,6 +45,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetheredClient;
import android.net.TetheringManager;
import android.net.TetheringRequestParcel;
@@ -55,7 +57,6 @@
import android.net.dhcp.IDhcpServer;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
import android.os.Handler;
-import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -65,12 +66,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.InterfaceController;
import com.android.net.module.util.ip.IpNeighborMonitor;
@@ -83,12 +85,15 @@
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.networkstack.tethering.util.PrefixUtils;
+import com.android.networkstack.tethering.util.StateMachineShim;
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -102,7 +107,7 @@
*
* @hide
*/
-public class IpServer extends StateMachine {
+public class IpServer extends StateMachineShim {
public static final int STATE_UNAVAILABLE = 0;
public static final int STATE_AVAILABLE = 1;
public static final int STATE_TETHERED = 2;
@@ -127,6 +132,7 @@
// TODO: have this configurable
private static final int DHCP_LEASE_TIME_SECS = 3600;
+ private static final int NO_UPSTREAM = 0;
private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString("00:00:00:00:00:00");
private static final String TAG = "IpServer";
@@ -233,6 +239,7 @@
public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 12;
// request from PrivateAddressCoordinator to restart tethering.
public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 13;
+ public static final int CMD_SERVICE_FAILED_TO_START = BASE_IPSERVER + 14;
private final State mInitialState;
private final State mLocalHotspotState;
@@ -244,6 +251,11 @@
private final INetd mNetd;
@NonNull
private final BpfCoordinator mBpfCoordinator;
+ // Contains null if the connectivity module is unsupported, as the routing coordinator is not
+ // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
+ // must be able to find all classes at runtime.
+ @NonNull
+ private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
private final Callback mCallback;
private final InterfaceController mInterfaceCtrl;
private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -259,6 +271,12 @@
private int mLastError;
private int mServingMode;
private InterfaceSet mUpstreamIfaceSet; // may change over time
+ // mInterfaceParams can't be final now because IpServer will be created when receives
+ // WIFI_AP_STATE_CHANGED broadcasts or when it detects that the wifi interface has come up.
+ // In the latter case, the interface is not fully initialized and the MAC address might not
+ // be correct (it will be set with a randomized MAC address later).
+ // TODO: Consider create the IpServer only when tethering want to enable it, then we can
+ // make mInterfaceParams final.
private InterfaceParams mInterfaceParams;
// TODO: De-duplicate this with mLinkProperties above. Currently, these link
// properties are those selected by the IPv6TetheringCoordinator and relayed
@@ -283,6 +301,8 @@
private int mLastIPv6UpstreamIfindex = 0;
private boolean mUpstreamSupportsBpf = false;
+ @NonNull
+ private Set<IpPrefix> mLastIPv6UpstreamPrefixes = Collections.emptySet();
private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer {
public void accept(NeighborEvent e) {
@@ -295,18 +315,22 @@
private LinkAddress mIpv4Address;
private final TetheringMetrics mTetheringMetrics;
+ private final Handler mHandler;
// TODO: Add a dependency object to pass the data members or variables from the tethering
// object. It helps to reduce the arguments of the constructor.
public IpServer(
- String ifaceName, Looper looper, int interfaceType, SharedLog log,
- INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
+ String ifaceName, Handler handler, int interfaceType, SharedLog log,
+ INetd netd, @NonNull BpfCoordinator bpfCoordinator,
+ @Nullable LateSdk<RoutingCoordinatorManager> routingCoordinator, Callback callback,
TetheringConfiguration config, PrivateAddressCoordinator addressCoordinator,
TetheringMetrics tetheringMetrics, Dependencies deps) {
- super(ifaceName, looper);
+ super(ifaceName, USE_SYNC_SM ? null : handler.getLooper());
+ mHandler = handler;
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
- mBpfCoordinator = coordinator;
+ mBpfCoordinator = bpfCoordinator;
+ mRoutingCoordinator = routingCoordinator;
mCallback = callback;
mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
mIfaceName = ifaceName;
@@ -335,13 +359,22 @@
mTetheredState = new TetheredState();
mUnavailableState = new UnavailableState();
mWaitingForRestartState = new WaitingForRestartState();
- addState(mInitialState);
- addState(mLocalHotspotState);
- addState(mTetheredState);
- addState(mWaitingForRestartState, mTetheredState);
- addState(mUnavailableState);
+ final ArrayList allStates = new ArrayList<StateInfo>();
+ allStates.add(new StateInfo(mInitialState, null));
+ allStates.add(new StateInfo(mLocalHotspotState, null));
+ allStates.add(new StateInfo(mTetheredState, null));
+ allStates.add(new StateInfo(mWaitingForRestartState, mTetheredState));
+ allStates.add(new StateInfo(mUnavailableState, null));
+ addAllStates(allStates);
+ }
- setInitialState(mInitialState);
+ private Handler getHandler() {
+ return mHandler;
+ }
+
+ /** Start IpServer state machine. */
+ public void start() {
+ start(mInitialState);
}
/** Interface name which IpServer served.*/
@@ -482,7 +515,12 @@
private void handleError() {
mLastError = TETHER_ERROR_DHCPSERVER_ERROR;
- transitionTo(mInitialState);
+ if (USE_SYNC_SM) {
+ sendMessage(CMD_SERVICE_FAILED_TO_START, TETHER_ERROR_DHCPSERVER_ERROR);
+ } else {
+ sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START,
+ TETHER_ERROR_DHCPSERVER_ERROR);
+ }
}
}
@@ -740,7 +778,7 @@
RaParams params = null;
String upstreamIface = null;
InterfaceParams upstreamIfaceParams = null;
- int upstreamIfIndex = 0;
+ int upstreamIfIndex = NO_UPSTREAM;
if (v6only != null) {
upstreamIface = v6only.getInterfaceName();
@@ -754,13 +792,8 @@
if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment);
- for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
- if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
-
- final IpPrefix prefix = new IpPrefix(
- linkAddr.getAddress(), linkAddr.getPrefixLength());
- params.prefixes.add(prefix);
-
+ params.prefixes = getTetherableIpv6Prefixes(v6only);
+ for (IpPrefix prefix : params.prefixes) {
final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
if (dnsServer != null) {
params.dnses.add(dnsServer);
@@ -772,7 +805,7 @@
// CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential
// timing issue. It prevents that the IPv6 capability is updated later than
// CMD_TETHER_CONNECTION_CHANGED.
- mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
+ mBpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfIndex, upstreamIface);
// If v6only is null, we pass in null to setRaParams(), which handles
// deprecation of any existing RA data.
@@ -780,10 +813,12 @@
// Not support BPF on virtual upstream interface
final boolean upstreamSupportsBpf = upstreamIface != null && !isVcnInterface(upstreamIface);
- updateIpv6ForwardingRules(
- mLastIPv6UpstreamIfindex, upstreamIfIndex, upstreamSupportsBpf, null);
+ final Set<IpPrefix> upstreamPrefixes = params != null ? params.prefixes : Set.of();
+ updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamPrefixes,
+ upstreamIfIndex, upstreamPrefixes, upstreamSupportsBpf);
mLastIPv6LinkProperties = v6only;
mLastIPv6UpstreamIfindex = upstreamIfIndex;
+ mLastIPv6UpstreamPrefixes = upstreamPrefixes;
mUpstreamSupportsBpf = upstreamSupportsBpf;
if (mDadProxy != null) {
mDadProxy.setUpstreamIface(upstreamIfaceParams);
@@ -801,21 +836,62 @@
for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
}
- private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ private void addInterfaceToNetwork(final int netId, @NonNull final String ifaceName) {
try {
- // It's safe to call networkAddInterface() even if
- // the interface is already in the local_network.
- mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName);
- try {
- // Add routes from local network. Note that adding routes that
- // already exist does not cause an error (EEXIST is silently ignored).
- NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
- } catch (IllegalStateException e) {
- mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
- return;
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
+ // TODO : remove this call in favor of using the LocalNetworkConfiguration
+ // correctly, which will let ConnectivityService do it automatically.
+ mRoutingCoordinator.value.addInterfaceToNetwork(netId, ifaceName);
+ } else {
+ mNetd.networkAddInterface(netId, ifaceName);
}
} catch (ServiceSpecificException | RemoteException e) {
mLog.e("Failed to add " + mIfaceName + " to local table: ", e);
+ }
+ }
+
+ private void addInterfaceForward(@NonNull final String fromIface,
+ @NonNull final String toIface) throws ServiceSpecificException, RemoteException {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
+ mRoutingCoordinator.value.addInterfaceForward(fromIface, toIface);
+ } else {
+ mNetd.tetherAddForward(fromIface, toIface);
+ mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
+ }
+ }
+
+ private void removeInterfaceForward(@NonNull final String fromIface,
+ @NonNull final String toIface) {
+ if (SdkLevel.isAtLeastS() && null != mRoutingCoordinator.value) {
+ try {
+ mRoutingCoordinator.value.removeInterfaceForward(fromIface, toIface);
+ } catch (ServiceSpecificException e) {
+ mLog.e("Exception in removeInterfaceForward", e);
+ }
+ } else {
+ try {
+ mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception in ipfwdRemoveInterfaceForward", e);
+ }
+ try {
+ mNetd.tetherRemoveForward(fromIface, toIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception in disableNat", e);
+ }
+ }
+ }
+
+ private void addRoutesToLocalNetwork(@NonNull final List<RouteInfo> toBeAdded) {
+ // It's safe to call addInterfaceToNetwork() even if
+ // the interface is already in the local_network.
+ addInterfaceToNetwork(INetd.LOCAL_NET_ID, mIfaceName);
+ try {
+ // Add routes from local network. Note that adding routes that
+ // already exist does not cause an error (EEXIST is silently ignored).
+ NetdUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded);
+ } catch (IllegalStateException e) {
+ mLog.e("Failed to add IPv4/v6 routes to local table: " + e);
return;
}
@@ -887,25 +963,26 @@
}
}
- // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream
- // changes or if a neighbor event is received.
- private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex,
- boolean upstreamSupportsBpf, NeighborEvent e) {
- // If no longer have an upstream or upstream not supports BPF, clear forwarding rules and do
- // nothing else.
- // TODO: Rather than always clear rules, ensure whether ipv6 ever enable first.
- if (upstreamIfindex == 0 || !upstreamSupportsBpf) {
- mBpfCoordinator.tetherOffloadRuleClear(this);
- return;
- }
+ private int getInterfaceIndexForRule(int ifindex, boolean supportsBpf) {
+ return supportsBpf ? ifindex : NO_UPSTREAM;
+ }
+ // Handles updates to IPv6 forwarding rules if the upstream or its prefixes change.
+ private void updateIpv6ForwardingRules(int prevUpstreamIfindex,
+ @NonNull Set<IpPrefix> prevUpstreamPrefixes, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes, boolean upstreamSupportsBpf) {
// If the upstream interface has changed, remove all rules and re-add them with the new
- // upstream interface.
- if (prevUpstreamIfindex != upstreamIfindex) {
- mBpfCoordinator.tetherOffloadRuleUpdate(this, upstreamIfindex);
+ // upstream interface. If upstream is a virtual network, treated as no upstream.
+ if (prevUpstreamIfindex != upstreamIfindex
+ || !prevUpstreamPrefixes.equals(upstreamPrefixes)) {
+ mBpfCoordinator.updateAllIpv6Rules(this, this.mInterfaceParams,
+ getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf),
+ upstreamPrefixes);
}
+ }
- // If we're here to process a NeighborEvent, do so now.
+ // Handles updates to IPv6 downstream rules if a neighbor event is received.
+ private void addOrRemoveIpv6Downstream(NeighborEvent e) {
// mInterfaceParams must be non-null or the event would not have arrived.
if (e == null) return;
if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress()
@@ -917,8 +994,9 @@
// Do this here instead of in the Ipv6DownstreamRule constructor to ensure that we
// never add rules with a null MAC, only delete them.
MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
- Ipv6DownstreamRule rule = new Ipv6DownstreamRule(upstreamIfindex, mInterfaceParams.index,
- (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
+ Ipv6DownstreamRule rule = new Ipv6DownstreamRule(
+ getInterfaceIndexForRule(mLastIPv6UpstreamIfindex, mUpstreamSupportsBpf),
+ mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac);
if (e.isValid()) {
mBpfCoordinator.addIpv6DownstreamRule(this, rule);
} else {
@@ -951,8 +1029,7 @@
if (mInterfaceParams != null
&& mInterfaceParams.index == e.ifindex
&& mInterfaceParams.hasMacAddress) {
- updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex,
- mUpstreamSupportsBpf, e);
+ addOrRemoveIpv6Downstream(e);
updateClientInfoIpv4(e);
}
}
@@ -1085,7 +1162,19 @@
startServingInterface();
if (mLastError != TETHER_ERROR_NO_ERROR) {
- transitionTo(mInitialState);
+ // This will transition to InitialState right away, regardless of whether any
+ // message is already waiting in the StateMachine queue (including maybe some
+ // message to go to InitialState). InitialState will then process any pending
+ // message (and generally ignores them). It is difficult to know for sure whether
+ // this is correct in all cases, but this is equivalent to what IpServer was doing
+ // in previous versions of the mainline module.
+ // TODO : remove sendMessageAtFrontOfQueueToAsyncSM after migrating to the Sync
+ // StateMachine.
+ if (USE_SYNC_SM) {
+ sendSelfMessageToSyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+ } else {
+ sendMessageAtFrontOfQueueToAsyncSM(CMD_SERVICE_FAILED_TO_START, mLastError);
+ }
}
if (DBG) Log.d(TAG, getStateString(mDesiredInterfaceState) + " serve " + mIfaceName);
@@ -1175,6 +1264,9 @@
mCallback.requestEnableTethering(mInterfaceType, false /* enabled */);
transitionTo(mWaitingForRestartState);
break;
+ case CMD_SERVICE_FAILED_TO_START:
+ mLog.e("start serving fail, error: " + message.arg1);
+ transitionTo(mInitialState);
default:
return false;
}
@@ -1285,6 +1377,7 @@
@Override
public void exit() {
cleanupUpstream();
+ mBpfCoordinator.clearAllIpv6Rules(IpServer.this);
super.exit();
}
@@ -1303,7 +1396,8 @@
for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
mUpstreamIfaceSet = null;
- mBpfCoordinator.tetherOffloadRuleClear(IpServer.this);
+ mBpfCoordinator.updateAllIpv6Rules(
+ IpServer.this, IpServer.this.mInterfaceParams, NO_UPSTREAM, Set.of());
}
private void cleanupUpstreamInterface(String upstreamIface) {
@@ -1312,16 +1406,7 @@
// to remove their rules, which generates errors.
// Just do the best we can.
mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface);
- try {
- mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface);
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Exception in ipfwdRemoveInterfaceForward: " + e.toString());
- }
- try {
- mNetd.tetherRemoveForward(mIfaceName, upstreamIface);
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Exception in disableNat: " + e.toString());
- }
+ removeInterfaceForward(mIfaceName, upstreamIface);
}
@Override
@@ -1370,17 +1455,16 @@
final InterfaceParams upstreamIfaceParams =
mDeps.getInterfaceParams(ifname);
if (upstreamIfaceParams != null) {
- mBpfCoordinator.addUpstreamNameToLookupTable(
+ mBpfCoordinator.maybeAddUpstreamToLookupTable(
upstreamIfaceParams.index, ifname);
}
}
mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
try {
- mNetd.tetherAddForward(mIfaceName, ifname);
- mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname);
+ addInterfaceForward(mIfaceName, ifname);
} catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Exception enabling NAT: " + e.toString());
+ mLog.e("Exception enabling iface forward", e);
cleanupUpstream();
mLastError = TETHER_ERROR_ENABLE_FORWARDING_ERROR;
transitionTo(mInitialState);
@@ -1430,6 +1514,8 @@
class UnavailableState extends State {
@Override
public void enter() {
+ // TODO: move mIpNeighborMonitor.stop() to TetheredState#exit, and trigger a neighbours
+ // dump after starting mIpNeighborMonitor.
mIpNeighborMonitor.stop();
mLastError = TETHER_ERROR_NO_ERROR;
sendInterfaceState(STATE_UNAVAILABLE);
@@ -1488,4 +1574,21 @@
}
return random;
}
+
+ /** Get IPv6 prefixes from LinkProperties */
+ @NonNull
+ @VisibleForTesting
+ static HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull Collection<LinkAddress> addrs) {
+ final HashSet<IpPrefix> prefixes = new HashSet<>();
+ for (LinkAddress linkAddr : addrs) {
+ if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+ prefixes.add(new IpPrefix(linkAddr.getAddress(), RFC7421_PREFIX_LENGTH));
+ }
+ return prefixes;
+ }
+
+ @NonNull
+ private HashSet<IpPrefix> getTetherableIpv6Prefixes(@NonNull LinkProperties lp) {
+ return getTetherableIpv6Prefixes(lp.getLinkAddresses());
+ }
}
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 50d6c4b..5e9bbcb 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -18,6 +18,7 @@
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
@@ -38,12 +39,21 @@
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.FdEventsReader;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.structs.Icmpv6Header;
import com.android.net.module.util.structs.LlaOption;
@@ -103,6 +113,11 @@
private static final int DAY_IN_SECONDS = 86_400;
+ // Commands for IpServer to control RouterAdvertisementDaemon
+ private static final int CMD_START = 1;
+ private static final int CMD_STOP = 2;
+ private static final int CMD_BUILD_NEW_RA = 3;
+
private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
@@ -120,9 +135,13 @@
@GuardedBy("mLock")
private RaParams mRaParams;
+ // To be accessed only from RaMessageHandler
+ private RsPacketListener mRsPacketListener;
+
private volatile FileDescriptor mSocket;
private volatile MulticastTransmitter mMulticastTransmitter;
- private volatile UnicastResponder mUnicastResponder;
+ private volatile RaMessageHandler mRaMessageHandler;
+ private volatile HandlerThread mRaHandlerThread;
/** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
public static class RaParams {
@@ -244,6 +263,94 @@
}
}
+ private class RaMessageHandler extends Handler {
+ RaMessageHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case CMD_START:
+ mRsPacketListener = new RsPacketListener(this);
+ mRsPacketListener.start();
+ break;
+ case CMD_STOP:
+ if (mRsPacketListener != null) {
+ mRsPacketListener.stop();
+ mRsPacketListener = null;
+ }
+ break;
+ case CMD_BUILD_NEW_RA:
+ synchronized (mLock) {
+ // raInfo.first is deprecatedParams and raInfo.second is newParams.
+ final Pair<RaParams, RaParams> raInfo = (Pair<RaParams, RaParams>) msg.obj;
+ if (raInfo.first != null) {
+ mDeprecatedInfoTracker.putPrefixes(raInfo.first.prefixes);
+ mDeprecatedInfoTracker.putDnses(raInfo.first.dnses);
+ }
+
+ if (raInfo.second != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(raInfo.second.prefixes);
+ mDeprecatedInfoTracker.removeDnses(raInfo.second.dnses);
+ }
+ mRaParams = raInfo.second;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
+ break;
+ default:
+ Log.e(TAG, "Unknown message, cmd = " + String.valueOf(msg.what));
+ break;
+ }
+ }
+ }
+
+ private class RsPacketListener extends FdEventsReader<RsPacketListener.RecvBuffer> {
+ private static final class RecvBuffer {
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ final byte[] mBytes = new byte[IPV6_MIN_MTU];
+ final InetSocketAddress mSrcAddr = new InetSocketAddress(0);
+ }
+
+ RsPacketListener(@NonNull Handler handler) {
+ super(handler, new RecvBuffer());
+ }
+
+ @Override
+ protected int recvBufSize(@NonNull RecvBuffer buffer) {
+ return buffer.mBytes.length;
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mSocket;
+ }
+
+ @Override
+ protected int readPacket(@NonNull FileDescriptor fd, @NonNull RecvBuffer buffer)
+ throws Exception {
+ return Os.recvfrom(
+ fd, buffer.mBytes, 0, buffer.mBytes.length, 0 /* flags */, buffer.mSrcAddr);
+ }
+
+ @Override
+ protected final void handlePacket(@NonNull RecvBuffer buffer, int length) {
+ // Do the least possible amount of validations.
+ if (buffer.mSrcAddr == null
+ || length <= 0
+ || buffer.mBytes[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
+ return;
+ }
+
+ maybeSendRA(buffer.mSrcAddr);
+ }
+ }
+
public RouterAdvertisementDaemon(InterfaceParams ifParams) {
mInterface = ifParams;
mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
@@ -252,48 +359,43 @@
/** Build new RA.*/
public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
- synchronized (mLock) {
- if (deprecatedParams != null) {
- mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
- mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
- }
-
- if (newParams != null) {
- // Process information that is no longer deprecated.
- mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
- mDeprecatedInfoTracker.removeDnses(newParams.dnses);
- }
-
- mRaParams = newParams;
- assembleRaLocked();
- }
-
- maybeNotifyMulticastTransmitter();
+ final Pair<RaParams, RaParams> raInfo = new Pair<>(deprecatedParams, newParams);
+ sendMessage(CMD_BUILD_NEW_RA, raInfo);
}
/** Start router advertisement daemon. */
public boolean start() {
if (!createSocket()) {
+ Log.e(TAG, "Failed to start RouterAdvertisementDaemon.");
return false;
}
mMulticastTransmitter = new MulticastTransmitter();
mMulticastTransmitter.start();
- mUnicastResponder = new UnicastResponder();
- mUnicastResponder.start();
+ mRaHandlerThread = new HandlerThread(TAG);
+ mRaHandlerThread.start();
+ mRaMessageHandler = new RaMessageHandler(mRaHandlerThread.getLooper());
- return true;
+ return sendMessage(CMD_START);
}
/** Stop router advertisement daemon. */
public void stop() {
+ if (!sendMessage(CMD_STOP)) {
+ Log.e(TAG, "RouterAdvertisementDaemon has been stopped or was never started.");
+ return;
+ }
+
+ mRaHandlerThread.quitSafely();
+ mRaHandlerThread = null;
+ mRaMessageHandler = null;
+
closeSocket();
// Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
// the thread's termination.
maybeNotifyMulticastTransmitter();
mMulticastTransmitter = null;
- mUnicastResponder = null;
}
@GuardedBy("mLock")
@@ -503,7 +605,7 @@
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
try {
- mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ mSocket = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
@@ -565,34 +667,17 @@
}
}
- private final class UnicastResponder extends Thread {
- private final InetSocketAddress mSolicitor = new InetSocketAddress(0);
- // The recycled buffer for receiving Router Solicitations from clients.
- // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
- // This is fine since currently only byte 0 is examined anyway.
- private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
+ private boolean sendMessage(int cmd) {
+ return sendMessage(cmd, null);
+ }
- @Override
- public void run() {
- while (isSocketValid()) {
- try {
- // Blocking receive.
- final int rval = Os.recvfrom(
- mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
- // Do the least possible amount of validation.
- if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
- continue;
- }
- } catch (ErrnoException | SocketException e) {
- if (isSocketValid()) {
- Log.e(TAG, "recvfrom error: " + e);
- }
- continue;
- }
-
- maybeSendRA(mSolicitor);
- }
+ private boolean sendMessage(int cmd, @Nullable Object obj) {
+ if (mRaMessageHandler == null) {
+ return false;
}
+
+ return mRaMessageHandler.sendMessage(
+ Message.obtain(mRaMessageHandler, cmd, obj));
}
// TODO: Consider moving this to run on a provided Looper as a Handler,
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 1b23a6c..9f542f4 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -28,6 +28,7 @@
import static android.system.OsConstants.ETH_P_IPV6;
import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
@@ -50,6 +51,7 @@
import android.system.ErrnoException;
import android.system.OsConstants;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
@@ -88,8 +90,6 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -122,7 +122,6 @@
private static final int DUMP_TIMEOUT_MS = 10_000;
private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
"00:00:00:00:00:00");
- private static final IpPrefix IPV6_ZERO_PREFIX64 = new IpPrefix("::/64");
private static final String TETHER_DOWNSTREAM4_MAP_PATH = makeMapPath(DOWNSTREAM, 4);
private static final String TETHER_UPSTREAM4_MAP_PATH = makeMapPath(UPSTREAM, 4);
private static final String TETHER_DOWNSTREAM6_FS_PATH = makeMapPath(DOWNSTREAM, 6);
@@ -153,6 +152,7 @@
static final int NF_CONNTRACK_UDP_TIMEOUT_STREAM = 180;
@VisibleForTesting
static final int INVALID_MTU = 0;
+ static final int NO_UPSTREAM = 0;
// List of TCP port numbers which aren't offloaded because the packets require the netfilter
// conntrack helper. See also TetherController::setForwardRules in netd.
@@ -221,6 +221,23 @@
// TODO: Remove the unused interface name.
private final SparseArray<String> mInterfaceNames = new SparseArray<>();
+ // How IPv6 upstream rules and downstream rules are managed in BpfCoordinator:
+ // 1. Each upstream rule represents a downstream interface to an upstream interface forwarding.
+ // No upstream rule will be exist if there is no upstream interface.
+ // Note that there is at most one upstream interface for a given downstream interface.
+ // 2. Each downstream rule represents an IPv6 neighbor, regardless of the existence of the
+ // upstream interface. If the upstream is not present, the downstream rules have an upstream
+ // interface index of NO_UPSTREAM, only exist in BpfCoordinator and won't be written to the
+ // BPF map. When the upstream comes back, those downstream rules will be updated by calling
+ // Ipv6DownstreamRule#onNewUpstream and written to the BPF map again. We don't remove the
+ // downstream rules when upstream is lost is because the upstream may come back with the
+ // same prefix and we won't receive any neighbor update event in this case.
+ // TODO: Remove downstream rules when upstream is lost and dump neighbors table when upstream
+ // interface comes back in order to reconstruct the downstream rules.
+ // 3. It is the same thing for BpfCoordinator if there is no upstream interface or the upstream
+ // interface is a virtual interface (which currently not supports BPF). In this case,
+ // IpServer will update its upstream ifindex to NO_UPSTREAM to the BpfCoordinator.
+
// Map of downstream rule maps. Each of these maps represents the IPv6 forwarding rules for a
// given downstream. Each map:
// - Is owned by the IpServer that is responsible for that downstream.
@@ -240,6 +257,16 @@
private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6DownstreamRule>>
mIpv6DownstreamRules = new LinkedHashMap<>();
+ // Map of IPv6 upstream rules maps. Each of these maps represents the IPv6 upstream rules for a
+ // given downstream. Each map:
+ // - Is owned by the IpServer that is responsible for that downstream.
+ // - Must only be modified by that IpServer.
+ // - Is created when the IpServer adds its first upstream rule, and deleted when the IpServer
+ // deletes its last upstream rule (or clears its upstream rules)
+ // - Each upstream rule in the ArraySet is corresponding to an upstream interface.
+ private final ArrayMap<IpServer, ArraySet<Ipv6UpstreamRule>>
+ mIpv6UpstreamRules = new ArrayMap<>();
+
// Map of downstream client maps. Each of these maps represents the IPv4 clients for a given
// downstream. Needed to build IPv4 forwarding rules when conntrack events are received.
// Each map:
@@ -603,130 +630,176 @@
}
/**
- * Add IPv6 downstream rule. After adding the first rule on a given upstream, must add the data
- * limit on the given upstream.
- * Note that this can be only called on handler thread.
+ * Add IPv6 upstream rule. After adding the first rule on a given upstream, must add the
+ * data limit on the given upstream.
*/
- public void addIpv6DownstreamRule(
- @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
+ private void addIpv6UpstreamRule(
+ @NonNull final IpServer ipServer, @NonNull final Ipv6UpstreamRule rule) {
if (!isUsingBpf()) return;
- // TODO: Perhaps avoid to add a duplicate rule.
- if (!mBpfCoordinatorShim.addIpv6DownstreamRule(rule)) return;
-
- if (!mIpv6DownstreamRules.containsKey(ipServer)) {
- mIpv6DownstreamRules.put(ipServer, new LinkedHashMap<Inet6Address,
- Ipv6DownstreamRule>());
- }
- LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
-
// Add upstream and downstream interface index to dev map.
maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex);
// When the first rule is added to an upstream, setup upstream forwarding and data limit.
maybeSetLimit(rule.upstreamIfindex);
- if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
- // TODO: support upstream forwarding on non-point-to-point interfaces.
- // TODO: get the MTU from LinkProperties and update the rules when it changes.
- Ipv6UpstreamRule upstreamRule = new Ipv6UpstreamRule(rule.upstreamIfindex,
- rule.downstreamIfindex, IPV6_ZERO_PREFIX64, rule.srcMac, NULL_MAC_ADDRESS,
- NULL_MAC_ADDRESS);
- if (!mBpfCoordinatorShim.addIpv6UpstreamRule(upstreamRule)) {
- mLog.e("Failed to add upstream IPv6 forwarding rule: " + upstreamRule);
- }
+ // TODO: support upstream forwarding on non-point-to-point interfaces.
+ // TODO: get the MTU from LinkProperties and update the rules when it changes.
+ if (!mBpfCoordinatorShim.addIpv6UpstreamRule(rule)) {
+ return;
}
- // Must update the adding rule after calling #isAnyRuleOnUpstream because it needs to
- // check if it is about adding a first rule for a given upstream.
+ ArraySet<Ipv6UpstreamRule> rules = mIpv6UpstreamRules.computeIfAbsent(
+ ipServer, k -> new ArraySet<Ipv6UpstreamRule>());
+ rules.add(rule);
+ }
+
+ /**
+ * Clear all IPv6 upstream rules for a given downstream. After removing the last rule on a given
+ * upstream, must clear data limit, update the last tether stats and remove the tether stats in
+ * the BPF maps.
+ */
+ private void clearIpv6UpstreamRules(@NonNull final IpServer ipServer) {
+ if (!isUsingBpf()) return;
+
+ final ArraySet<Ipv6UpstreamRule> upstreamRules = mIpv6UpstreamRules.remove(ipServer);
+ if (upstreamRules == null) return;
+
+ int upstreamIfindex = 0;
+ for (Ipv6UpstreamRule rule: upstreamRules) {
+ if (upstreamIfindex != 0 && rule.upstreamIfindex != upstreamIfindex) {
+ Log.wtf(TAG, "BUG: upstream rules point to more than one interface");
+ }
+ upstreamIfindex = rule.upstreamIfindex;
+ mBpfCoordinatorShim.removeIpv6UpstreamRule(rule);
+ }
+ // Clear the limit if there are no more rules on the given upstream.
+ // Using upstreamIfindex outside the loop is fine because all the rules for a given IpServer
+ // will always have the same upstream index (since they are always added all together by
+ // updateAllIpv6Rules).
+ // The upstreamIfindex can't be 0 because we won't add an Ipv6UpstreamRule with
+ // upstreamIfindex == 0 and if there is no Ipv6UpstreamRule for an IpServer, it will be
+ // removed from mIpv6UpstreamRules.
+ if (upstreamIfindex == 0) {
+ Log.wtf(TAG, "BUG: upstream rules have empty Set or rule.upstreamIfindex == 0");
+ return;
+ }
+ maybeClearLimit(upstreamIfindex);
+ }
+
+ /**
+ * Add IPv6 downstream rule.
+ * Note that this can be only called on handler thread.
+ */
+ public void addIpv6DownstreamRule(
+ @NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
+ if (!isUsingBpf()) return;
+
+ // TODO: Perhaps avoid to add a duplicate rule.
+ if (rule.upstreamIfindex != NO_UPSTREAM
+ && !mBpfCoordinatorShim.addIpv6DownstreamRule(rule)) return;
+
+ LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
+ mIpv6DownstreamRules.computeIfAbsent(ipServer,
+ k -> new LinkedHashMap<Inet6Address, Ipv6DownstreamRule>());
rules.put(rule.address, rule);
}
/**
- * Remove IPv6 downstream rule. After removing the last rule on a given upstream, must clear
- * data limit, update the last tether stats and remove the tether stats in the BPF maps.
+ * Remove IPv6 downstream rule.
* Note that this can be only called on handler thread.
*/
public void removeIpv6DownstreamRule(
@NonNull final IpServer ipServer, @NonNull final Ipv6DownstreamRule rule) {
if (!isUsingBpf()) return;
- if (!mBpfCoordinatorShim.removeIpv6DownstreamRule(rule)) return;
+ if (rule.upstreamIfindex != NO_UPSTREAM
+ && !mBpfCoordinatorShim.removeIpv6DownstreamRule(rule)) return;
LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules = mIpv6DownstreamRules.get(ipServer);
if (rules == null) return;
- // Must remove rules before calling #isAnyRuleOnUpstream because it needs to check if
- // the last rule is removed for a given upstream. If no rule is removed, return early.
- // Avoid unnecessary work on a non-existent rule which may have never been added or
- // removed already.
+ // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule which
+ // may have never been added or removed already.
if (rules.remove(rule.address) == null) return;
// Remove the downstream entry if it has no more rule.
if (rules.isEmpty()) {
mIpv6DownstreamRules.remove(ipServer);
}
+ }
- // If no more rules between this upstream and downstream, stop upstream forwarding.
- if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
- Ipv6UpstreamRule upstreamRule = new Ipv6UpstreamRule(rule.upstreamIfindex,
- rule.downstreamIfindex, IPV6_ZERO_PREFIX64, rule.srcMac, NULL_MAC_ADDRESS,
- NULL_MAC_ADDRESS);
- if (!mBpfCoordinatorShim.removeIpv6UpstreamRule(upstreamRule)) {
- mLog.e("Failed to remove upstream IPv6 forwarding rule: " + upstreamRule);
- }
+ /**
+ * Clear all downstream rules for a given IpServer and return a copy of all removed rules.
+ */
+ @Nullable
+ private Collection<Ipv6DownstreamRule> clearIpv6DownstreamRules(
+ @NonNull final IpServer ipServer) {
+ final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> downstreamRules =
+ mIpv6DownstreamRules.remove(ipServer);
+ if (downstreamRules == null) return null;
+
+ final Collection<Ipv6DownstreamRule> removedRules = downstreamRules.values();
+ for (final Ipv6DownstreamRule rule : removedRules) {
+ if (rule.upstreamIfindex == NO_UPSTREAM) continue;
+ mBpfCoordinatorShim.removeIpv6DownstreamRule(rule);
}
-
- // Do cleanup functionality if there is no more rule on the given upstream.
- maybeClearLimit(rule.upstreamIfindex);
+ return removedRules;
}
/**
* Clear all forwarding rules for a given downstream.
* Note that this can be only called on handler thread.
- * TODO: rename to tetherOffloadRuleClear6 because of IPv6 only.
*/
- public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
+ public void clearAllIpv6Rules(@NonNull final IpServer ipServer) {
if (!isUsingBpf()) return;
- final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
- mIpv6DownstreamRules.get(ipServer);
- if (rules == null) return;
-
- // Need to build a rule list because the rule map may be changed in the iteration.
- for (final Ipv6DownstreamRule rule : new ArrayList<Ipv6DownstreamRule>(rules.values())) {
- removeIpv6DownstreamRule(ipServer, rule);
- }
+ // Clear downstream rules first, because clearing upstream rules fetches the stats, and
+ // fetching the stats requires that no rules be forwarding traffic to or from the upstream.
+ clearIpv6DownstreamRules(ipServer);
+ clearIpv6UpstreamRules(ipServer);
}
/**
- * Update existing forwarding rules to new upstream for a given downstream.
+ * Delete all upstream and downstream rules for the passed-in IpServer, and if the new upstream
+ * is nonzero, reapply them to the new upstream.
* Note that this can be only called on handler thread.
*/
- public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
+ public void updateAllIpv6Rules(@NonNull final IpServer ipServer,
+ final InterfaceParams interfaceParams, int newUpstreamIfindex,
+ @NonNull final Set<IpPrefix> newUpstreamPrefixes) {
if (!isUsingBpf()) return;
- final LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules =
- mIpv6DownstreamRules.get(ipServer);
- if (rules == null) return;
+ // Remove IPv6 downstream rules. Remove the old ones before adding the new rules, otherwise
+ // we need to keep a copy of the old rules.
+ // We still need to keep the downstream rules even when the upstream goes away because it
+ // may come back with the same prefixes (unlikely, but possible). Neighbor entries won't be
+ // deleted and we're not expected to receive new Neighbor events in this case.
+ // TODO: Add new rule first to reduce the latency which has no rule. But this is okay
+ // because if this is a new upstream, it will probably have different prefixes than
+ // the one these downstream rules are in. If so, they will never see any downstream
+ // traffic before new neighbor entries are created.
+ final Collection<Ipv6DownstreamRule> deletedDownstreamRules =
+ clearIpv6DownstreamRules(ipServer);
- // Need to build a rule list because the rule map may be changed in the iteration.
- // First remove all the old rules, then add all the new rules. This is because the upstream
- // forwarding code in addIpv6DownstreamRule cannot support rules on two upstreams at the
- // same time. Deleting the rules first ensures that upstream forwarding is disabled on the
- // old upstream when the last rule is removed from it, and re-enabled on the new upstream
- // when the first rule is added to it.
- // TODO: Once the IPv6 client processing code has moved from IpServer to BpfCoordinator, do
- // something smarter.
- final ArrayList<Ipv6DownstreamRule> rulesCopy = new ArrayList<>(rules.values());
- for (final Ipv6DownstreamRule rule : rulesCopy) {
- // Remove the old rule before adding the new one because the map uses the same key for
- // both rules. Reversing the processing order causes that the new rule is removed as
- // unexpected.
- // TODO: Add new rule first to reduce the latency which has no rule.
- removeIpv6DownstreamRule(ipServer, rule);
+ // Remove IPv6 upstream rules. Downstream rules must be removed first because
+ // BpfCoordinatorShimImpl#tetherOffloadGetAndClearStats will be called after the removal of
+ // the last upstream rule and it requires that no rules be forwarding traffic to or from
+ // that upstream.
+ clearIpv6UpstreamRules(ipServer);
+
+ // Add new upstream rules.
+ if (newUpstreamIfindex != 0 && interfaceParams != null && interfaceParams.macAddr != null) {
+ for (final IpPrefix ipPrefix : newUpstreamPrefixes) {
+ addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule(
+ newUpstreamIfindex, interfaceParams.index, ipPrefix,
+ interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS));
+ }
}
- for (final Ipv6DownstreamRule rule : rulesCopy) {
+
+ // Add updated downstream rules.
+ if (deletedDownstreamRules == null) return;
+ for (final Ipv6DownstreamRule rule : deletedDownstreamRules) {
addIpv6DownstreamRule(ipServer, rule.onNewUpstream(newUpstreamIfindex));
}
}
@@ -737,7 +810,7 @@
* expects the interface name in NetworkStats object.
* Note that this can be only called on handler thread.
*/
- public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) {
+ public void maybeAddUpstreamToLookupTable(int upstreamIfindex, @Nullable String upstreamIface) {
if (!isUsingBpf()) return;
if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return;
@@ -1007,7 +1080,7 @@
* TODO: consider error handling if the attach program failed.
*/
public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) {
- if (isVcnInterface(extIface)) return;
+ if (!isUsingBpf() || isVcnInterface(extIface)) return;
if (forwardingPairExists(intIface, extIface)) return;
@@ -1031,6 +1104,8 @@
* Detach BPF program
*/
public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) {
+ if (!isUsingBpf()) return;
+
forwardingPairRemove(intIface, extIface);
// Detaching program may fail because the interface has been removed already.
@@ -1182,10 +1257,30 @@
pw.decreaseIndent();
}
+ /**
+ * Returns a /64 IpPrefix corresponding to the passed in byte array
+ *
+ * @param ip64 byte array to convert format
+ * @return the converted IpPrefix
+ */
+ @VisibleForTesting
+ public static IpPrefix bytesToPrefix(final byte[] ip64) {
+ IpPrefix sourcePrefix;
+ byte[] prefixBytes = Arrays.copyOf(ip64, IPV6_ADDR_LEN);
+ try {
+ sourcePrefix = new IpPrefix(InetAddress.getByAddress(prefixBytes), 64);
+ } catch (UnknownHostException e) {
+ // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte array
+ // is the wrong length, but we allocate it with fixed length IPV6_ADDR_LEN.
+ throw new IllegalArgumentException("Invalid IPv6 address");
+ }
+ return sourcePrefix;
+ }
+
private String ipv6UpstreamRuleToString(TetherUpstream6Key key, Tether6Value value) {
- return String.format("%d(%s) [%s] -> %d(%s) %04x [%s] [%s]",
- key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif),
- value.ethProto, value.ethSrcMac, value.ethDstMac);
+ return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s]",
+ key.iif, getIfName(key.iif), key.dstMac, bytesToPrefix(key.src64), value.oif,
+ getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac);
}
private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
@@ -1235,8 +1330,8 @@
// TODO: use dump utils with headerline and lambda which prints key and value to reduce
// duplicate bpf map dump code.
private void dumpBpfForwardingRulesIpv6(IndentingPrintWriter pw) {
- pw.println("IPv6 Upstream: iif(iface) [inDstMac] -> oif(iface) etherType [outSrcMac] "
- + "[outDstMac]");
+ pw.println("IPv6 Upstream: iif(iface) [inDstMac] [sourcePrefix] -> oif(iface) etherType "
+ + "[outSrcMac] [outDstMac]");
pw.increaseIndent();
dumpIpv6UpstreamRules(pw);
pw.decreaseIndent();
@@ -1480,8 +1575,7 @@
*/
@NonNull
public TetherUpstream6Key makeTetherUpstream6Key() {
- byte[] prefixBytes = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
- long prefix64 = ByteBuffer.wrap(prefixBytes).order(ByteOrder.BIG_ENDIAN).getLong();
+ final byte[] prefix64 = Arrays.copyOf(sourcePrefix.getRawAddress(), 8);
return new TetherUpstream6Key(downstreamIfindex, inDstMac, prefix64);
}
@@ -1508,10 +1602,8 @@
@Override
public int hashCode() {
- // TODO: if this is ever used in production code, don't pass ifindices
- // to Objects.hash() to avoid autoboxing overhead.
- return Objects.hash(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
- outSrcMac, outDstMac);
+ return 13 * upstreamIfindex + 41 * downstreamIfindex
+ + Objects.hash(sourcePrefix, inDstMac, outSrcMac, outDstMac);
}
@Override
@@ -1636,9 +1728,8 @@
@Override
public int hashCode() {
- // TODO: if this is ever used in production code, don't pass ifindices
- // to Objects.hash() to avoid autoboxing overhead.
- return Objects.hash(upstreamIfindex, downstreamIfindex, address, srcMac, dstMac);
+ return 13 * upstreamIfindex + 41 * downstreamIfindex
+ + Objects.hash(address, srcMac, dstMac);
}
@Override
@@ -1967,9 +2058,8 @@
}
private int getInterfaceIndexFromRules(@NonNull String ifName) {
- for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
- mIpv6DownstreamRules.values()) {
- for (Ipv6DownstreamRule rule : rules.values()) {
+ for (ArraySet<Ipv6UpstreamRule> rules : mIpv6UpstreamRules.values()) {
+ for (Ipv6UpstreamRule rule : rules) {
final int upstreamIfindex = rule.upstreamIfindex;
if (TextUtils.equals(ifName, mInterfaceNames.get(upstreamIfindex))) {
return upstreamIfindex;
@@ -1987,6 +2077,7 @@
}
private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) {
+ if (!isUsingBpf()) return false;
if (ifIndex == 0) {
Log.wtf(TAG, "Invalid interface index.");
return false;
@@ -2060,28 +2151,14 @@
// TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called
// both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream.
private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
- for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
- mIpv6DownstreamRules.values()) {
- for (Ipv6DownstreamRule rule : rules.values()) {
+ for (ArraySet<Ipv6UpstreamRule> rules : mIpv6UpstreamRules.values()) {
+ for (Ipv6UpstreamRule rule : rules) {
if (upstreamIfindex == rule.upstreamIfindex) return true;
}
}
return false;
}
- private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) {
- for (LinkedHashMap<Inet6Address, Ipv6DownstreamRule> rules :
- mIpv6DownstreamRules.values()) {
- for (Ipv6DownstreamRule rule : rules.values()) {
- if (downstreamIfindex == rule.downstreamIfindex
- && upstreamIfindex == rule.upstreamIfindex) {
- return true;
- }
- }
- }
- return false;
- }
-
// TODO: remove the index from map while the interface has been removed because the map size
// is 64 entries. See packages\modules\Connectivity\Tethering\bpf_progs\offload.c.
private void maybeAddDevMap(int upstreamIfindex, int downstreamIfindex) {
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
index e7dc757..9ef0f45 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalAidlImpl.java
@@ -19,18 +19,21 @@
import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_AIDL;
import android.annotation.NonNull;
+import android.annotation.TargetApi;
import android.hardware.tetheroffload.ForwardedStats;
import android.hardware.tetheroffload.IOffload;
import android.hardware.tetheroffload.ITetheringOffloadCallback;
import android.hardware.tetheroffload.NatTimeoutUpdate;
import android.hardware.tetheroffload.NetworkProtocol;
import android.hardware.tetheroffload.OffloadCallbackEvent;
+import android.os.Build;
import android.os.Handler;
import android.os.NativeHandle;
import android.os.ParcelFileDescriptor;
import android.os.ServiceManager;
import android.system.OsConstants;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.OffloadHardwareInterface.OffloadHalCallback;
@@ -40,6 +43,7 @@
/**
* The implementation of IOffloadHal which based on Stable AIDL interface
*/
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class OffloadHalAidlImpl implements IOffloadHal {
private static final String TAG = OffloadHalAidlImpl.class.getSimpleName();
private static final String HAL_INSTANCE_NAME = IOffload.DESCRIPTOR + "/default";
@@ -52,6 +56,7 @@
private TetheringOffloadCallback mTetheringOffloadCallback;
+ @VisibleForTesting
public OffloadHalAidlImpl(int version, @NonNull IOffload offload, @NonNull Handler handler,
@NonNull SharedLog log) {
mOffloadVersion = version;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
index e0a9878..71922f9 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
@@ -74,10 +74,7 @@
*/
public boolean initOffload(@NonNull NativeHandle handle1, @NonNull NativeHandle handle2,
@NonNull OffloadHalCallback callback) {
- final String logmsg = String.format("initOffload(%d, %d, %s)",
- handle1.getFileDescriptor().getInt$(), handle2.getFileDescriptor().getInt$(),
- (callback == null) ? "null"
- : "0x" + Integer.toHexString(System.identityHashCode(callback)));
+ final String logmsg = "initOffload()";
mOffloadHalCallback = callback;
mTetheringOffloadCallback = new TetheringOffloadCallback(
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
index 36a1c3c..0cc3dd9 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
@@ -32,10 +32,10 @@
@Field(order = 1, type = Type.EUI48, padding = 6)
public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
- @Field(order = 2, type = Type.S64)
- public final long src64; // The top 64-bits of the source ip.
+ @Field(order = 2, type = Type.ByteArray, arraysize = 8)
+ public final byte[] src64; // The top 64-bits of the source ip.
- public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac, long src64) {
+ public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac, final byte[] src64) {
Objects.requireNonNull(dstMac);
this.iif = iif;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index b371178..da3b584 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -90,6 +90,7 @@
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetherStatesParcel;
import android.net.TetheredClient;
import android.net.TetheringCallbackStartedParcel;
@@ -136,6 +137,7 @@
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.common.BluetoothPanShim;
import com.android.networkstack.apishim.common.BluetoothPanShim.TetheredInterfaceCallbackShim;
@@ -250,6 +252,10 @@
private final Handler mHandler;
private final INetd mNetd;
private final NetdCallback mNetdCallback;
+ // Contains null if the connectivity module is unsupported, as the routing coordinator is not
+ // available. Must use LateSdk because MessageUtils enumerates fields in this class, so it
+ // must be able to find all classes at runtime.
+ @NonNull private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinator;
private final UserRestrictionActionListener mTetheringRestriction;
private final ActiveDataSubIdListener mActiveDataSubIdListener;
private final ConnectedClientsTracker mConnectedClientsTracker;
@@ -296,6 +302,7 @@
mDeps = deps;
mContext = mDeps.getContext();
mNetd = mDeps.getINetd(mContext);
+ mRoutingCoordinator = mDeps.getRoutingCoordinator(mContext);
mLooper = mDeps.getTetheringLooper();
mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper);
mTetheringMetrics = mDeps.getTetheringMetrics();
@@ -320,8 +327,10 @@
return mConfig;
}
});
- mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMainSM, mLog,
- TetherMainSM.EVENT_UPSTREAM_CALLBACK);
+ mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mHandler, mLog,
+ (what, obj) -> {
+ mTetherMainSM.sendMessage(TetherMainSM.EVENT_UPSTREAM_CALLBACK, what, 0, obj);
+ });
mForwardedDownstreams = new HashSet<>();
IntentFilter filter = new IntentFilter();
@@ -1680,6 +1689,8 @@
static final int EVENT_IFACE_UPDATE_LINKPROPERTIES = BASE_MAIN_SM + 7;
// Events from EntitlementManager to choose upstream again.
static final int EVENT_UPSTREAM_PERMISSION_CHANGED = BASE_MAIN_SM + 8;
+ // Internal request from IpServer to enable or disable downstream.
+ static final int EVENT_REQUEST_CHANGE_DOWNSTREAM = BASE_MAIN_SM + 9;
private final State mInitialState;
private final State mTetherModeAliveState;
@@ -2179,6 +2190,12 @@
}
break;
}
+ case EVENT_REQUEST_CHANGE_DOWNSTREAM: {
+ final int tetheringType = message.arg1;
+ final Boolean enabled = (Boolean) message.obj;
+ enableTetheringInternal(tetheringType, enabled, null);
+ break;
+ }
default:
retValue = false;
break;
@@ -2717,83 +2734,73 @@
}
}
- private IpServer.Callback makeControlCallback() {
- return new IpServer.Callback() {
- @Override
- public void updateInterfaceState(IpServer who, int state, int lastError) {
- notifyInterfaceStateChange(who, state, lastError);
+ private class ControlCallback extends IpServer.Callback {
+ @Override
+ public void updateInterfaceState(IpServer who, int state, int lastError) {
+ final String iface = who.interfaceName();
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.ipServer.equals(who)) {
+ tetherState.lastState = state;
+ tetherState.lastError = lastError;
+ } else {
+ if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
}
- @Override
- public void updateLinkProperties(IpServer who, LinkProperties newLp) {
- notifyLinkPropertiesChanged(who, newLp);
- }
+ mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, lastError));
- @Override
- public void dhcpLeasesChanged() {
- maybeDhcpLeasesChanged();
+ // If TetherMainSM is in ErrorState, TetherMainSM stays there.
+ // Thus we give a chance for TetherMainSM to recover to InitialState
+ // by sending CMD_CLEAR_ERROR
+ if (lastError == TETHER_ERROR_INTERNAL_ERROR) {
+ mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
}
-
- @Override
- public void requestEnableTethering(int tetheringType, boolean enabled) {
- enableTetheringInternal(tetheringType, enabled, null);
+ int which;
+ switch (state) {
+ case IpServer.STATE_UNAVAILABLE:
+ case IpServer.STATE_AVAILABLE:
+ which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
+ break;
+ case IpServer.STATE_TETHERED:
+ case IpServer.STATE_LOCAL_ONLY:
+ which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
+ break;
+ default:
+ Log.wtf(TAG, "Unknown interface state: " + state);
+ return;
}
- };
- }
-
- // TODO: Move into TetherMainSM.
- private void notifyInterfaceStateChange(IpServer who, int state, int error) {
- final String iface = who.interfaceName();
- final TetherState tetherState = mTetherStates.get(iface);
- if (tetherState != null && tetherState.ipServer.equals(who)) {
- tetherState.lastState = state;
- tetherState.lastError = error;
- } else {
- if (DBG) Log.d(TAG, "got notification from stale iface " + iface);
+ mTetherMainSM.sendMessage(which, state, 0, who);
+ sendTetherStateChangedBroadcast();
}
- mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error));
-
- // If TetherMainSM is in ErrorState, TetherMainSM stays there.
- // Thus we give a chance for TetherMainSM to recover to InitialState
- // by sending CMD_CLEAR_ERROR
- if (error == TETHER_ERROR_INTERNAL_ERROR) {
- mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who);
- }
- int which;
- switch (state) {
- case IpServer.STATE_UNAVAILABLE:
- case IpServer.STATE_AVAILABLE:
- which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
- break;
- case IpServer.STATE_TETHERED:
- case IpServer.STATE_LOCAL_ONLY:
- which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
- break;
- default:
- Log.wtf(TAG, "Unknown interface state: " + state);
+ @Override
+ public void updateLinkProperties(IpServer who, LinkProperties newLp) {
+ final String iface = who.interfaceName();
+ final int state;
+ final TetherState tetherState = mTetherStates.get(iface);
+ if (tetherState != null && tetherState.ipServer.equals(who)) {
+ state = tetherState.lastState;
+ } else {
+ mLog.log("got notification from stale iface " + iface);
return;
- }
- mTetherMainSM.sendMessage(which, state, 0, who);
- sendTetherStateChangedBroadcast();
- }
+ }
- private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) {
- final String iface = who.interfaceName();
- final int state;
- final TetherState tetherState = mTetherStates.get(iface);
- if (tetherState != null && tetherState.ipServer.equals(who)) {
- state = tetherState.lastState;
- } else {
- mLog.log("got notification from stale iface " + iface);
- return;
+ mLog.log(String.format(
+ "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
+ iface, IpServer.getStateString(state), newLp));
+ final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
+ mTetherMainSM.sendMessage(which, state, 0, newLp);
}
- mLog.log(String.format(
- "OBSERVED LinkProperties update iface=%s state=%s lp=%s",
- iface, IpServer.getStateString(state), newLp));
- final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES;
- mTetherMainSM.sendMessage(which, state, 0, newLp);
+ @Override
+ public void dhcpLeasesChanged() {
+ maybeDhcpLeasesChanged();
+ }
+
+ @Override
+ public void requestEnableTethering(int tetheringType, boolean enabled) {
+ mTetherMainSM.sendMessage(TetherMainSM.EVENT_REQUEST_CHANGE_DOWNSTREAM,
+ tetheringType, 0, enabled ? Boolean.TRUE : Boolean.FALSE);
+ }
}
private boolean hasSystemFeature(final String feature) {
@@ -2834,9 +2841,10 @@
mLog.i("adding IpServer for: " + iface);
final TetherState tetherState = new TetherState(
- new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
- makeControlCallback(), mConfig, mPrivateAddressCoordinator,
- mTetheringMetrics, mDeps.getIpServerDependencies()), isNcm);
+ new IpServer(iface, mHandler, interfaceType, mLog, mNetd, mBpfCoordinator,
+ mRoutingCoordinator, new ControlCallback(), mConfig,
+ mPrivateAddressCoordinator, mTetheringMetrics,
+ mDeps.getIpServerDependencies()), isNcm);
mTetherStates.put(iface, tetherState);
tetherState.ipServer.start();
}
@@ -2862,4 +2870,9 @@
} catch (RemoteException e) { }
});
}
+
+ @VisibleForTesting
+ public TetherMainSM getTetherMainSMForTesting() {
+ return mTetherMainSM;
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 747cc20..502fee8 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -136,6 +136,9 @@
*/
public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+ /** A flag for using synchronous or asynchronous state machine. */
+ public static final boolean USE_SYNC_SM = false;
+
public final String[] tetherableUsbRegexs;
public final String[] tetherableWifiRegexs;
public final String[] tetherableWigigRegexs;
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 741a5c5..d02e8e8 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -16,11 +16,14 @@
package com.android.networkstack.tethering;
+import android.annotation.Nullable;
import android.app.usage.NetworkStatsManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothPan;
import android.content.Context;
import android.net.INetd;
+import android.net.RoutingCoordinatorManager;
+import android.net.connectivity.ConnectivityInternalApiUtil;
import android.net.ip.IpServer;
import android.os.Build;
import android.os.Handler;
@@ -32,7 +35,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import com.android.internal.util.StateMachine;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.BluetoothPanShimImpl;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -79,9 +83,9 @@
/**
* Get a reference to the UpstreamNetworkMonitor to be used by tethering.
*/
- public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
- SharedLog log, int what) {
- return new UpstreamNetworkMonitor(ctx, target, log, what);
+ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, Handler h,
+ SharedLog log, UpstreamNetworkMonitor.EventListener listener) {
+ return new UpstreamNetworkMonitor(ctx, h, log, listener);
}
/**
@@ -122,6 +126,16 @@
}
/**
+ * Get the routing coordinator, or null if below S.
+ */
+ @Nullable
+ public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(Context context) {
+ if (!SdkLevel.isAtLeastS()) return new LateSdk<>(null);
+ return new LateSdk<>(
+ ConnectivityInternalApiUtil.getRoutingCoordinatorManager(context));
+ }
+
+ /**
* Get a reference to the TetheringNotificationUpdater to be used by tethering.
*/
public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx,
@@ -135,7 +149,7 @@
public abstract Looper getTetheringLooper();
/**
- * Get Context of TetheringSerice.
+ * Get Context of TetheringService.
*/
public abstract Context getContext();
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index ac2aa7b..7a05d74 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -44,7 +44,6 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.StateMachine;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
import com.android.networkstack.apishim.common.ConnectivityManagerShim;
@@ -111,9 +110,8 @@
private final Context mContext;
private final SharedLog mLog;
- private final StateMachine mTarget;
private final Handler mHandler;
- private final int mWhat;
+ private final EventListener mEventListener;
private final HashMap<Network, UpstreamNetworkState> mNetworkMap = new HashMap<>();
private HashSet<IpPrefix> mLocalPrefixes;
private ConnectivityManager mCM;
@@ -135,12 +133,11 @@
private Network mDefaultInternetNetwork;
private boolean mPreferTestNetworks;
- public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
+ public UpstreamNetworkMonitor(Context ctx, Handler h, SharedLog log, EventListener listener) {
mContext = ctx;
- mTarget = tgt;
- mHandler = mTarget.getHandler();
+ mHandler = h;
mLog = log.forSubComponent(TAG);
- mWhat = what;
+ mEventListener = listener;
mLocalPrefixes = new HashSet<>();
mIsDefaultCellularUpstream = false;
mCM = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -374,11 +371,12 @@
network, newNc));
}
- mNetworkMap.put(network, new UpstreamNetworkState(
- prev.linkProperties, newNc, network));
+ final UpstreamNetworkState uns =
+ new UpstreamNetworkState(prev.linkProperties, newNc, network);
+ mNetworkMap.put(network, uns);
// TODO: If sufficient information is available to select a more
// preferable upstream, do so now and notify the target.
- notifyTarget(EVENT_ON_CAPABILITIES, network);
+ mEventListener.onUpstreamEvent(EVENT_ON_CAPABILITIES, uns);
}
private @Nullable UpstreamNetworkState updateLinkProperties(@NonNull Network network,
@@ -411,7 +409,7 @@
private void handleLinkProp(Network network, LinkProperties newLp) {
final UpstreamNetworkState ns = updateLinkProperties(network, newLp);
if (ns != null) {
- notifyTarget(EVENT_ON_LINKPROPERTIES, ns);
+ mEventListener.onUpstreamEvent(EVENT_ON_LINKPROPERTIES, ns);
}
}
@@ -438,7 +436,7 @@
// preferable upstream, do so now and notify the target. Likewise,
// if the current upstream network is gone, notify the target of the
// fact that we now have no upstream at all.
- notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
+ mEventListener.onUpstreamEvent(EVENT_ON_LOST, mNetworkMap.remove(network));
}
private void maybeHandleNetworkSwitch(@NonNull Network network) {
@@ -456,14 +454,14 @@
// Default network changed. Update local data and notify tethering.
Log.d(TAG, "New default Internet network: " + network);
mDefaultInternetNetwork = network;
- notifyTarget(EVENT_DEFAULT_SWITCHED, ns);
+ mEventListener.onUpstreamEvent(EVENT_DEFAULT_SWITCHED, ns);
}
private void recomputeLocalPrefixes() {
final HashSet<IpPrefix> localPrefixes = allLocalPrefixes(mNetworkMap.values());
if (!mLocalPrefixes.equals(localPrefixes)) {
mLocalPrefixes = localPrefixes;
- notifyTarget(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
+ mEventListener.onUpstreamEvent(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone());
}
}
@@ -502,12 +500,13 @@
// onLinkPropertiesChanged right after this method and mDefaultInternetNetwork will
// be updated then.
//
- // Technically, not updating here isn't necessary, because the notifications to
- // Tethering sent by notifyTarget are messages sent to a state machine running on
- // the same thread as this method, and so cannot arrive until after this method has
- // returned. However, it is not a good idea to rely on that because fact that
- // Tethering uses multiple state machines running on the same thread is a major
- // source of race conditions and something that should be fixed.
+ // Technically, mDefaultInternetNetwork could be updated here, because the
+ // Callback#onChange implementation sends messages to the state machine running
+ // on the same thread as this method. If there is new default network change,
+ // the message cannot arrive until onLinkPropertiesChanged returns.
+ // However, it is not a good idea to rely on that because fact that Tethering uses
+ // multiple state machines running on the same thread is a major source of race
+ // conditions and something that should be fixed.
//
// TODO: is it correct that this code always updates EntitlementManager?
// This code runs when the default network connects or changes capabilities, but the
@@ -551,7 +550,7 @@
mIsDefaultCellularUpstream = false;
mEntitlementMgr.notifyUpstream(false);
Log.d(TAG, "Lost default Internet network: " + network);
- notifyTarget(EVENT_DEFAULT_SWITCHED, null);
+ mEventListener.onUpstreamEvent(EVENT_DEFAULT_SWITCHED, null);
return;
}
@@ -569,14 +568,6 @@
if (cb != null) cm().unregisterNetworkCallback(cb);
}
- private void notifyTarget(int which, Network network) {
- notifyTarget(which, mNetworkMap.get(network));
- }
-
- private void notifyTarget(int which, Object obj) {
- mTarget.sendMessage(mWhat, which, 0, obj);
- }
-
private static class TypeStatePair {
public int type = TYPE_NONE;
public UpstreamNetworkState ns = null;
@@ -698,4 +689,10 @@
public void setPreferTestNetworks(boolean prefer) {
mPreferTestNetworks = prefer;
}
+
+ /** An interface to notify upstream network changes. */
+ public interface EventListener {
+ /** Notify the client of some event */
+ void onUpstreamEvent(int what, Object obj);
+ }
}
diff --git a/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
new file mode 100644
index 0000000..078a35f
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/StateMachineShim.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.util;
+
+import android.annotation.Nullable;
+import android.os.Looper;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo;
+
+import java.util.List;
+
+/** A wrapper to decide whether use synchronous state machine for tethering. */
+public class StateMachineShim {
+ // Exactly one of mAsyncSM or mSyncSM is non-null.
+ private final AsyncStateMachine mAsyncSM;
+ private final SyncStateMachine mSyncSM;
+
+ /**
+ * The Looper parameter is only needed for AsyncSM, so if looper is null, the shim will be
+ * created for SyncSM.
+ */
+ public StateMachineShim(final String name, @Nullable final Looper looper) {
+ this(name, looper, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public StateMachineShim(final String name, @Nullable final Looper looper,
+ final Dependencies deps) {
+ if (looper == null) {
+ mAsyncSM = null;
+ mSyncSM = deps.makeSyncStateMachine(name, Thread.currentThread());
+ } else {
+ mAsyncSM = deps.makeAsyncStateMachine(name, looper);
+ mSyncSM = null;
+ }
+ }
+
+ /** A dependencies class which used for testing injection. */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Create SyncSM instance, for injection. */
+ public SyncStateMachine makeSyncStateMachine(final String name, final Thread thread) {
+ return new SyncStateMachine(name, thread);
+ }
+
+ /** Create AsyncSM instance, for injection. */
+ public AsyncStateMachine makeAsyncStateMachine(final String name, final Looper looper) {
+ return new AsyncStateMachine(name, looper);
+ }
+ }
+
+ /** Start the state machine */
+ public void start(final State initialState) {
+ if (mSyncSM != null) {
+ mSyncSM.start(initialState);
+ } else {
+ mAsyncSM.setInitialState(initialState);
+ mAsyncSM.start();
+ }
+ }
+
+ /** Add states to state machine. */
+ public void addAllStates(final List<StateInfo> stateInfos) {
+ if (mSyncSM != null) {
+ mSyncSM.addAllStates(stateInfos);
+ } else {
+ for (final StateInfo info : stateInfos) {
+ mAsyncSM.addState(info.state, info.parent);
+ }
+ }
+ }
+
+ /**
+ * Transition to given state.
+ *
+ * SyncSM doesn't allow this be called during state transition (#enter() or #exit() methods),
+ * or multiple times while processing a single message.
+ */
+ public void transitionTo(final State state) {
+ if (mSyncSM != null) {
+ mSyncSM.transitionTo(state);
+ } else {
+ mAsyncSM.transitionTo(state);
+ }
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what) {
+ sendMessage(what, 0, 0, null);
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what, Object obj) {
+ sendMessage(what, 0, 0, obj);
+ }
+
+ /** Send message to state machine. */
+ public void sendMessage(int what, int arg1) {
+ sendMessage(what, arg1, 0, null);
+ }
+
+ /**
+ * Send message to state machine.
+ *
+ * If using asynchronous state machine, putting the message into looper's message queue.
+ * Tethering runs on single looper thread that ipServers and mainSM all share with same message
+ * queue. The enqueued message will be processed by asynchronous state machine when all the
+ * messages before such enqueued message are processed.
+ * If using synchronous state machine, the message is processed right away without putting into
+ * looper's message queue.
+ */
+ public void sendMessage(int what, int arg1, int arg2, Object obj) {
+ if (mSyncSM != null) {
+ mSyncSM.processMessage(what, arg1, arg2, obj);
+ } else {
+ mAsyncSM.sendMessage(what, arg1, arg2, obj);
+ }
+ }
+
+ /**
+ * Send message after delayMillis millisecond.
+ *
+ * This can only be used with async state machine, so this will throw if using sync state
+ * machine.
+ */
+ public void sendMessageDelayedToAsyncSM(final int what, final long delayMillis) {
+ if (mSyncSM != null) {
+ throw new IllegalStateException("sendMessageDelayed can only be used with async SM");
+ }
+
+ mAsyncSM.sendMessageDelayed(what, delayMillis);
+ }
+
+ /**
+ * Enqueue a message to the front of the queue.
+ * Protected, may only be called by instances of async state machine.
+ *
+ * Message is ignored if state machine has quit.
+ */
+ protected void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+ if (mSyncSM != null) {
+ throw new IllegalStateException("sendMessageAtFrontOfQueue can only be used with"
+ + " async SM");
+ }
+
+ mAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1);
+ }
+
+ /**
+ * Send self message.
+ * This can only be used with sync state machine, so this will throw if using async state
+ * machine.
+ */
+ public void sendSelfMessageToSyncSM(final int what, final Object obj) {
+ if (mSyncSM == null) {
+ throw new IllegalStateException("sendSelfMessage can only be used with sync SM");
+ }
+
+ mSyncSM.sendSelfMessage(what, 0, 0, obj);
+ }
+
+ /**
+ * An alias StateMahchine class with public construtor.
+ *
+ * Since StateMachine.java only provides protected construtor, adding a child class so that this
+ * shim could create StateMachine instance.
+ */
+ @VisibleForTesting
+ public static class AsyncStateMachine extends StateMachine {
+ public AsyncStateMachine(final String name, final Looper looper) {
+ super(name, looper);
+ }
+
+ /** Enqueue a message to the front of the queue for this state machine. */
+ public void sendMessageAtFrontOfQueueToAsyncSM(int what, int arg1) {
+ sendMessageAtFrontOfQueue(what, arg1);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java b/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
index e94febb..e845f3f 100644
--- a/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
+++ b/Tethering/src/com/android/networkstack/tethering/wear/CompanionDeviceManagerProxy.java
@@ -19,7 +19,7 @@
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
import android.content.Context;
-import android.net.connectivity.TiramisuConnectivityInternalApiUtil;
+import android.net.connectivity.ConnectivityInternalApiUtil;
import android.net.wear.ICompanionDeviceManagerProxy;
import android.os.Build;
import android.os.RemoteException;
@@ -39,7 +39,7 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public CompanionDeviceManagerProxy(Context context) {
mService = ICompanionDeviceManagerProxy.Stub.asInterface(
- TiramisuConnectivityInternalApiUtil.getCompanionDeviceManagerProxyService(context));
+ ConnectivityInternalApiUtil.getCompanionDeviceManagerProxyService(context));
}
/**
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 0702aa7..377da91 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -146,8 +146,6 @@
private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
private final PackageManager mPackageManager = mContext.getPackageManager();
private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
- private final UiAutomation mUiAutomation =
- InstrumentationRegistry.getInstrumentation().getUiAutomation();
// Late initialization in setUp()
private boolean mRunTests;
@@ -276,7 +274,6 @@
} finally {
mHandlerThread.quitSafely();
mHandlerThread.join();
- mUiAutomation.dropShellPermissionIdentity();
}
}
@@ -366,6 +363,11 @@
private volatile Collection<TetheredClient> mClients = null;
private volatile Network mUpstream = null;
+ // The dnsmasq in R might block netd for 20 seconds, which can also block tethering
+ // enable/disable for 20 seconds. To fix this, changing the timeouts from 5 seconds to 30
+ // seconds. See b/289881008.
+ private static final int EXPANDED_TIMEOUT_MS = 30000;
+
MyTetheringEventCallback(TetheringManager tm, String iface) {
this(tm, iface, null);
mAcceptAnyUpstream = true;
@@ -424,13 +426,13 @@
}
public void awaitInterfaceTethered() throws Exception {
- assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms",
- mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue("Ethernet not tethered after " + EXPANDED_TIMEOUT_MS + "ms",
+ mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
public void awaitInterfaceLocalOnly() throws Exception {
- assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms",
- mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue("Ethernet not local-only after " + EXPANDED_TIMEOUT_MS + "ms",
+ mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
// Used to check if the callback has registered. When the callback is registered,
@@ -444,8 +446,9 @@
}
public void awaitCallbackRegistered() throws Exception {
- if (!mCallbackRegisteredLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- fail("Did not receive callback registered signal after " + TIMEOUT_MS + "ms");
+ if (!mCallbackRegisteredLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ fail("Did not receive callback registered signal after " + EXPANDED_TIMEOUT_MS
+ + "ms");
}
}
@@ -457,11 +460,11 @@
if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return;
if (mInterfaceWasTethered) {
- assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
- mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms",
+ mTetheringStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} else if (mInterfaceWasLocalOnly) {
- assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
- mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms",
+ mLocalOnlyStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} else {
fail(mIface + " cannot be both tethered and local-only. Update this test class.");
}
@@ -488,8 +491,9 @@
}
public Collection<TetheredClient> awaitClientConnected() throws Exception {
- assertTrue("Did not receive client connected callback after " + TIMEOUT_MS + "ms",
- mClientConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue("Did not receive client connected callback after "
+ + EXPANDED_TIMEOUT_MS + "ms",
+ mClientConnectedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS));
return mClients;
}
@@ -506,10 +510,10 @@
}
public Network awaitUpstreamChanged(boolean throwTimeoutException) throws Exception {
- if (!mUpstreamLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ if (!mUpstreamLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
final String errorMessage = "Did not receive upstream "
+ (mAcceptAnyUpstream ? "any" : mExpectedUpstream)
- + " callback after " + TIMEOUT_MS + "ms";
+ + " callback after " + EXPANDED_TIMEOUT_MS + "ms";
if (throwTimeoutException) {
throw new TimeoutException(errorMessage);
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 076fde3..4949eaa 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -47,7 +47,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.test.filters.MediumTest;
+import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.Ipv6Utils;
@@ -79,7 +79,7 @@
import java.util.concurrent.TimeoutException;
@RunWith(AndroidJUnit4.class)
-@MediumTest
+@LargeTest
public class EthernetTetheringTest extends EthernetTetheringTestBase {
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
index 328e3fb..dac5b63 100644
--- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -16,8 +16,6 @@
package android.net.ip;
-import static android.net.RouteInfo.RTN_UNICAST;
-
import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
@@ -42,12 +40,13 @@
import android.net.INetd;
import android.net.IpPrefix;
import android.net.MacAddress;
-import android.net.RouteInfo;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -55,7 +54,6 @@
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.Ipv6Utils;
-import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.Struct;
import com.android.net.module.util.structs.EthernetHeader;
import com.android.net.module.util.structs.Icmpv6Header;
@@ -80,7 +78,6 @@
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.HashSet;
-import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -332,10 +329,12 @@
// Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to
// send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local
// address is present).
- final String iface = mTetheredParams.name;
- final RouteInfo linkLocalRoute =
- new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
- NetdUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+ try {
+ sNetd.networkAddRoute(INetd.LOCAL_NET_ID, mTetheredParams.name,
+ "fe80::/64", INetd.NEXTHOP_NONE);
+ } catch (RemoteException | ServiceSpecificException e) {
+ throw new IllegalStateException(e);
+ }
final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
mTetheredPacketReader.sendResponse(rs);
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
index 81d4fbe..60f2d17 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java
@@ -44,6 +44,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.StructNlMsgHdr;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -84,6 +85,14 @@
mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ }
+
void findConnectionOrThrow(FileDescriptor fd, InetSocketAddress local, InetSocketAddress remote)
throws Exception {
Log.d(TAG, "Looking for socket " + local + " -> " + remote);
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index c0718d1..a7064e8 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -34,15 +34,10 @@
import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
import static android.net.ip.IpServer.STATE_TETHERED;
import static android.net.ip.IpServer.STATE_UNAVAILABLE;
-import static android.system.OsConstants.ETH_P_IPV6;
+import static android.net.ip.IpServer.getTetherableIpv6Prefixes;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
-import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
-import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
-import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
-import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
-import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -56,15 +51,14 @@
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
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;
@@ -80,8 +74,7 @@
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
-import android.net.TetherOffloadRuleParcel;
-import android.net.TetherStatsParcel;
+import android.net.RoutingCoordinatorManager;
import android.net.dhcp.DhcpServerCallbacks;
import android.net.dhcp.DhcpServingParamsParcel;
import android.net.dhcp.IDhcpEventCallbacks;
@@ -94,35 +87,16 @@
import android.os.test.TestLooper;
import android.text.TextUtils;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.net.module.util.BpfMap;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
-import com.android.net.module.util.Struct.S32;
-import com.android.net.module.util.bpf.Tether4Key;
-import com.android.net.module.util.bpf.Tether4Value;
-import com.android.net.module.util.bpf.TetherStatsKey;
-import com.android.net.module.util.bpf.TetherStatsValue;
-import com.android.net.module.util.ip.ConntrackMonitor;
import com.android.net.module.util.ip.IpNeighborMonitor;
-import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
-import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
import com.android.networkstack.tethering.BpfCoordinator;
-import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
-import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
-import com.android.networkstack.tethering.Tether6Value;
-import com.android.networkstack.tethering.TetherDevKey;
-import com.android.networkstack.tethering.TetherDevValue;
-import com.android.networkstack.tethering.TetherDownstream6Key;
-import com.android.networkstack.tethering.TetherLimitKey;
-import com.android.networkstack.tethering.TetherLimitValue;
-import com.android.networkstack.tethering.TetherUpstream6Key;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.networkstack.tethering.metrics.TetheringMetrics;
import com.android.networkstack.tethering.util.InterfaceSet;
@@ -136,17 +110,15 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.Inet4Address;
-import java.net.Inet6Address;
import java.net.InetAddress;
-import java.util.Arrays;
import java.util.List;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -158,6 +130,7 @@
private static final String UPSTREAM_IFACE = "upstream0";
private static final String UPSTREAM_IFACE2 = "upstream1";
private static final String IPSEC_IFACE = "ipsec0";
+ private static final int NO_UPSTREAM = 0;
private static final int UPSTREAM_IFINDEX = 101;
private static final int UPSTREAM_IFINDEX2 = 102;
private static final int IPSEC_IFINDEX = 103;
@@ -183,6 +156,18 @@
private final LinkAddress mTestAddress = new LinkAddress("192.168.42.5/24");
private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
+ private static final Set<LinkAddress> NO_ADDRESSES = Set.of();
+ private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES =
+ Set.of(new LinkAddress("2001:db8:0:1234::168/64"));
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES =
+ Set.of(new IpPrefix("2001:db8:0:1234::/64"));
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 = Set.of(
+ new LinkAddress("2001:db8:0:1234::168/64"),
+ new LinkAddress("2001:db8:0:abcd::168/64"));
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES2 = Set.of(
+ new IpPrefix("2001:db8:0:1234::/64"), new IpPrefix("2001:db8:0:abcd::/64"));
+
@Mock private INetd mNetd;
@Mock private IpServer.Callback mCallback;
@Mock private SharedLog mSharedLog;
@@ -192,29 +177,21 @@
@Mock private IpNeighborMonitor mIpNeighborMonitor;
@Mock private IpServer.Dependencies mDependencies;
@Mock private PrivateAddressCoordinator mAddressCoordinator;
+ private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
+ new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
- @Mock private ConntrackMonitor mConntrackMonitor;
@Mock private TetheringMetrics mTetheringMetrics;
- @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
- @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
- @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
- @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
- @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
- @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
- @Mock private BpfMap<TetherDevKey, TetherDevValue> mBpfDevMap;
- @Mock private BpfMap<S32, S32> mBpfErrorMap;
+ @Mock private BpfCoordinator mBpfCoordinator;
@Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
- private final TestLooper mLooper = new TestLooper();
+ private TestLooper mLooper;
+ private Handler mHandler;
private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
ArgumentCaptor.forClass(LinkProperties.class);
private IpServer mIpServer;
private InterfaceConfigurationParcel mInterfaceConfiguration;
- private NeighborEventConsumer mNeighborEventConsumer;
- private BpfCoordinator mBpfCoordinator;
- private BpfCoordinator.Dependencies mBpfDeps;
private void initStateMachine(int interfaceType) throws Exception {
initStateMachine(interfaceType, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
@@ -236,46 +213,47 @@
mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH;
}
- ArgumentCaptor<NeighborEventConsumer> neighborCaptor =
- ArgumentCaptor.forClass(NeighborEventConsumer.class);
- doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(),
- neighborCaptor.capture());
+ doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(), any());
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
- // Recreate mBpfCoordinator again here because mTetherConfig has changed
- mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
- mIpServer = new IpServer(
- IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
- mCallback, mTetherConfig, mAddressCoordinator, mTetheringMetrics, mDependencies);
+ when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
+ mIpServer = createIpServer(interfaceType);
+ verify(mIpNeighborMonitor).start();
mIpServer.start();
- mNeighborEventConsumer = neighborCaptor.getValue();
// Starting the state machine always puts us in a consistent state and notifies
// the rest of the world that we've changed from an unknown to available state.
mLooper.dispatchAll();
- reset(mNetd, mCallback);
+ reset(mNetd, mCallback, mIpNeighborMonitor);
when(mRaDaemon.start()).thenReturn(true);
}
private void initTetheredStateMachine(int interfaceType, String upstreamIface)
throws Exception {
- initTetheredStateMachine(interfaceType, upstreamIface, false,
+ initTetheredStateMachine(interfaceType, upstreamIface, NO_ADDRESSES, false,
DEFAULT_USING_BPF_OFFLOAD);
}
private void initTetheredStateMachine(int interfaceType, String upstreamIface,
- boolean usingLegacyDhcp, boolean usingBpfOffload) throws Exception {
+ Set<LinkAddress> upstreamAddresses, boolean usingLegacyDhcp, boolean usingBpfOffload)
+ throws Exception {
initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload);
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
if (upstreamIface != null) {
+ InterfaceParams interfaceParams = mDependencies.getInterfaceParams(upstreamIface);
+ assertNotNull("missing upstream interface: " + upstreamIface, interfaceParams);
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(upstreamIface);
+ lp.setLinkAddresses(upstreamAddresses);
dispatchTetherConnectionChanged(upstreamIface, lp, 0);
+ Set<IpPrefix> upstreamPrefixes = getTetherableIpv6Prefixes(lp.getLinkAddresses());
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, interfaceParams.index, upstreamPrefixes);
}
- reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+ reset(mNetd, mBpfCoordinator, mCallback, mAddressCoordinator);
when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
anyBoolean())).thenReturn(mTestAddress);
}
@@ -303,90 +281,42 @@
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
- mBpfDeps = new BpfCoordinator.Dependencies() {
- @NonNull
- public Handler getHandler() {
- return new Handler(mLooper.getLooper());
- }
-
- @NonNull
- public INetd getNetd() {
- return mNetd;
- }
-
- @NonNull
- public NetworkStatsManager getNetworkStatsManager() {
- return mStatsManager;
- }
-
- @NonNull
- public SharedLog getSharedLog() {
- return mSharedLog;
- }
-
- @Nullable
- public TetheringConfiguration getTetherConfig() {
- return mTetherConfig;
- }
-
- @NonNull
- public ConntrackMonitor getConntrackMonitor(
- ConntrackMonitor.ConntrackEventConsumer consumer) {
- return mConntrackMonitor;
- }
-
- @Nullable
- public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
- return mBpfDownstream4Map;
- }
-
- @Nullable
- public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
- return mBpfUpstream4Map;
- }
-
- @Nullable
- public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
- return mBpfDownstream6Map;
- }
-
- @Nullable
- public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
- return mBpfUpstream6Map;
- }
-
- @Nullable
- public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
- return mBpfStatsMap;
- }
-
- @Nullable
- public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
- return mBpfLimitMap;
- }
-
- @Nullable
- public BpfMap<TetherDevKey, TetherDevValue> getBpfDevMap() {
- return mBpfDevMap;
- }
-
- @Nullable
- public BpfMap<S32, S32> getBpfErrorMap() {
- return mBpfErrorMap;
- }
- };
- mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
+ // Simulate the behavior of RoutingCoordinator
+ if (null != mRoutingCoordinatorManager.value) {
+ doAnswer(it -> {
+ final String fromIface = (String) it.getArguments()[0];
+ final String toIface = (String) it.getArguments()[1];
+ mNetd.tetherAddForward(fromIface, toIface);
+ mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
+ return null;
+ }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
+ doAnswer(it -> {
+ final String fromIface = (String) it.getArguments()[0];
+ final String toIface = (String) it.getArguments()[1];
+ mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
+ mNetd.tetherRemoveForward(fromIface, toIface);
+ return null;
+ }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
+ }
setUpDhcpServer();
}
+ // In order to interact with syncSM from the test, IpServer must be created in test thread.
+ private IpServer createIpServer(final int interfaceType) {
+ mLooper = new TestLooper();
+ mHandler = new Handler(mLooper.getLooper());
+ return new IpServer(IFACE_NAME, mHandler, interfaceType, mSharedLog, mNetd, mBpfCoordinator,
+ mRoutingCoordinatorManager, mCallback, mTetherConfig, mAddressCoordinator,
+ mTetheringMetrics, mDependencies);
+
+ }
+
@Test
- public void startsOutAvailable() {
+ public void startsOutAvailable() throws Exception {
when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
.thenReturn(mIpNeighborMonitor);
- mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
- mNetd, mBpfCoordinator, mCallback, mTetherConfig, mAddressCoordinator,
- mTetheringMetrics, mDependencies);
+ mIpServer = createIpServer(TETHERING_BLUETOOTH);
mIpServer.start();
mLooper.dispatchAll();
verify(mCallback).updateInterfaceState(
@@ -531,7 +461,7 @@
InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
- inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX,
+ inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX,
UPSTREAM_IFACE);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
@@ -553,7 +483,7 @@
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
- inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
UPSTREAM_IFACE2);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
@@ -578,7 +508,7 @@
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
// tetherAddForward.
- inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
UPSTREAM_IFACE2);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
@@ -606,7 +536,7 @@
// Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
// ipfwdAddInterfaceForward.
- inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ inOrder.verify(mBpfCoordinator).maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2,
UPSTREAM_IFACE2);
inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
@@ -627,7 +557,12 @@
inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
- inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
+ inOrder.verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ // When tethering stops, upstream interface is set to zero and thus clearing all upstream
+ // rules. Downstream rules are needed to be cleared explicitly by calling
+ // BpfCoordinator#clearAllIpv6Rules in TetheredState#exit.
+ inOrder.verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
@@ -813,456 +748,89 @@
@Test
public void doesNotStartDhcpServerIfDisabled() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, NO_ADDRESSES,
+ true /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
verify(mDependencies, never()).makeDhcpServer(any(), any(), any());
}
- private InetAddress addr(String addr) throws Exception {
- return InetAddresses.parseNumericAddress(addr);
- }
-
- private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
- mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
- nudState, mac));
- mLooper.dispatchAll();
- }
-
- private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
- mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
- nudState, mac));
- mLooper.dispatchAll();
- }
-
- /**
- * Custom ArgumentMatcher for TetherOffloadRuleParcel. This is needed because generated stable
- * AIDL classes don't have equals(), so we cannot just use eq(). A custom assert, such as:
- *
- * private void checkFooCalled(StableParcelable p, ...) {
- * ArgumentCaptor<FooParam> captor = ArgumentCaptor.forClass(FooParam.class);
- * verify(mMock).foo(captor.capture());
- * Foo foo = captor.getValue();
- * assertFooMatchesExpectations(foo);
- * }
- *
- * almost works, but not quite. This is because if the code under test calls foo() twice, the
- * first call to checkFooCalled() matches both the calls, putting both calls into the captor,
- * and then fails with TooManyActualInvocations. It also makes it harder to use other mockito
- * features such as never(), inOrder(), etc.
- *
- * This approach isn't great because if the match fails, the error message is unhelpful
- * (actual: "android.net.TetherOffloadRuleParcel@8c827b0" or some such), but at least it does
- * work.
- *
- * TODO: consider making the error message more readable by adding a method that catching the
- * AssertionFailedError and throwing a new assertion with more details. See
- * NetworkMonitorTest#verifyNetworkTested.
- *
- * See ConnectivityServiceTest#assertRoutesAdded for an alternative approach which solves the
- * TooManyActualInvocations problem described above by forcing the caller of the custom assert
- * method to specify all expected invocations in one call. This is useful when the stable
- * parcelable class being asserted on has a corresponding Java object (eg., RouteInfo and
- * RouteInfoParcelable), and the caller can just pass in a list of them. It not useful here
- * because there is no such object.
- */
- private static class TetherOffloadRuleParcelMatcher implements
- ArgumentMatcher<TetherOffloadRuleParcel> {
- public final int upstreamIfindex;
- public final InetAddress dst;
- public final MacAddress dstMac;
-
- TetherOffloadRuleParcelMatcher(int upstreamIfindex, InetAddress dst, MacAddress dstMac) {
- this.upstreamIfindex = upstreamIfindex;
- this.dst = dst;
- this.dstMac = dstMac;
- }
-
- public boolean matches(TetherOffloadRuleParcel parcel) {
- return upstreamIfindex == parcel.inputInterfaceIndex
- && (TEST_IFACE_PARAMS.index == parcel.outputInterfaceIndex)
- && Arrays.equals(dst.getAddress(), parcel.destination)
- && (128 == parcel.prefixLength)
- && Arrays.equals(TEST_IFACE_PARAMS.macAddr.toByteArray(), parcel.srcL2Address)
- && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address);
- }
-
- public String toString() {
- return String.format("TetherOffloadRuleParcelMatcher(%d, %s, %s",
- upstreamIfindex, dst.getHostAddress(), dstMac);
- }
- }
-
- @NonNull
- private static TetherOffloadRuleParcel matches(
- int upstreamIfindex, InetAddress dst, MacAddress dstMac) {
- return argThat(new TetherOffloadRuleParcelMatcher(upstreamIfindex, dst, dstMac));
- }
-
- @NonNull
- private static Ipv6DownstreamRule makeDownstreamRule(int upstreamIfindex,
- @NonNull InetAddress dst, @NonNull MacAddress dstMac) {
- return new Ipv6DownstreamRule(upstreamIfindex, TEST_IFACE_PARAMS.index,
- (Inet6Address) dst, TEST_IFACE_PARAMS.macAddr, dstMac);
- }
-
- @NonNull
- private static TetherDownstream6Key makeDownstream6Key(int upstreamIfindex,
- @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst) {
- return new TetherDownstream6Key(upstreamIfindex, upstreamMac, dst.getAddress());
- }
-
- @NonNull
- private static Tether6Value makeDownstream6Value(@NonNull final MacAddress dstMac) {
- return new Tether6Value(TEST_IFACE_PARAMS.index, dstMac,
- TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
- }
-
- private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
- if (inOrder != null) {
- return inOrder.verify(t);
- } else {
- return verify(t);
- }
- }
-
- private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex,
- @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
- @NonNull final MacAddress dstMac) throws Exception {
- if (mBpfDeps.isAtLeastS()) {
- verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
- makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
- makeDownstream6Value(dstMac));
- } else {
- verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst,
- dstMac));
- }
- }
-
- private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex,
- @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
- @NonNull final MacAddress dstMac) throws Exception {
- if (mBpfDeps.isAtLeastS()) {
- verify(mBpfDownstream6Map, never()).updateEntry(
- makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
- makeDownstream6Value(dstMac));
- } else {
- verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac));
- }
- }
-
- private void verifyNeverTetherOffloadRuleAdd() throws Exception {
- if (mBpfDeps.isAtLeastS()) {
- verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
- } else {
- verify(mNetd, never()).tetherOffloadRuleAdd(any());
- }
- }
-
- private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex,
- @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
- @NonNull final MacAddress dstMac) throws Exception {
- if (mBpfDeps.isAtLeastS()) {
- verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(makeDownstream6Key(
- upstreamIfindex, upstreamMac, dst));
- } else {
- // |dstMac| is not required for deleting rules. Used bacause tetherOffloadRuleRemove
- // uses a whole rule to be a argument.
- // See system/netd/server/TetherController.cpp/TetherController#removeOffloadRule.
- verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(upstreamIfindex, dst,
- dstMac));
- }
- }
-
- private void verifyNeverTetherOffloadRuleRemove() throws Exception {
- if (mBpfDeps.isAtLeastS()) {
- verify(mBpfDownstream6Map, never()).deleteEntry(any());
- } else {
- verify(mNetd, never()).tetherOffloadRuleRemove(any());
- }
- }
-
- private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex)
- throws Exception {
- if (!mBpfDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
- TEST_IFACE_PARAMS.macAddr, 0);
- final Tether6Value value = new Tether6Value(upstreamIfindex,
- MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
- ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
- verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
- }
-
- private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder)
- throws Exception {
- if (!mBpfDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
- TEST_IFACE_PARAMS.macAddr, 0);
- verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
- }
-
- private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
- if (!mBpfDeps.isAtLeastS()) return;
- if (inOrder != null) {
- inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any());
- inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
- inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
- } else {
- verify(mBpfUpstream6Map, never()).deleteEntry(any());
- verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
- verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
- }
- }
-
- @NonNull
- private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
- TetherStatsParcel parcel = new TetherStatsParcel();
- parcel.ifIndex = ifIndex;
- return parcel;
- }
-
- private void resetNetdBpfMapAndCoordinator() throws Exception {
- reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfCoordinator);
- // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
- // potentially crash the test) if the stats map is empty.
- when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
- when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
- .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
- when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2))
- .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2));
- // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
- // potentially crash the test) if the stats map is empty.
- final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0);
- when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros);
- when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros);
- }
-
@Test
- public void addRemoveipv6ForwardingRules() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
+ public void ipv6UpstreamInterfaceChanges() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
- final int myIfindex = TEST_IFACE_PARAMS.index;
- final int notMyIfindex = myIfindex - 1;
-
- final InetAddress neighA = InetAddresses.parseNumericAddress("2001:db8::1");
- final InetAddress neighB = InetAddresses.parseNumericAddress("2001:db8::2");
- final InetAddress neighLL = InetAddresses.parseNumericAddress("fe80::1");
- final InetAddress neighMC = InetAddresses.parseNumericAddress("ff02::1234");
- final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
- final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
- final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
-
- resetNetdBpfMapAndCoordinator();
- verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-
- // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
- // tetherOffloadGetAndClearStats in netd while the rules are changed.
-
- // Events on other interfaces are ignored.
- recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-
- // Events on this interface are received and sent to netd.
- recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
- verify(mBpfCoordinator).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
- verifyTetherOffloadRuleAdd(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
- resetNetdBpfMapAndCoordinator();
-
- recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
- verify(mBpfCoordinator).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
- verifyTetherOffloadRuleAdd(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyNoUpstreamIpv6ForwardingChange(null);
- resetNetdBpfMapAndCoordinator();
-
- // Link-local and multicast neighbors are ignored.
- recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
- recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
-
- // A neighbor that is no longer valid causes the rule to be removed.
- // NUD_FAILED events do not have a MAC address.
- recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
- verify(mBpfCoordinator).removeIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macNull));
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull);
- verifyNoUpstreamIpv6ForwardingChange(null);
- resetNetdBpfMapAndCoordinator();
-
- // A neighbor that is deleted causes the rule to be removed.
- recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
- verify(mBpfCoordinator).removeIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macNull));
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
- verifyStopUpstreamIpv6Forwarding(null);
- resetNetdBpfMapAndCoordinator();
-
- // Upstream changes result in updating the rules.
- recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
- recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
- resetNetdBpfMapAndCoordinator();
-
- InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ // Upstream interface changes result in updating the rules.
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE2);
+ lp.setLinkAddresses(UPSTREAM_ADDRESSES);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
- verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2);
- verifyTetherOffloadRuleRemove(inOrder,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
- verifyTetherOffloadRuleRemove(inOrder,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(inOrder);
- verifyTetherOffloadRuleAdd(inOrder,
- UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
- verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
- verifyTetherOffloadRuleAdd(inOrder,
- UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
- verifyNoUpstreamIpv6ForwardingChange(inOrder);
- resetNetdBpfMapAndCoordinator();
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+ reset(mBpfCoordinator);
+
+ // Upstream link addresses change result in updating the rules.
+ LinkProperties lp2 = new LinkProperties();
+ lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ reset(mBpfCoordinator);
// When the upstream is lost, rules are removed.
dispatchTetherConnectionChanged(null, null, 0);
- // Clear function is called two times by:
+ // Upstream clear function is called two times by:
// - processMessage CMD_TETHER_CONNECTION_CHANGED for the upstream is lost.
// - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
// See dispatchTetherConnectionChanged.
- verify(mBpfCoordinator, times(2)).tetherOffloadRuleClear(mIpServer);
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(inOrder);
- resetNetdBpfMapAndCoordinator();
+ verify(mBpfCoordinator, times(2)).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ reset(mBpfCoordinator);
// If the upstream is IPv4-only, no rules are added.
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- resetNetdBpfMapAndCoordinator();
- recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
- // Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost.
- verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
- verifyNoUpstreamIpv6ForwardingChange(null);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ verify(mBpfCoordinator, never()).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ reset(mBpfCoordinator);
- // Rules can be added again once upstream IPv6 connectivity is available.
+ // Rules are added again once upstream IPv6 connectivity is available.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
- verify(mBpfCoordinator).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
- verifyTetherOffloadRuleAdd(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
- verify(mBpfCoordinator, never()).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
- verifyNeverTetherOffloadRuleAdd(
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ reset(mBpfCoordinator);
// If upstream IPv6 connectivity is lost, rules are removed.
- resetNetdBpfMapAndCoordinator();
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
- verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(null);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ reset(mBpfCoordinator);
- // When the interface goes down, rules are removed.
+ // When upstream IPv6 connectivity comes back, rules are added.
lp.setInterfaceName(UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1);
- recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
- recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
- verify(mBpfCoordinator).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighA, macA));
- verifyTetherOffloadRuleAdd(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
- verify(mBpfCoordinator).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neighB, macB));
- verifyTetherOffloadRuleAdd(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- resetNetdBpfMapAndCoordinator();
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ reset(mBpfCoordinator);
+
+ // When the downstream interface goes down, rules are removed.
+ mIpServer.stop();
+ mLooper.dispatchAll();
+ verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer);
+ verify(mBpfCoordinator).updateAllIpv6Rules(
+ mIpServer, TEST_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ reset(mBpfCoordinator);
+ }
+
+ @Test
+ public void stopNeighborMonitoringWhenInterfaceDown() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, UPSTREAM_ADDRESSES,
+ false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
mIpServer.stop();
mLooper.dispatchAll();
- verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
- verifyStopUpstreamIpv6Forwarding(null);
verify(mIpNeighborMonitor).stop();
- resetNetdBpfMapAndCoordinator();
- }
-
- @Test
- public void enableDisableUsingBpfOffload() throws Exception {
- final int myIfindex = TEST_IFACE_PARAMS.index;
- final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
- final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
- final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
-
- // Expect that rules can be only added/removed when the BPF offload config is enabled.
- // Note that the BPF offload disabled case is not a realistic test case. Because IP
- // neighbor monitor doesn't start if BPF offload is disabled, there should have no
- // neighbor event listening. This is used for testing the protection check just in case.
- // TODO: Perhaps remove the BPF offload disabled case test once this check isn't needed
- // anymore.
-
- // [1] Enable BPF offload.
- // A neighbor that is added or deleted causes the rule to be added or removed.
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- true /* usingBpfOffload */);
- resetNetdBpfMapAndCoordinator();
-
- recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
- verify(mBpfCoordinator).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neigh, macA));
- verifyTetherOffloadRuleAdd(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macA);
- verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
- resetNetdBpfMapAndCoordinator();
-
- recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
- verify(mBpfCoordinator).removeIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(UPSTREAM_IFINDEX, neigh, macNull));
- verifyTetherOffloadRuleRemove(null,
- UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull);
- verifyStopUpstreamIpv6Forwarding(null);
- resetNetdBpfMapAndCoordinator();
-
- // [2] Disable BPF offload.
- // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- false /* usingBpfOffload */);
- resetNetdBpfMapAndCoordinator();
-
- recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
- verifyNeverTetherOffloadRuleAdd();
- verifyNoUpstreamIpv6ForwardingChange(null);
- resetNetdBpfMapAndCoordinator();
-
- recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
- verifyNeverTetherOffloadRuleRemove();
- verifyNoUpstreamIpv6ForwardingChange(null);
- resetNetdBpfMapAndCoordinator();
- }
-
- @Test
- public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- false /* usingBpfOffload */);
-
- // IP neighbor monitor doesn't start if BPF offload is disabled.
- verify(mIpNeighborMonitor, never()).start();
}
private LinkProperties buildIpv6OnlyLinkProperties(final String iface) {
@@ -1518,75 +1086,4 @@
public void testDadProxyUpdates_EnabledAfterR() throws Exception {
checkDadProxyEnabled(true);
}
-
- @Test
- public void testSkipVirtualNetworkInBpf() throws Exception {
- initTetheredStateMachine(TETHERING_BLUETOOTH, null);
- final LinkProperties v6Only = new LinkProperties();
- v6Only.setInterfaceName(IPSEC_IFACE);
- dispatchTetherConnectionChanged(IPSEC_IFACE, v6Only, 0);
-
- verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, IPSEC_IFACE);
- verify(mNetd).tetherAddForward(IFACE_NAME, IPSEC_IFACE);
- verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, IPSEC_IFACE);
-
- final int myIfindex = TEST_IFACE_PARAMS.index;
- final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1");
- final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
- recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, mac);
- verify(mBpfCoordinator, never()).addIpv6DownstreamRule(
- mIpServer, makeDownstreamRule(IPSEC_IFINDEX, neigh, mac));
- }
-
- // TODO: move to BpfCoordinatorTest once IpNeighborMonitor is migrated to BpfCoordinator.
- @Test
- public void addRemoveTetherClient() throws Exception {
- initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
- DEFAULT_USING_BPF_OFFLOAD);
-
- final int myIfindex = TEST_IFACE_PARAMS.index;
- final int notMyIfindex = myIfindex - 1;
-
- final InetAddress neighA = InetAddresses.parseNumericAddress("192.168.80.1");
- final InetAddress neighB = InetAddresses.parseNumericAddress("192.168.80.2");
- final InetAddress neighLL = InetAddresses.parseNumericAddress("169.254.0.1");
- final InetAddress neighMC = InetAddresses.parseNumericAddress("224.0.0.1");
- final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00");
- final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
- final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
-
- // Events on other interfaces are ignored.
- recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator);
-
- // Events on this interface are received and sent to BpfCoordinator.
- recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
- verify(mBpfCoordinator).tetherOffloadClientAdd(mIpServer, new ClientInfo(myIfindex,
- TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighA, macA));
- clearInvocations(mBpfCoordinator);
-
- recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
- verify(mBpfCoordinator).tetherOffloadClientAdd(mIpServer, new ClientInfo(myIfindex,
- TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighB, macB));
- clearInvocations(mBpfCoordinator);
-
- // Link-local and multicast neighbors are ignored.
- recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator);
- recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator);
- clearInvocations(mBpfCoordinator);
-
- // A neighbor that is no longer valid causes the client to be removed.
- // NUD_FAILED events do not have a MAC address.
- recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
- verify(mBpfCoordinator).tetherOffloadClientRemove(mIpServer, new ClientInfo(myIfindex,
- TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighA, macNull));
- clearInvocations(mBpfCoordinator);
-
- // A neighbor that is deleted causes the client to be removed.
- recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
- verify(mBpfCoordinator).tetherOffloadClientRemove(mIpServer, new ClientInfo(myIfindex,
- TEST_IFACE_PARAMS.macAddr, (Inet4Address) neighB, macNull));
- }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 04eb430..47ecf58 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -25,6 +25,8 @@
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.net.ip.IpServer.STATE_TETHERED;
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
import static android.system.OsConstants.ETH_P_IP;
import static android.system.OsConstants.ETH_P_IPV6;
@@ -43,6 +45,11 @@
import static com.android.net.module.util.netlink.ConntrackMessage.TupleProto;
import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH;
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
+import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU;
import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
@@ -55,7 +62,9 @@
import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+import static com.android.testutils.MiscAsserts.assertSameElements;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -70,10 +79,15 @@
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
@@ -86,12 +100,16 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkStats;
+import android.net.RoutingCoordinatorManager;
import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
import android.net.ip.IpServer;
+import android.net.ip.RouterAdvertisementDaemon;
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.SparseArray;
import androidx.annotation.NonNull;
@@ -101,10 +119,12 @@
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.Struct.S32;
import com.android.net.module.util.bpf.Tether4Key;
@@ -113,6 +133,9 @@
import com.android.net.module.util.bpf.TetherStatsValue;
import com.android.net.module.util.ip.ConntrackMonitor;
import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer;
+import com.android.net.module.util.ip.IpNeighborMonitor;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEvent;
+import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer;
import com.android.net.module.util.netlink.ConntrackMessage;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkUtils;
@@ -120,6 +143,8 @@
import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule;
+import com.android.networkstack.tethering.metrics.TetheringMetrics;
+import com.android.networkstack.tethering.util.InterfaceSet;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -136,6 +161,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
+import org.mockito.verification.VerificationMode;
import java.io.StringWriter;
import java.net.Inet4Address;
@@ -145,7 +171,9 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -159,25 +187,46 @@
private static final int TEST_NET_ID = 24;
private static final int TEST_NET_ID2 = 25;
- private static final int INVALID_IFINDEX = 0;
+ private static final int NO_UPSTREAM = 0;
private static final int UPSTREAM_IFINDEX = 1001;
private static final int UPSTREAM_XLAT_IFINDEX = 1002;
private static final int UPSTREAM_IFINDEX2 = 1003;
private static final int DOWNSTREAM_IFINDEX = 2001;
private static final int DOWNSTREAM_IFINDEX2 = 2002;
+ private static final int IPSEC_IFINDEX = 103;
private static final String UPSTREAM_IFACE = "rmnet0";
private static final String UPSTREAM_XLAT_IFACE = "v4-rmnet0";
private static final String UPSTREAM_IFACE2 = "wlan0";
+ private static final String DOWNSTREAM_IFACE = "downstream1";
+ private static final String DOWNSTREAM_IFACE2 = "downstream2";
+ private static final String IPSEC_IFACE = "ipsec0";
private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
private static final MacAddress DOWNSTREAM_MAC2 = MacAddress.fromString("ab:90:78:56:34:12");
private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
+ private static final MacAddress MAC_NULL = MacAddress.fromString("00:00:00:00:00:00");
- private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
- private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+ private static final LinkAddress UPSTREAM_ADDRESS = new LinkAddress("2001:db8:0:1234::168/64");
+ private static final LinkAddress UPSTREAM_ADDRESS2 = new LinkAddress("2001:db8:0:abcd::168/64");
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES = Set.of(UPSTREAM_ADDRESS);
+ private static final Set<LinkAddress> UPSTREAM_ADDRESSES2 =
+ Set.of(UPSTREAM_ADDRESS, UPSTREAM_ADDRESS2);
+ private static final IpPrefix UPSTREAM_PREFIX = new IpPrefix("2001:db8:0:1234::/64");
+ private static final IpPrefix UPSTREAM_PREFIX2 = new IpPrefix("2001:db8:0:abcd::/64");
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES = Set.of(UPSTREAM_PREFIX);
+ private static final Set<IpPrefix> UPSTREAM_PREFIXES2 =
+ Set.of(UPSTREAM_PREFIX, UPSTREAM_PREFIX2);
+ private static final Set<IpPrefix> NO_PREFIXES = Set.of();
+
+ private static final InetAddress NEIGH_A =
+ InetAddresses.parseNumericAddress("2001:db8:0:1234::1");
+ private static final InetAddress NEIGH_B =
+ InetAddresses.parseNumericAddress("2001:db8:0:1234::2");
+ private static final InetAddress NEIGH_LL = InetAddresses.parseNumericAddress("fe80::1");
+ private static final InetAddress NEIGH_MC = InetAddresses.parseNumericAddress("ff02::1234");
private static final Inet4Address REMOTE_ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
@@ -193,7 +242,6 @@
private static final Inet4Address XLAT_LOCAL_IPV4ADDR =
(Inet4Address) InetAddresses.parseNumericAddress("192.0.0.46");
private static final IpPrefix NAT64_IP_PREFIX = new IpPrefix("64:ff9b::/96");
- private static final IpPrefix IPV6_ZERO_PREFIX = new IpPrefix("::/64");
// Generally, public port and private port are the same in the NAT conntrack message.
// TODO: consider using different private port and public port for testing.
@@ -213,6 +261,14 @@
private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams(
UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.fromString("44:55:66:00:00:0c"),
NetworkStackConstants.ETHER_MTU);
+ private static final InterfaceParams DOWNSTREAM_IFACE_PARAMS = new InterfaceParams(
+ DOWNSTREAM_IFACE, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, NetworkStackConstants.ETHER_MTU);
+ private static final InterfaceParams DOWNSTREAM_IFACE_PARAMS2 = new InterfaceParams(
+ DOWNSTREAM_IFACE2, DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2,
+ NetworkStackConstants.ETHER_MTU);
+ private static final InterfaceParams IPSEC_IFACE_PARAMS = new InterfaceParams(
+ IPSEC_IFACE, IPSEC_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS,
+ NetworkStackConstants.ETHER_MTU);
private static final Map<Integer, UpstreamInformation> UPSTREAM_INFORMATIONS = Map.of(
UPSTREAM_IFINDEX, new UpstreamInformation(UPSTREAM_IFACE_PARAMS,
@@ -392,6 +448,14 @@
@Mock private IpServer mIpServer2;
@Mock private TetheringConfiguration mTetherConfig;
@Mock private ConntrackMonitor mConntrackMonitor;
+ @Mock private IpNeighborMonitor mIpNeighborMonitor;
+ @Mock private RouterAdvertisementDaemon mRaDaemon;
+ @Mock private IpServer.Dependencies mIpServerDeps;
+ @Mock private IpServer.Callback mIpServerCallback;
+ @Mock private PrivateAddressCoordinator mAddressCoordinator;
+ private final LateSdk<RoutingCoordinatorManager> mRoutingCoordinatorManager =
+ new LateSdk<>(SdkLevel.isAtLeastS() ? mock(RoutingCoordinatorManager.class) : null);
+ @Mock private TetheringMetrics mTetheringMetrics;
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
@@ -400,6 +464,7 @@
// Late init since the object must be initialized by the BPF coordinator instance because
// it has to access the non-static function of BPF coordinator.
private BpfConntrackEventConsumer mConsumer;
+ private NeighborEventConsumer mNeighborEventConsumer;
private HashMap<IpServer, HashMap<Inet4Address, ClientInfo>> mTetherClients;
private long mElapsedRealtimeNanos = 0;
@@ -407,6 +472,7 @@
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
+ private final Handler mHandler = new Handler(mTestLooper.getLooper());
private final IBpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map =
spy(new TestBpfMap<>(Tether4Key.class, Tether4Value.class));
private final IBpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map =
@@ -427,7 +493,7 @@
spy(new BpfCoordinator.Dependencies() {
@NonNull
public Handler getHandler() {
- return new Handler(mTestLooper.getLooper());
+ return mHandler;
}
@NonNull
@@ -507,6 +573,24 @@
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
+
+ // Simulate the behavior of RoutingCoordinator
+ if (null != mRoutingCoordinatorManager.value) {
+ doAnswer(it -> {
+ final String fromIface = (String) it.getArguments()[0];
+ final String toIface = (String) it.getArguments()[1];
+ mNetd.tetherAddForward(fromIface, toIface);
+ mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
+ return null;
+ }).when(mRoutingCoordinatorManager.value).addInterfaceForward(any(), any());
+ doAnswer(it -> {
+ final String fromIface = (String) it.getArguments()[0];
+ final String toIface = (String) it.getArguments()[1];
+ mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
+ mNetd.tetherRemoveForward(fromIface, toIface);
+ return null;
+ }).when(mRoutingCoordinatorManager.value).removeInterfaceForward(any(), any());
+ }
}
private void waitForIdle() {
@@ -520,7 +604,68 @@
}
@NonNull
+ private IpServer makeAndStartIpServer(String interfaceName, BpfCoordinator bpfCoordinator)
+ throws Exception {
+ final LinkAddress testAddress = new LinkAddress("192.168.42.5/24");
+ when(mIpServerDeps.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
+ when(mIpServerDeps.getInterfaceParams(DOWNSTREAM_IFACE)).thenReturn(
+ DOWNSTREAM_IFACE_PARAMS);
+ when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
+ when(mIpServerDeps.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
+ when(mIpServerDeps.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS);
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+ anyBoolean())).thenReturn(testAddress);
+ when(mRaDaemon.start()).thenReturn(true);
+ ArgumentCaptor<NeighborEventConsumer> neighborEventCaptor =
+ ArgumentCaptor.forClass(NeighborEventConsumer.class);
+ doReturn(mIpNeighborMonitor).when(mIpServerDeps).getIpNeighborMonitor(any(), any(),
+ neighborEventCaptor.capture());
+ final IpServer ipServer = new IpServer(
+ interfaceName, mHandler, TETHERING_WIFI, new SharedLog("test"), mNetd,
+ bpfCoordinator, mRoutingCoordinatorManager, mIpServerCallback, mTetherConfig,
+ mAddressCoordinator, mTetheringMetrics, mIpServerDeps);
+ ipServer.start();
+ ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ mTestLooper.dispatchAll();
+
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ lp.setLinkAddresses(UPSTREAM_ADDRESSES);
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, 0);
+
+ mNeighborEventConsumer = neighborEventCaptor.getValue();
+ return ipServer;
+ }
+
+ private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface,
+ LinkProperties v6lp, int ttlAdjustment) {
+ dispatchTetherConnectionChanged(ipServer, upstreamIface);
+ ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, ttlAdjustment, 0, v6lp);
+ mTestLooper.dispatchAll();
+ }
+
+ private void dispatchTetherConnectionChanged(IpServer ipServer, String upstreamIface) {
+ final InterfaceSet ifs = (upstreamIface != null) ? new InterfaceSet(upstreamIface) : null;
+ ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifs);
+ mTestLooper.dispatchAll();
+ }
+
+ private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
+ mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr,
+ nudState, mac));
+ mTestLooper.dispatchAll();
+ }
+
+ private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) {
+ mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr,
+ nudState, mac));
+ mTestLooper.dispatchAll();
+ }
+
+ @NonNull
private BpfCoordinator makeBpfCoordinator() throws Exception {
+ // mStatsManager will be invoked twice if BpfCoordinator is created the second time.
+ clearInvocations(mStatsManager);
final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
@@ -617,10 +762,14 @@
}
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ return verifyWithOrder(inOrder, t, times(1));
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t, VerificationMode mode) {
if (inOrder != null) {
- return inOrder.verify(t);
+ return inOrder.verify(t, mode);
} else {
- return verify(t);
+ return verify(t, mode);
}
}
@@ -640,22 +789,49 @@
}
}
- private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
- MacAddress downstreamMac, int upstreamIfindex) throws Exception {
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
if (!mDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac, 0);
- final Tether6Value value = new Tether6Value(upstreamIfindex,
- MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
- ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
- verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+ for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+ final byte[] prefix64 = prefixToIp64(upstreamPrefix);
+ final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index,
+ DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, ETH_P_IPV6,
+ NetworkStackConstants.ETHER_MTU);
+ expected.put(key, value);
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ ArgumentCaptor<Tether6Value> valueCaptor =
+ ArgumentCaptor.forClass(Tether6Value.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+ keyCaptor.capture(), valueCaptor.capture());
+ List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+ List<Tether6Value> values = valueCaptor.getAllValues();
+ ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+ for (int i = 0; i < keys.size(); i++) {
+ captured.put(keys.get(i), values.get(i));
+ }
+ assertEquals(expected, captured);
}
- private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
- MacAddress downstreamMac)
- throws Exception {
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder,
+ @NonNull Set<IpPrefix> upstreamPrefixes) throws Exception {
if (!mDeps.isAtLeastS()) return;
- final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac, 0);
- verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ Set<TetherUpstream6Key> expected = new ArraySet<>();
+ for (IpPrefix upstreamPrefix : upstreamPrefixes) {
+ final byte[] prefix64 = prefixToIp64(upstreamPrefix);
+ final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index,
+ DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64);
+ expected.add(key);
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+ keyCaptor.capture());
+ assertEquals(expected, new ArraySet(keyCaptor.getAllValues()));
}
private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
@@ -671,6 +847,39 @@
}
}
+ private void verifyAddUpstreamRule(@Nullable InOrder inOrder,
+ @NonNull Ipv6UpstreamRule rule) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(
+ rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+ }
+
+ private void verifyAddUpstreamRules(@Nullable InOrder inOrder,
+ @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ ArrayMap<TetherUpstream6Key, Tether6Value> expected = new ArrayMap<>();
+ for (Ipv6UpstreamRule rule : rules) {
+ expected.put(rule.makeTetherUpstream6Key(), rule.makeTether6Value());
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ ArgumentCaptor<Tether6Value> valueCaptor =
+ ArgumentCaptor.forClass(Tether6Value.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).insertEntry(
+ keyCaptor.capture(), valueCaptor.capture());
+ List<TetherUpstream6Key> keys = keyCaptor.getAllValues();
+ List<Tether6Value> values = valueCaptor.getAllValues();
+ ArrayMap<TetherUpstream6Key, Tether6Value> captured = new ArrayMap<>();
+ for (int i = 0; i < keys.size(); i++) {
+ captured.put(keys.get(i), values.get(i));
+ }
+ assertEquals(expected, captured);
+ }
+
+ private void verifyAddDownstreamRule(@NonNull Ipv6DownstreamRule rule) throws Exception {
+ verifyAddDownstreamRule(null, rule);
+ }
+
private void verifyAddDownstreamRule(@Nullable InOrder inOrder,
@NonNull Ipv6DownstreamRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -681,6 +890,11 @@
}
}
+ private void verifyNeverAddUpstreamRule() throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ }
+
private void verifyNeverAddDownstreamRule() throws Exception {
if (mDeps.isAtLeastS()) {
verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
@@ -689,6 +903,32 @@
}
}
+ private void verifyRemoveUpstreamRule(@Nullable InOrder inOrder,
+ @NonNull final Ipv6UpstreamRule rule) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(
+ rule.makeTetherUpstream6Key());
+ }
+
+ private void verifyRemoveUpstreamRules(@Nullable InOrder inOrder,
+ @NonNull Set<Ipv6UpstreamRule> rules) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ List<TetherUpstream6Key> expected = new ArrayList<>();
+ for (Ipv6UpstreamRule rule : rules) {
+ expected.add(rule.makeTetherUpstream6Key());
+ }
+ ArgumentCaptor<TetherUpstream6Key> keyCaptor =
+ ArgumentCaptor.forClass(TetherUpstream6Key.class);
+ verifyWithOrder(inOrder, mBpfUpstream6Map, times(expected.size())).deleteEntry(
+ keyCaptor.capture());
+ assertSameElements(expected, keyCaptor.getAllValues());
+ }
+
+ private void verifyRemoveDownstreamRule(@NonNull final Ipv6DownstreamRule rule)
+ throws Exception {
+ verifyRemoveDownstreamRule(null, rule);
+ }
+
private void verifyRemoveDownstreamRule(@Nullable InOrder inOrder,
@NonNull final Ipv6DownstreamRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -699,6 +939,11 @@
}
}
+ private void verifyNeverRemoveUpstreamRule() throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ }
+
private void verifyNeverRemoveDownstreamRule() throws Exception {
if (mDeps.isAtLeastS()) {
verify(mBpfDownstream6Map, never()).deleteEntry(any());
@@ -763,24 +1008,33 @@
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// InOrder is required because mBpfStatsMap may be accessed by both
// BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
// The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called
// mBpfStatsMap#getValue and get a wrong calling count which counts all.
- final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
- final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
- coordinator.addIpv6DownstreamRule(mIpServer, rule);
- verifyAddDownstreamRule(inOrder, rule);
+ final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfDownstream6Map, mBpfLimitMap,
+ mBpfStatsMap);
+ final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(
+ mobileIfIndex, NEIGH_A, MAC_A);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
+ verifyAddUpstreamRule(inOrder, upstreamRule);
+ coordinator.addIpv6DownstreamRule(mIpServer, downstreamRule);
+ verifyAddDownstreamRule(inOrder, downstreamRule);
- // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+ // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.removeIpv6DownstreamRule(mIpServer, rule);
- verifyRemoveDownstreamRule(inOrder, rule);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verifyRemoveDownstreamRule(inOrder, downstreamRule);
+ verifyRemoveUpstreamRule(inOrder, upstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
}
@@ -806,7 +1060,7 @@
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)});
@@ -847,8 +1101,8 @@
// Add interface name to lookup table. In realistic case, the upstream interface name will
// be added by IpServer when IpServer has received with a new IPv6 upstream update event.
- coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(wlanIfIndex, wlanIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// [1] Both interface stats are changed.
// Setup the tether stats of wlan and mobile interface. Note that move forward the time of
@@ -912,7 +1166,7 @@
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 100;
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// Verify that set quota to 0 will immediately triggers a callback.
mTetherStatsProvider.onSetAlert(0);
@@ -939,8 +1193,37 @@
mTetherStatsProviderCb.assertNoCallback();
}
- // The custom ArgumentMatcher simply comes from IpServerTest.
- // TODO: move both of them into a common utility class for reusing the code.
+ /**
+ * Custom ArgumentMatcher for TetherOffloadRuleParcel. This is needed because generated stable
+ * AIDL classes don't have equals(), so we cannot just use eq(). A custom assert, such as:
+ *
+ * private void checkFooCalled(StableParcelable p, ...) {
+ * ArgumentCaptor<@FooParam> captor = ArgumentCaptor.forClass(FooParam.class);
+ * verify(mMock).foo(captor.capture());
+ * Foo foo = captor.getValue();
+ * assertFooMatchesExpectations(foo);
+ * }
+ *
+ * almost works, but not quite. This is because if the code under test calls foo() twice, the
+ * first call to checkFooCalled() matches both the calls, putting both calls into the captor,
+ * and then fails with TooManyActualInvocations. It also makes it harder to use other mockito
+ * features such as never(), inOrder(), etc.
+ *
+ * This approach isn't great because if the match fails, the error message is unhelpful
+ * (actual: "android.net.TetherOffloadRuleParcel@8c827b0" or some such), but at least it does
+ * work.
+ *
+ * TODO: consider making the error message more readable by adding a method that catching the
+ * AssertionFailedError and throwing a new assertion with more details. See
+ * NetworkMonitorTest#verifyNetworkTested.
+ *
+ * See ConnectivityServiceTest#assertRoutesAdded for an alternative approach which solves the
+ * TooManyActualInvocations problem described above by forcing the caller of the custom assert
+ * method to specify all expected invocations in one call. This is useful when the stable
+ * parcelable class being asserted on has a corresponding Java object (eg., RouteInfo and
+ * RouteInfoParcelable), and the caller can just pass in a list of them. It not useful here
+ * because there is no such object.
+ */
private static class TetherOffloadRuleParcelMatcher implements
ArgumentMatcher<TetherOffloadRuleParcel> {
public final int upstreamIfindex;
@@ -978,10 +1261,10 @@
}
@NonNull
- private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex) {
- return new Ipv6UpstreamRule(upstreamIfindex, DOWNSTREAM_IFINDEX,
- IPV6_ZERO_PREFIX, DOWNSTREAM_MAC, MacAddress.ALL_ZEROS_ADDRESS,
- MacAddress.ALL_ZEROS_ADDRESS);
+ private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex,
+ int downstreamIfindex, @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) {
+ return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
}
@NonNull
@@ -992,34 +1275,74 @@
}
@Test
- public void testRuleMakeTetherDownstream6Key() throws Exception {
+ public void testIpv6DownstreamRuleMakeTetherDownstream6Key() throws Exception {
final int mobileIfIndex = 100;
final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
- assertEquals(key.iif, mobileIfIndex);
- assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS); // rawip upstream
- assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
+ assertEquals(mobileIfIndex, key.iif);
+ assertEquals(MacAddress.ALL_ZEROS_ADDRESS, key.dstMac); // rawip upstream
+ assertArrayEquals(NEIGH_A.getAddress(), key.neigh6);
// iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28.
assertEquals(28, key.writeToBytes().length);
}
@Test
- public void testRuleMakeTether6Value() throws Exception {
+ public void testIpv6DownstreamRuleMakeTether6Value() throws Exception {
final int mobileIfIndex = 100;
final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
final Tether6Value value = rule.makeTether6Value();
- assertEquals(value.oif, DOWNSTREAM_IFINDEX);
- assertEquals(value.ethDstMac, MAC_A);
- assertEquals(value.ethSrcMac, DOWNSTREAM_MAC);
- assertEquals(value.ethProto, ETH_P_IPV6);
- assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU);
- // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20.
+ assertEquals(DOWNSTREAM_IFINDEX, value.oif);
+ assertEquals(MAC_A, value.ethDstMac);
+ assertEquals(DOWNSTREAM_MAC, value.ethSrcMac);
+ assertEquals(ETH_P_IPV6, value.ethProto);
+ assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu);
+ // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20
assertEquals(20, value.writeToBytes().length);
}
@Test
+ public void testIpv6UpstreamRuleMakeTetherUpstream6Key() {
+ final byte[] bytes = new byte[]{(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0xab, (byte) 0xcd, (byte) 0xfe, (byte) 0x00};
+ final IpPrefix prefix = new IpPrefix("2001:db8:abcd:fe00::/64");
+ final Ipv6UpstreamRule rule = buildTestUpstreamRule(UPSTREAM_IFINDEX,
+ DOWNSTREAM_IFINDEX, prefix, DOWNSTREAM_MAC);
+
+ final TetherUpstream6Key key = rule.makeTetherUpstream6Key();
+ assertEquals(DOWNSTREAM_IFINDEX, key.iif);
+ assertEquals(DOWNSTREAM_MAC, key.dstMac);
+ assertArrayEquals(bytes, key.src64);
+ // iif (4) + dstMac (6) + padding (6) + src64 (8) = 24
+ assertEquals(24, key.writeToBytes().length);
+ }
+
+ @Test
+ public void testIpv6UpstreamRuleMakeTether6Value() {
+ final IpPrefix prefix = new IpPrefix("2001:db8:abcd:fe00::/64");
+ final Ipv6UpstreamRule rule = buildTestUpstreamRule(UPSTREAM_IFINDEX,
+ DOWNSTREAM_IFINDEX, prefix, DOWNSTREAM_MAC);
+
+ final Tether6Value value = rule.makeTether6Value();
+ assertEquals(UPSTREAM_IFINDEX, value.oif);
+ assertEquals(MAC_NULL, value.ethDstMac);
+ assertEquals(MAC_NULL, value.ethSrcMac);
+ assertEquals(ETH_P_IPV6, value.ethProto);
+ assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu);
+ // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20
+ assertEquals(20, value.writeToBytes().length);
+ }
+
+ @Test
+ public void testBytesToPrefix() {
+ final byte[] bytes = new byte[]{(byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+ (byte) 0x00, (byte) 0x00, (byte) 0x12, (byte) 0x34};
+ final IpPrefix prefix = new IpPrefix("2001:db8:0:1234::/64");
+ assertEquals(prefix, BpfCoordinator.bytesToPrefix(bytes));
+ }
+
+ @Test
public void testSetDataLimit() throws Exception {
setupFunctioningNetdInterface();
@@ -1027,17 +1350,19 @@
final String mobileIface = "rmnet_data0";
final int mobileIfIndex = 100;
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// [1] Default limit.
// Set the unlimited quota as default if the service has never applied a data limit for a
// given upstream. Note that the data limit only be applied on an upstream which has rules.
- final Ipv6DownstreamRule rule = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
- final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
- coordinator.addIpv6DownstreamRule(mIpServer, rule);
- verifyAddDownstreamRule(inOrder, rule);
+ final Ipv6UpstreamRule rule = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
+ verifyAddUpstreamRule(inOrder, rule);
inOrder.verifyNoMoreInteractions();
// [2] Specific limit.
@@ -1062,7 +1387,6 @@
}
}
- // TODO: Test the case in which the rules are changed from different IpServer objects.
@Test
public void testSetDataLimitOnRule6Change() throws Exception {
setupFunctioningNetdInterface();
@@ -1071,39 +1395,45 @@
final String mobileIface = "rmnet_data0";
final int mobileIfIndex = 100;
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
// Applying a data limit to the current upstream does not take any immediate action.
// The data limit could be only set on an upstream which has rules.
final long limit = 12345;
- final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+ final InOrder inOrder = inOrder(mNetd, mBpfUpstream6Map, mBpfLimitMap, mBpfStatsMap);
mTetherStatsProvider.onSetLimit(mobileIface, limit);
waitForIdle();
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
- // Adding the first rule on current upstream immediately sends the quota to netd.
- final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(mobileIfIndex, NEIGH_A, MAC_A);
- coordinator.addIpv6DownstreamRule(mIpServer, ruleA);
- verifyAddDownstreamRule(inOrder, ruleA);
+ // Adding the first rule on current upstream immediately sends the quota to BPF.
+ final Ipv6UpstreamRule ruleA = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
+ verifyAddUpstreamRule(inOrder, ruleA);
inOrder.verifyNoMoreInteractions();
- // Adding the second rule on current upstream does not send the quota to netd.
- final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(mobileIfIndex, NEIGH_B, MAC_B);
- coordinator.addIpv6DownstreamRule(mIpServer, ruleB);
- verifyAddDownstreamRule(inOrder, ruleB);
+ // Adding the second rule on current upstream does not send the quota to BPF.
+ final Ipv6UpstreamRule ruleB = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX2, UPSTREAM_PREFIX, DOWNSTREAM_MAC2);
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, mobileIfIndex, UPSTREAM_PREFIXES);
+ verifyAddUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
- // Removing the second rule on current upstream does not send the quota to netd.
- coordinator.removeIpv6DownstreamRule(mIpServer, ruleB);
- verifyRemoveDownstreamRule(inOrder, ruleB);
+ // Removing the second rule on current upstream does not send the quota to BPF.
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, NO_UPSTREAM, NO_PREFIXES);
+ verifyRemoveUpstreamRule(inOrder, ruleB);
verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
- // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+ // Removing the last rule on current upstream immediately sends the cleanup stuff to BPF.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
- coordinator.removeIpv6DownstreamRule(mIpServer, ruleA);
- verifyRemoveDownstreamRule(inOrder, ruleA);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, NO_UPSTREAM, NO_PREFIXES);
+ verifyRemoveUpstreamRule(inOrder, ruleA);
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
inOrder.verifyNoMoreInteractions();
}
@@ -1118,8 +1448,8 @@
final String mobileIface = "rmnet_data0";
final Integer ethIfIndex = 100;
final Integer mobileIfIndex = 101;
- coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
- coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+ coordinator.maybeAddUpstreamToLookupTable(ethIfIndex, ethIface);
+ coordinator.maybeAddUpstreamToLookupTable(mobileIfIndex, mobileIface);
final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
mBpfStatsMap);
@@ -1133,20 +1463,28 @@
// [1] Adding rules on the upstream Ethernet.
// Note that the default data limit is applied after the first rule is added.
+ final Ipv6UpstreamRule ethernetUpstreamRule = buildTestUpstreamRule(
+ ethIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
final Ipv6DownstreamRule ethernetRuleA = buildTestDownstreamRule(
ethIfIndex, NEIGH_A, MAC_A);
final Ipv6DownstreamRule ethernetRuleB = buildTestDownstreamRule(
ethIfIndex, NEIGH_B, MAC_B);
- coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
- verifyAddDownstreamRule(inOrder, ethernetRuleA);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, ethIfIndex, UPSTREAM_PREFIXES);
verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
- verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex);
+ verifyAddUpstreamRule(inOrder, ethernetUpstreamRule);
+ coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleA);
+ verifyAddDownstreamRule(inOrder, ethernetRuleA);
coordinator.addIpv6DownstreamRule(mIpServer, ethernetRuleB);
verifyAddDownstreamRule(inOrder, ethernetRuleB);
// [2] Update the existing rules from Ethernet to cellular.
+ final Ipv6UpstreamRule mobileUpstreamRule = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ final Ipv6UpstreamRule mobileUpstreamRule2 = buildTestUpstreamRule(
+ mobileIfIndex, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX2, DOWNSTREAM_MAC);
final Ipv6DownstreamRule mobileRuleA = buildTestDownstreamRule(
mobileIfIndex, NEIGH_A, MAC_A);
final Ipv6DownstreamRule mobileRuleB = buildTestDownstreamRule(
@@ -1155,26 +1493,26 @@
buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
// Update the existing rules for upstream changes. The rules are removed and re-added one
- // by one for updating upstream interface index by #tetherOffloadRuleUpdate.
- coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
+ // by one for updating upstream interface index and prefixes by #tetherOffloadRuleUpdate.
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, mobileIfIndex, UPSTREAM_PREFIXES2);
verifyRemoveDownstreamRule(inOrder, ethernetRuleA);
verifyRemoveDownstreamRule(inOrder, ethernetRuleB);
- verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ verifyRemoveUpstreamRule(inOrder, ethernetUpstreamRule);
verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
- verifyAddDownstreamRule(inOrder, mobileRuleA);
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
true /* isInit */);
- verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
- mobileIfIndex);
+ verifyAddUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
+ verifyAddDownstreamRule(inOrder, mobileRuleA);
verifyAddDownstreamRule(inOrder, mobileRuleB);
// [3] Clear all rules for a given IpServer.
updateStatsEntryForTetherOffloadGetAndClearStats(
buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
- coordinator.tetherOffloadRuleClear(mIpServer);
+ coordinator.clearAllIpv6Rules(mIpServer);
verifyRemoveDownstreamRule(inOrder, mobileRuleA);
verifyRemoveDownstreamRule(inOrder, mobileRuleB);
- verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ verifyRemoveUpstreamRules(inOrder, Set.of(mobileUpstreamRule, mobileUpstreamRule2));
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
// [4] Force pushing stats update to verify that the last diff of stats is reported on all
@@ -1204,7 +1542,7 @@
// The interface name lookup table can't be added.
final String iface = "rmnet_data0";
final Integer ifIndex = 100;
- coordinator.addUpstreamNameToLookupTable(ifIndex, iface);
+ coordinator.maybeAddUpstreamToLookupTable(ifIndex, iface);
assertEquals(0, coordinator.getInterfaceNamesForTesting().size());
// The rule can't be added.
@@ -1230,14 +1568,15 @@
assertEquals(1, rules.size());
// The rule can't be cleared.
- coordinator.tetherOffloadRuleClear(mIpServer);
+ coordinator.clearAllIpv6Rules(mIpServer);
verifyNeverRemoveDownstreamRule();
rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
// The rule can't be updated.
- coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
+ coordinator.updateAllIpv6Rules(mIpServer, DOWNSTREAM_IFACE_PARAMS,
+ rule.upstreamIfindex + 1 /* new */, UPSTREAM_PREFIXES);
verifyNeverRemoveDownstreamRule();
verifyNeverAddDownstreamRule();
rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer);
@@ -1533,12 +1872,12 @@
//
// @param coordinator BpfCoordinator instance.
// @param upstreamIfindex upstream interface index. can be the following values.
- // INVALID_IFINDEX: no upstream interface
+ // NO_UPSTREAM: no upstream interface
// UPSTREAM_IFINDEX: CELLULAR (raw ip interface)
// UPSTREAM_IFINDEX2: WIFI (ethernet interface)
private void setUpstreamInformationTo(final BpfCoordinator coordinator,
@Nullable Integer upstreamIfindex) {
- if (upstreamIfindex == INVALID_IFINDEX) {
+ if (upstreamIfindex == NO_UPSTREAM) {
coordinator.updateUpstreamNetworkState(null);
return;
}
@@ -1552,7 +1891,7 @@
// interface index.
doReturn(upstreamInfo.interfaceParams).when(mDeps).getInterfaceParams(
upstreamInfo.interfaceParams.name);
- coordinator.addUpstreamNameToLookupTable(upstreamInfo.interfaceParams.index,
+ coordinator.maybeAddUpstreamToLookupTable(upstreamInfo.interfaceParams.index,
upstreamInfo.interfaceParams.name);
final LinkProperties lp = new LinkProperties();
@@ -1677,19 +2016,23 @@
public void testAddDevMapRule6() throws Exception {
final BpfCoordinator coordinator = makeBpfCoordinator();
- coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
- final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
- final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
-
- coordinator.addIpv6DownstreamRule(mIpServer, ruleA);
+ coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+ coordinator.updateAllIpv6Rules(
+ mIpServer, DOWNSTREAM_IFACE_PARAMS, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
eq(new TetherDevValue(UPSTREAM_IFINDEX)));
verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)),
eq(new TetherDevValue(DOWNSTREAM_IFINDEX)));
clearInvocations(mBpfDevMap);
- coordinator.addIpv6DownstreamRule(mIpServer, ruleB);
- verify(mBpfDevMap, never()).updateEntry(any(), any());
+ // Adding the second downstream, only the second downstream ifindex is added to DevMap,
+ // the existing upstream ifindex won't be added again.
+ coordinator.updateAllIpv6Rules(
+ mIpServer2, DOWNSTREAM_IFACE_PARAMS2, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)),
+ eq(new TetherDevValue(DOWNSTREAM_IFINDEX2)));
+ verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)),
+ eq(new TetherDevValue(UPSTREAM_IFINDEX)));
}
@Test
@@ -1966,6 +2309,10 @@
100 /* nonzero, CT_NEW */);
}
+ private static byte[] prefixToIp64(IpPrefix prefix) {
+ return Arrays.copyOf(prefix.getRawAddress(), 8);
+ }
+
void checkRule4ExistInUpstreamDownstreamMap() throws Exception {
assertEquals(UPSTREAM4_RULE_VALUE_A, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A));
assertEquals(DOWNSTREAM4_RULE_VALUE_A, mBpfDownstream4Map.getValue(
@@ -2033,7 +2380,7 @@
assertNull(mTetherClients.get(mIpServer2));
}
- private void asseertClientInfoExist(@NonNull IpServer ipServer,
+ private void assertClientInfoExists(@NonNull IpServer ipServer,
@NonNull ClientInfo clientInfo) {
HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
assertNotNull(clients);
@@ -2042,16 +2389,16 @@
// Although either ClientInfo for a given downstream (IpServer) is not found or a given
// client address is not found on a given downstream can be treated "ClientInfo not
- // exist", we still want to know the real reason exactly. For example, we don't the
+ // exist", we still want to know the real reason exactly. For example, we don't know the
// exact reason in the following:
- // assertNull(clients == null ? clients : clients.get(clientInfo.clientAddress));
+ // assertNull(clients == null ? clients : clients.get(clientAddress));
// This helper only verifies the case that the downstream still has at least one client.
// In other words, ClientInfo for a given IpServer has not been removed yet.
- private void asseertClientInfoNotExist(@NonNull IpServer ipServer,
- @NonNull ClientInfo clientInfo) {
+ private void assertClientInfoDoesNotExist(@NonNull IpServer ipServer,
+ @NonNull Inet4Address clientAddress) {
HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
assertNotNull(clients);
- assertNull(clients.get(clientInfo.clientAddress));
+ assertNull(clients.get(clientAddress));
}
@Test
@@ -2083,12 +2430,12 @@
// [3] Switch upstream from the first upstream (rawip, bpf supported) to no upstream. Clear
// all rules.
- setUpstreamInformationTo(coordinator, INVALID_IFINDEX);
+ setUpstreamInformationTo(coordinator, NO_UPSTREAM);
checkRule4NotExistInUpstreamDownstreamMap();
// Client information should be not deleted.
- asseertClientInfoExist(mIpServer, CLIENT_INFO_A);
- asseertClientInfoExist(mIpServer2, CLIENT_INFO_B);
+ assertClientInfoExists(mIpServer, CLIENT_INFO_A);
+ assertClientInfoExists(mIpServer2, CLIENT_INFO_B);
}
@Test
@@ -2103,8 +2450,8 @@
PRIVATE_ADDR2, MAC_B);
coordinator.tetherOffloadClientAdd(mIpServer, clientA);
coordinator.tetherOffloadClientAdd(mIpServer, clientB);
- asseertClientInfoExist(mIpServer, clientA);
- asseertClientInfoExist(mIpServer, clientB);
+ assertClientInfoExists(mIpServer, clientA);
+ assertClientInfoExists(mIpServer, clientB);
// Add the rules for client A and client B.
final Tether4Key upstream4KeyA = makeUpstream4Key(
@@ -2128,8 +2475,8 @@
// [2] Remove client information A. Only the rules on client A should be removed and
// the rules on client B should exist.
coordinator.tetherOffloadClientRemove(mIpServer, clientA);
- asseertClientInfoNotExist(mIpServer, clientA);
- asseertClientInfoExist(mIpServer, clientB);
+ assertClientInfoDoesNotExist(mIpServer, clientA.clientAddress);
+ assertClientInfoExists(mIpServer, clientB);
assertNull(mBpfUpstream4Map.getValue(upstream4KeyA));
assertNull(mBpfDownstream4Map.getValue(downstream4KeyA));
assertEquals(upstream4ValueB, mBpfUpstream4Map.getValue(upstream4KeyB));
@@ -2137,9 +2484,9 @@
// [3] Remove client information B. The rules on client B should be removed.
// Exactly, ClientInfo for a given IpServer is removed because the last client B
- // has been removed from the downstream. Can't use the helper #asseertClientInfoExist
+ // has been removed from the downstream. Can't use the helper #assertClientInfoExists
// to check because the container ClientInfo for a given downstream has been removed.
- // See #asseertClientInfoExist.
+ // See #assertClientInfoExists.
coordinator.tetherOffloadClientRemove(mIpServer, clientB);
assertNull(mTetherClients.get(mIpServer));
assertNull(mBpfUpstream4Map.getValue(upstream4KeyB));
@@ -2150,13 +2497,15 @@
public void testIpv6ForwardingRuleToString() throws Exception {
final Ipv6DownstreamRule downstreamRule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A,
MAC_A);
- assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8::1, "
+ assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, address: 2001:db8:0:1234::1, "
+ "srcMac: 12:34:56:78:90:ab, dstMac: 00:00:00:00:00:0a",
downstreamRule.toString());
- final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(UPSTREAM_IFINDEX);
- assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, sourcePrefix: ::/64, "
- + "inDstMac: 12:34:56:78:90:ab, outSrcMac: 00:00:00:00:00:00, "
- + "outDstMac: 00:00:00:00:00:00", upstreamRule.toString());
+ final Ipv6UpstreamRule upstreamRule = buildTestUpstreamRule(
+ UPSTREAM_IFINDEX, DOWNSTREAM_IFINDEX, UPSTREAM_PREFIX, DOWNSTREAM_MAC);
+ assertEquals("upstreamIfindex: 1001, downstreamIfindex: 2001, "
+ + "sourcePrefix: 2001:db8:0:1234::/64, inDstMac: 12:34:56:78:90:ab, "
+ + "outSrcMac: 00:00:00:00:00:00, outDstMac: 00:00:00:00:00:00",
+ upstreamRule.toString());
}
private void verifyDump(@NonNull final BpfCoordinator coordinator) {
@@ -2206,8 +2555,9 @@
final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
mBpfDownstream6Map.insertEntry(rule.makeTetherDownstream6Key(), rule.makeTether6Value());
+ final byte[] prefix64 = prefixToIp64(UPSTREAM_PREFIX);
final TetherUpstream6Key upstream6Key = new TetherUpstream6Key(DOWNSTREAM_IFINDEX,
- DOWNSTREAM_MAC, 0);
+ DOWNSTREAM_MAC, prefix64);
final Tether6Value upstream6Value = new Tether6Value(UPSTREAM_IFINDEX,
MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
@@ -2221,7 +2571,7 @@
0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */));
// dumpDevmap
- coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+ coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
mBpfDevMap.insertEntry(
new TetherDevKey(UPSTREAM_IFINDEX),
new TetherDevValue(UPSTREAM_IFINDEX));
@@ -2390,7 +2740,7 @@
// +-------+-------+-------+-------+-------+
// [1] Mobile IPv4 only
- coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+ coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
final UpstreamNetworkState mobileIPv4UpstreamState = new UpstreamNetworkState(
buildUpstreamLinkProperties(UPSTREAM_IFACE,
@@ -2442,7 +2792,7 @@
verifyIpv4Upstream(ipv4UpstreamIndices, interfaceNames);
// Mobile IPv6 and xlat
- // IpServer doesn't add xlat interface mapping via #addUpstreamNameToLookupTable on
+ // IpServer doesn't add xlat interface mapping via #maybeAddUpstreamToLookupTable on
// S and T devices.
coordinator.updateUpstreamNetworkState(mobile464xlatUpstreamState);
// Upstream IPv4 address mapping is removed because xlat interface is not supported.
@@ -2457,7 +2807,7 @@
// [6] Wifi IPv4 and IPv6
// Expect that upstream index map is cleared because ether ip is not supported.
- coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2, UPSTREAM_IFACE2);
+ coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX2, UPSTREAM_IFACE2);
doReturn(UPSTREAM_IFACE_PARAMS2).when(mDeps).getInterfaceParams(UPSTREAM_IFACE2);
final UpstreamNetworkState wifiDualStackUpstreamState = new UpstreamNetworkState(
buildUpstreamLinkProperties(UPSTREAM_IFACE2,
@@ -2476,4 +2826,296 @@
public void testUpdateUpstreamNetworkState() throws Exception {
verifyUpdateUpstreamNetworkState();
}
+
+ @NonNull
+ private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
+ TetherStatsParcel parcel = new TetherStatsParcel();
+ parcel.ifIndex = ifIndex;
+ return parcel;
+ }
+
+ private void resetNetdAndBpfMaps() throws Exception {
+ reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+ // potentially crash the test) if the stats map is empty.
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
+ when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
+ .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
+ when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2))
+ .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2));
+ // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+ // potentially crash the test) if the stats map is empty.
+ final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0);
+ when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros);
+ when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros);
+ }
+
+ @Test
+ public void addRemoveIpv6ForwardingRules() throws Exception {
+ final int myIfindex = DOWNSTREAM_IFINDEX;
+ final int notMyIfindex = myIfindex - 1;
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+
+ resetNetdAndBpfMaps();
+ verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
+ // tetherOffloadGetAndClearStats in netd while the rules are changed.
+
+ // Events on other interfaces are ignored.
+ recvNewNeigh(notMyIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+ verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // Events on this interface are received and sent to BpfCoordinator.
+ recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+ final Ipv6DownstreamRule ruleA = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+ verifyAddDownstreamRule(ruleA);
+ resetNetdAndBpfMaps();
+
+ recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
+ final Ipv6DownstreamRule ruleB = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B);
+ verifyAddDownstreamRule(ruleB);
+ resetNetdAndBpfMaps();
+
+ // Link-local and multicast neighbors are ignored.
+ recvNewNeigh(myIfindex, NEIGH_LL, NUD_REACHABLE, MAC_A);
+ verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ recvNewNeigh(myIfindex, NEIGH_MC, NUD_REACHABLE, MAC_A);
+ verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // A neighbor that is no longer valid causes the rule to be removed.
+ // NUD_FAILED events do not have a MAC address.
+ recvNewNeigh(myIfindex, NEIGH_A, NUD_FAILED, null);
+ final Ipv6DownstreamRule ruleANull = buildTestDownstreamRule(
+ UPSTREAM_IFINDEX, NEIGH_A, MAC_NULL);
+ verifyRemoveDownstreamRule(ruleANull);
+ resetNetdAndBpfMaps();
+
+ // A neighbor that is deleted causes the rule to be removed.
+ recvDelNeigh(myIfindex, NEIGH_B, NUD_STALE, MAC_B);
+ final Ipv6DownstreamRule ruleBNull = buildTestDownstreamRule(
+ UPSTREAM_IFINDEX, NEIGH_B, MAC_NULL);
+ verifyRemoveDownstreamRule(ruleBNull);
+ resetNetdAndBpfMaps();
+
+ // Upstream interface changes result in updating the rules.
+ recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+ recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
+ resetNetdAndBpfMaps();
+
+ InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE2);
+ lp.setLinkAddresses(UPSTREAM_ADDRESSES);
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp, -1);
+ final Ipv6DownstreamRule ruleA2 = buildTestDownstreamRule(
+ UPSTREAM_IFINDEX2, NEIGH_A, MAC_A);
+ final Ipv6DownstreamRule ruleB2 = buildTestDownstreamRule(
+ UPSTREAM_IFINDEX2, NEIGH_B, MAC_B);
+ verifyRemoveDownstreamRule(inOrder, ruleA);
+ verifyRemoveDownstreamRule(inOrder, ruleB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES);
+ verifyAddDownstreamRule(inOrder, ruleA2);
+ verifyAddDownstreamRule(inOrder, ruleB2);
+ verifyNoUpstreamIpv6ForwardingChange(inOrder);
+ resetNetdAndBpfMaps();
+
+ // Upstream link addresses change result in updating the rules.
+ LinkProperties lp2 = new LinkProperties();
+ lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, -1);
+ verifyRemoveDownstreamRule(inOrder, ruleA2);
+ verifyRemoveDownstreamRule(inOrder, ruleB2);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ verifyAddDownstreamRule(inOrder, ruleA2);
+ verifyAddDownstreamRule(inOrder, ruleB2);
+ resetNetdAndBpfMaps();
+
+ // When the upstream is lost, rules are removed.
+ dispatchTetherConnectionChanged(ipServer, null, null, 0);
+ verifyStopUpstreamIpv6Forwarding(inOrder, UPSTREAM_PREFIXES2);
+ verifyRemoveDownstreamRule(ruleA2);
+ verifyRemoveDownstreamRule(ruleB2);
+ // Upstream lost doesn't clear the downstream rules from the maps.
+ // Do that here.
+ recvDelNeigh(myIfindex, NEIGH_A, NUD_STALE, MAC_A);
+ recvDelNeigh(myIfindex, NEIGH_B, NUD_STALE, MAC_B);
+ resetNetdAndBpfMaps();
+
+ // If the upstream is IPv4-only, no IPv6 rules are added to BPF map.
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE);
+ resetNetdAndBpfMaps();
+ recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ // Downstream rules are only added to BpfCoordinator but not BPF map.
+ verifyNeverAddDownstreamRule();
+ verifyNoMoreInteractions(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
+
+ // Rules can be added again once upstream IPv6 connectivity is available. The existing rules
+ // with an upstream of NO_UPSTREAM are reapplied.
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ verifyAddDownstreamRule(ruleA);
+ recvNewNeigh(myIfindex, NEIGH_B, NUD_REACHABLE, MAC_B);
+ verifyAddDownstreamRule(ruleB);
+
+ // If upstream IPv6 connectivity is lost, rules are removed.
+ resetNetdAndBpfMaps();
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, null, 0);
+ verifyRemoveDownstreamRule(ruleA);
+ verifyRemoveDownstreamRule(ruleB);
+ verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
+
+ // When upstream IPv6 connectivity comes back, upstream rules are added and downstream rules
+ // are reapplied.
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE, lp, -1);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES);
+ verifyAddDownstreamRule(ruleA);
+ verifyAddDownstreamRule(ruleB);
+ resetNetdAndBpfMaps();
+
+ // When the downstream interface goes down, rules are removed.
+ ipServer.stop();
+ mTestLooper.dispatchAll();
+ verifyStopUpstreamIpv6Forwarding(null, UPSTREAM_PREFIXES);
+ verifyRemoveDownstreamRule(ruleA);
+ verifyRemoveDownstreamRule(ruleB);
+ verify(mIpNeighborMonitor).stop();
+ resetNetdAndBpfMaps();
+ }
+
+ @Test
+ public void enableDisableUsingBpfOffload() throws Exception {
+ final int myIfindex = DOWNSTREAM_IFINDEX;
+
+ // Expect that rules can be only added/removed when the BPF offload config is enabled.
+ // Note that the BPF offload disabled case is not a realistic test case. Because IP
+ // neighbor monitor doesn't start if BPF offload is disabled, there should have no
+ // neighbor event listening. This is used for testing the protection check just in case.
+ // TODO: Perhaps remove the BPF offload disabled case test once this check isn't needed
+ // anymore.
+
+ // [1] Enable BPF offload.
+ // A neighbor that is added or deleted causes the rule to be added or removed.
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+ resetNetdAndBpfMaps();
+
+ recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+ final Ipv6DownstreamRule rule = buildTestDownstreamRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A);
+ verifyAddDownstreamRule(rule);
+ resetNetdAndBpfMaps();
+
+ recvDelNeigh(myIfindex, NEIGH_A, NUD_STALE, MAC_A);
+ final Ipv6DownstreamRule ruleNull = buildTestDownstreamRule(
+ UPSTREAM_IFINDEX, NEIGH_A, MAC_NULL);
+ verifyRemoveDownstreamRule(ruleNull);
+ resetNetdAndBpfMaps();
+
+ // Upstream IPv6 connectivity change causes upstream rules change.
+ LinkProperties lp2 = new LinkProperties();
+ lp2.setInterfaceName(UPSTREAM_IFACE2);
+ lp2.setLinkAddresses(UPSTREAM_ADDRESSES2);
+ dispatchTetherConnectionChanged(ipServer, UPSTREAM_IFACE2, lp2, 0);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2);
+ resetNetdAndBpfMaps();
+
+ // [2] Disable BPF offload.
+ // A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
+ when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
+ final BpfCoordinator coordinator2 = makeBpfCoordinator();
+ final IpServer ipServer2 = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator2);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdAndBpfMaps();
+
+ recvNewNeigh(myIfindex, NEIGH_A, NUD_REACHABLE, MAC_A);
+ verifyNeverAddDownstreamRule();
+ resetNetdAndBpfMaps();
+
+ recvDelNeigh(myIfindex, NEIGH_A, NUD_STALE, MAC_A);
+ verifyNeverRemoveDownstreamRule();
+ resetNetdAndBpfMaps();
+
+ // Upstream IPv6 connectivity change doesn't cause the rule to be added or removed.
+ dispatchTetherConnectionChanged(ipServer2, UPSTREAM_IFACE2, lp2, 0);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ verifyNeverRemoveDownstreamRule();
+ resetNetdAndBpfMaps();
+ }
+
+ @Test
+ public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception {
+ when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+
+ // IP neighbor monitor doesn't start if BPF offload is disabled.
+ verify(mIpNeighborMonitor, never()).start();
+ }
+
+ @Test
+ public void testSkipVirtualNetworkInBpf() throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+ final LinkProperties v6Only = new LinkProperties();
+ v6Only.setInterfaceName(IPSEC_IFACE);
+ v6Only.setLinkAddresses(UPSTREAM_ADDRESSES);
+
+ resetNetdAndBpfMaps();
+ dispatchTetherConnectionChanged(ipServer, IPSEC_IFACE, v6Only, 0);
+ verify(mNetd).tetherAddForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
+ verify(mNetd).ipfwdAddInterfaceForward(DOWNSTREAM_IFACE, IPSEC_IFACE);
+ verifyNeverAddUpstreamRule();
+
+ recvNewNeigh(DOWNSTREAM_IFINDEX, NEIGH_A, NUD_REACHABLE, MAC_A);
+ verifyNeverAddDownstreamRule();
+ }
+
+ @Test
+ public void addRemoveTetherClient() throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ final IpServer ipServer = makeAndStartIpServer(DOWNSTREAM_IFACE, coordinator);
+ final int myIfindex = DOWNSTREAM_IFINDEX;
+ final int notMyIfindex = myIfindex - 1;
+
+ final InetAddress neighA = InetAddresses.parseNumericAddress("192.168.80.1");
+ final InetAddress neighB = InetAddresses.parseNumericAddress("192.168.80.2");
+ final InetAddress neighLL = InetAddresses.parseNumericAddress("169.254.0.1");
+ final InetAddress neighMC = InetAddresses.parseNumericAddress("224.0.0.1");
+
+ // Events on other interfaces are ignored.
+ recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, MAC_A);
+ assertNull(mTetherClients.get(ipServer));
+
+ // Events on this interface are received and sent to BpfCoordinator.
+ recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, MAC_A);
+ assertClientInfoExists(ipServer,
+ new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighA, MAC_A));
+
+ recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, MAC_B);
+ assertClientInfoExists(ipServer,
+ new ClientInfo(myIfindex, DOWNSTREAM_MAC, (Inet4Address) neighB, MAC_B));
+
+ // Link-local and multicast neighbors are ignored.
+ recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, MAC_A);
+ assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighLL);
+ recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, MAC_A);
+ assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighMC);
+
+ // A neighbor that is no longer valid causes the client to be removed.
+ // NUD_FAILED events do not have a MAC address.
+ recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
+ assertClientInfoDoesNotExist(ipServer, (Inet4Address) neighA);
+
+ // A neighbor that is deleted causes the client to be removed.
+ recvDelNeigh(myIfindex, neighB, NUD_STALE, MAC_B);
+ // When last client information is deleted, IpServer will be removed from mTetherClients
+ assertNull(mTetherClients.get(ipServer));
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index 91b092a..6ebd6ae 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -126,16 +126,17 @@
final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
- final IpPrefix testDupRequest = asIpPrefix(newAddress);
- assertNotEquals(hotspotPrefix, testDupRequest);
- assertNotEquals(bluetoothPrefix, testDupRequest);
- mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
+ final IpPrefix newHotspotPrefix = asIpPrefix(newAddress);
+ assertNotEquals(hotspotPrefix, newHotspotPrefix);
+ assertNotEquals(bluetoothPrefix, newHotspotPrefix);
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix usbPrefix = asIpPrefix(usbAddress);
assertNotEquals(usbPrefix, bluetoothPrefix);
- assertNotEquals(usbPrefix, hotspotPrefix);
+ assertNotEquals(usbPrefix, newHotspotPrefix);
+
+ mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer);
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 770507e..5877fc5 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -142,6 +142,7 @@
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.RouteInfo;
+import android.net.RoutingCoordinatorManager;
import android.net.TetherStatesParcel;
import android.net.TetheredClient;
import android.net.TetheredClient.AddressInfo;
@@ -186,11 +187,11 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.util.StateMachine;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.IpNeighborMonitor;
import com.android.networkstack.apishim.common.BluetoothPanShim;
@@ -300,7 +301,7 @@
// Like so many Android system APIs, these cannot be mocked because it is marked final.
// We have to use the real versions.
private final PersistableBundle mCarrierConfig = new PersistableBundle();
- private final TestLooper mLooper = new TestLooper();
+ private TestLooper mLooper;
private Vector<Intent> mIntents;
private BroadcastInterceptingContext mServiceContext;
@@ -308,6 +309,7 @@
private BroadcastReceiver mBroadcastReceiver;
private Tethering mTethering;
private TestTetheringEventCallback mTetheringEventCallback;
+ private Tethering.TetherMainSM mTetherMainSM;
private PhoneStateListener mPhoneStateListener;
private InterfaceConfigurationParcel mInterfaceConfiguration;
private TetheringConfiguration mConfig;
@@ -317,6 +319,7 @@
private SoftApCallback mSoftApCallback;
private SoftApCallback mLocalOnlyHotspotCallback;
private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+ private UpstreamNetworkMonitor.EventListener mEventListener;
private TetheredInterfaceCallbackShim mTetheredInterfaceCallbackShim;
private TestConnectivityManager mCm;
@@ -430,7 +433,6 @@
}
public class MockTetheringDependencies extends TetheringDependencies {
- StateMachine mUpstreamNetworkMonitorSM;
ArrayList<IpServer> mAllDownstreams;
@Override
@@ -454,12 +456,12 @@
}
@Override
- public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
- StateMachine target, SharedLog log, int what) {
+ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, Handler h,
+ SharedLog log, UpstreamNetworkMonitor.EventListener listener) {
// Use a real object instead of a mock so that some tests can use a real UNM and some
// can use a mock.
- mUpstreamNetworkMonitorSM = target;
- mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what));
+ mEventListener = listener;
+ mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, h, log, listener));
return mUpstreamNetworkMonitor;
}
@@ -482,6 +484,12 @@
return mEntitleMgr;
}
+ @Nullable
+ @Override
+ public LateSdk<RoutingCoordinatorManager> getRoutingCoordinator(final Context context) {
+ return new LateSdk<>(null);
+ }
+
@Override
public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
int subId) {
@@ -672,7 +680,15 @@
mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class)));
- mTethering = makeTethering();
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
+ }
+
+ // In order to interact with syncSM from the test, tethering must be created in test thread.
+ private void initTetheringOnTestThread() throws Exception {
+ mLooper = new TestLooper();
+ mTethering = new Tethering(mTetheringDependencies);
+ mTetherMainSM = mTethering.getTetherMainSMForTesting();
verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
verify(mNetd).registerUnsolicitedEventListener(any());
verifyDefaultNetworkRequestFiled();
@@ -696,9 +712,6 @@
localOnlyCallbackCaptor.capture());
mLocalOnlyHotspotCallback = localOnlyCallbackCaptor.getValue();
}
-
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
}
private void setTetheringSupported(final boolean supported) {
@@ -730,10 +743,6 @@
doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
}
- private Tethering makeTethering() {
- return new Tethering(mTetheringDependencies);
- }
-
private TetheringRequestParcel createTetheringRequestParcel(final int type) {
return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL);
}
@@ -877,6 +886,7 @@
public void failingLocalOnlyHotspotLegacyApBroadcast(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
@@ -928,6 +938,7 @@
@Test
public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
initTetheringUpstream(upstreamState);
prepareUsbTethering();
@@ -1004,6 +1015,7 @@
public void workingLocalOnlyHotspotEnrichedApBroadcast(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// Emulate externally-visible WifiManager effects, causing the
// per-interface state machine to start up, and telling us that
// hotspot mode is to be started.
@@ -1067,6 +1079,7 @@
@Test
public void workingMobileUsbTethering_IPv4() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -1081,7 +1094,8 @@
}
@Test
- public void workingMobileUsbTethering_IPv4LegacyDhcp() {
+ public void workingMobileUsbTethering_IPv4LegacyDhcp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
sendConfigurationChanged();
@@ -1094,6 +1108,7 @@
@Test
public void workingMobileUsbTethering_IPv6() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState();
runUsbTethering(upstreamState);
@@ -1109,6 +1124,7 @@
@Test
public void workingMobileUsbTethering_DualStack() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
runUsbTethering(upstreamState);
@@ -1126,6 +1142,7 @@
@Test
public void workingMobileUsbTethering_MultipleUpstreams() throws Exception {
+ initTetheringOnTestThread();
UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState();
runUsbTethering(upstreamState);
@@ -1145,6 +1162,7 @@
@Test
public void workingMobileUsbTethering_v6Then464xlat() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -1165,10 +1183,7 @@
initTetheringUpstream(upstreamState);
// Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES.
- mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
- Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
- UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
- 0,
+ mEventListener.onUpstreamEvent(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
upstreamState);
mLooper.dispatchAll();
@@ -1186,6 +1201,7 @@
@Test
public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
sendConfigurationChanged();
@@ -1234,6 +1250,7 @@
}
private void verifyAutomaticUpstreamSelection(boolean configAutomatic) throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1333,6 +1350,7 @@
@Test
@IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
public void testLegacyUpstreamSelection() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1483,6 +1501,7 @@
// +-------+-------+-------+-------+-------+
//
private void verifyChooseDunUpstreamByAutomaticMode(boolean configAutomatic) throws Exception {
+ initTetheringOnTestThread();
// Enable automatic upstream selection.
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1543,6 +1562,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_defaultNetworkWifi() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1594,6 +1614,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_loseDefaultNetworkWifi() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1635,6 +1656,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_defaultNetworkCell() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
@@ -1679,6 +1701,7 @@
//
@Test
public void testChooseDunUpstreamByAutomaticMode_loseAndRegainDun() throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
final InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
setupDunUpstreamTest(true /* configAutomatic */, inOrder);
@@ -1720,6 +1743,7 @@
@Test
public void testChooseDunUpstreamByAutomaticMode_switchDefaultFromWifiToCell()
throws Exception {
+ initTetheringOnTestThread();
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState());
@@ -1757,6 +1781,7 @@
@Test
@IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
public void testChooseDunUpstreamByLegacyMode() throws Exception {
+ initTetheringOnTestThread();
// Enable Legacy upstream selection.
TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
@@ -1849,6 +1874,7 @@
@Test
public void workingNcmTethering() throws Exception {
+ initTetheringOnTestThread();
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -1856,7 +1882,8 @@
}
@Test
- public void workingNcmTethering_LegacyDhcp() {
+ public void workingNcmTethering_LegacyDhcp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
sendConfigurationChanged();
@@ -1878,6 +1905,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failingWifiTetheringLegacyApBroadcast() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
@@ -1906,6 +1934,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void workingWifiTetheringEnrichedApBroadcast() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
// Emulate pressing the WiFi tethering button.
@@ -1954,6 +1983,7 @@
// TODO: Test with and without interfaceStatusChanged().
@Test
public void failureEnablingIpForwarding() throws Exception {
+ initTetheringOnTestThread();
when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true);
doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME);
@@ -2101,7 +2131,8 @@
}
@Test
- public void testUntetherUsbWhenRestrictionIsOn() {
+ public void testUntetherUsbWhenRestrictionIsOn() throws Exception {
+ initTetheringOnTestThread();
// Start usb tethering and check that usb interface is tethered.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -2278,6 +2309,7 @@
@Test
public void testRegisterTetheringEventCallback() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
TestTetheringEventCallback callback2 = new TestTetheringEventCallback();
final TetheringInterface wifiIface = new TetheringInterface(
@@ -2342,6 +2374,7 @@
@Test
public void testReportFailCallbackIfOffloadNotSupported() throws Exception {
+ initTetheringOnTestThread();
final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
mTethering.registerTetheringEventCallback(callback);
@@ -2381,6 +2414,7 @@
@Test
public void testMultiSimAware() throws Exception {
+ initTetheringOnTestThread();
final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration();
assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.activeDataSubId);
@@ -2393,6 +2427,7 @@
@Test
public void testNoDuplicatedEthernetRequest() throws Exception {
+ initTetheringOnTestThread();
final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class);
when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), TEST_CALLER_PKG,
@@ -2413,6 +2448,7 @@
private void workingWifiP2pGroupOwner(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
if (emulateInterfaceStatusChanged) {
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
@@ -2452,6 +2488,7 @@
private void workingWifiP2pGroupClient(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
if (emulateInterfaceStatusChanged) {
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
@@ -2492,6 +2529,7 @@
private void workingWifiP2pGroupOwnerLegacyMode(
boolean emulateInterfaceStatusChanged) throws Exception {
+ initTetheringOnTestThread();
// change to legacy mode and update tethering information by chaning SIM
when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs))
.thenReturn(new String[]{});
@@ -2541,7 +2579,8 @@
}
@Test
- public void testDataSaverChanged() {
+ public void testDataSaverChanged() throws Exception {
+ initTetheringOnTestThread();
// Start Tethering.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
runUsbTethering(upstreamState);
@@ -2596,6 +2635,7 @@
@Test
public void testMultipleStartTethering() throws Exception {
+ initTetheringOnTestThread();
final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24");
final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24");
final String serverAddr = "192.168.20.1";
@@ -2639,6 +2679,7 @@
@Test
public void testRequestStaticIp() throws Exception {
+ initTetheringOnTestThread();
when(mResources.getInteger(R.integer.config_tether_usb_functions)).thenReturn(
TetheringConfiguration.TETHER_USB_NCM_FUNCTION);
when(mResources.getStringArray(R.array.config_tether_usb_regexs))
@@ -2668,15 +2709,14 @@
}
@Test
- public void testUpstreamNetworkChanged() {
- final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
- mTetheringDependencies.mUpstreamNetworkMonitorSM;
+ public void testUpstreamNetworkChanged() throws Exception {
+ initTetheringOnTestThread();
final InOrder inOrder = inOrder(mNotificationUpdater);
// Gain upstream.
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
initTetheringUpstream(upstreamState);
- stateMachine.chooseUpstreamType(true);
+ mTetherMainSM.chooseUpstreamType(true);
mTetheringEventCallback.expectUpstreamChanged(upstreamState.network);
inOrder.verify(mNotificationUpdater)
.onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
@@ -2684,7 +2724,7 @@
// Set the upstream with the same network ID but different object and the same capability.
final UpstreamNetworkState upstreamState2 = buildMobileIPv4UpstreamState();
initTetheringUpstream(upstreamState2);
- stateMachine.chooseUpstreamType(true);
+ mTetherMainSM.chooseUpstreamType(true);
// Expect that no upstream change event and capabilities changed event.
mTetheringEventCallback.assertNoUpstreamChangeCallback();
inOrder.verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
@@ -2694,34 +2734,33 @@
assertFalse(upstreamState3.networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED));
upstreamState3.networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
initTetheringUpstream(upstreamState3);
- stateMachine.chooseUpstreamType(true);
+ mTetherMainSM.chooseUpstreamType(true);
// Expect that no upstream change event and capabilities changed event.
mTetheringEventCallback.assertNoUpstreamChangeCallback();
- stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState3);
+ mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState3);
inOrder.verify(mNotificationUpdater)
.onUpstreamCapabilitiesChanged(upstreamState3.networkCapabilities);
// Lose upstream.
initTetheringUpstream(null);
- stateMachine.chooseUpstreamType(true);
+ mTetherMainSM.chooseUpstreamType(true);
mTetheringEventCallback.expectUpstreamChanged(NULL_NETWORK);
inOrder.verify(mNotificationUpdater).onUpstreamCapabilitiesChanged(null);
}
@Test
- public void testUpstreamCapabilitiesChanged() {
- final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM)
- mTetheringDependencies.mUpstreamNetworkMonitorSM;
+ public void testUpstreamCapabilitiesChanged() throws Exception {
+ initTetheringOnTestThread();
final InOrder inOrder = inOrder(mNotificationUpdater);
final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
initTetheringUpstream(upstreamState);
- stateMachine.chooseUpstreamType(true);
+ mTetherMainSM.chooseUpstreamType(true);
inOrder.verify(mNotificationUpdater)
.onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
- stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+ mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
inOrder.verify(mNotificationUpdater)
.onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
@@ -2730,7 +2769,7 @@
// Expect that capability is changed with new capability VALIDATED.
assertFalse(upstreamState.networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED));
upstreamState.networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
- stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
+ mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState);
inOrder.verify(mNotificationUpdater)
.onUpstreamCapabilitiesChanged(upstreamState.networkCapabilities);
@@ -2739,12 +2778,13 @@
final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
upstreamState.linkProperties, upstreamState.networkCapabilities,
new Network(WIFI_NETID));
- stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
+ mTetherMainSM.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
inOrder.verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
}
@Test
public void testUpstreamCapabilitiesChanged_startStopTethering() throws Exception {
+ initTetheringOnTestThread();
final TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
// Start USB tethering with no current upstream.
@@ -2766,6 +2806,7 @@
@Test
public void testDumpTetheringLog() throws Exception {
+ initTetheringOnTestThread();
final FileDescriptor mockFd = mock(FileDescriptor.class);
final PrintWriter mockPw = mock(PrintWriter.class);
runUsbTethering(null);
@@ -2779,6 +2820,7 @@
@Test
public void testExemptFromEntitlementCheck() throws Exception {
+ initTetheringOnTestThread();
setupForRequiredProvisioning();
final TetheringRequestParcel wifiNotExemptRequest =
createTetheringRequestParcel(TETHERING_WIFI, null, null, false,
@@ -2859,41 +2901,48 @@
final String iface, final int transportType) {
final UpstreamNetworkState upstream = buildV4UpstreamState(ipv4Address, network, iface,
transportType);
- mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage(
- Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK,
- UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
- 0,
- upstream);
+ mEventListener.onUpstreamEvent(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES, upstream);
mLooper.dispatchAll();
}
@Test
public void testHandleIpConflict() throws Exception {
+ initTetheringOnTestThread();
final Network wifiNetwork = new Network(200);
final Network[] allNetworks = { wifiNetwork };
doReturn(allNetworks).when(mCm).getAllNetworks();
+ InOrder inOrder = inOrder(mUsbManager, mNetd);
runUsbTethering(null);
+
+ inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
+
final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture());
final String ipv4Address = ifaceConfigCaptor.getValue().ipv4Addr;
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
- reset(mUsbManager);
// Cause a prefix conflict by assigning a /30 out of the downstream's /24 to the upstream.
updateV4Upstream(new LinkAddress(InetAddresses.parseNumericAddress(ipv4Address), 30),
wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI);
// verify turn off usb tethering
- verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
+ inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE);
sendUsbBroadcast(true, true, -1 /* function */);
mLooper.dispatchAll();
+ inOrder.verify(mNetd).tetherInterfaceRemove(TEST_RNDIS_IFNAME);
+
// verify restart usb tethering
- verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+ inOrder.verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
+
+ sendUsbBroadcast(true, true, TETHER_USB_RNDIS_FUNCTION);
+ mLooper.dispatchAll();
+ inOrder.verify(mNetd).tetherInterfaceAdd(TEST_RNDIS_IFNAME);
}
@Test
public void testNoAddressAvailable() throws Exception {
+ initTetheringOnTestThread();
final Network wifiNetwork = new Network(200);
final Network btNetwork = new Network(201);
final Network mobileNetwork = new Network(202);
@@ -2955,6 +3004,7 @@
@Test
public void testProvisioningNeededButUnavailable() throws Exception {
+ initTetheringOnTestThread();
assertTrue(mTethering.isTetheringSupported());
verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES);
@@ -2972,6 +3022,7 @@
@Test
public void testUpdateConnectedClients() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3021,6 +3072,7 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testUpdateConnectedClientsForLocalOnlyHotspot() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3053,6 +3105,7 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testConnectedClientsForSapAndLohsConcurrency() throws Exception {
+ initTetheringOnTestThread();
TestTetheringEventCallback callback = new TestTetheringEventCallback();
runAsShell(NETWORK_SETTINGS, () -> {
mTethering.registerTetheringEventCallback(callback);
@@ -3178,6 +3231,7 @@
@Test
public void testBluetoothTethering() throws Exception {
+ initTetheringOnTestThread();
// Switch to @IgnoreUpTo(Build.VERSION_CODES.S_V2) when it is available for AOSP.
assumeTrue(isAtLeastT());
@@ -3214,6 +3268,7 @@
@Test
public void testBluetoothTetheringBeforeT() throws Exception {
+ initTetheringOnTestThread();
// Switch to @IgnoreAfter(Build.VERSION_CODES.S_V2) when it is available for AOSP.
assumeFalse(isAtLeastT());
@@ -3261,6 +3316,7 @@
@Test
public void testBluetoothServiceDisconnects() throws Exception {
+ initTetheringOnTestThread();
final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR);
mockBluetoothSettings(true /* bluetoothOn */, true /* tetheringOn */);
mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH),
@@ -3415,6 +3471,7 @@
@Test
public void testUsbFunctionConfigurationChange() throws Exception {
+ initTetheringOnTestThread();
// Run TETHERING_NCM.
runNcmTethering();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
@@ -3473,6 +3530,7 @@
@Test
public void testTetheringSupported() throws Exception {
+ initTetheringOnTestThread();
final ArraySet<Integer> expectedTypes = getAllSupportedTetheringTypes();
// Check tethering is supported after initialization.
TestTetheringEventCallback callback = new TestTetheringEventCallback();
@@ -3545,6 +3603,7 @@
@Test
public void testIpv4AddressForSapAndLohsConcurrency() throws Exception {
+ initTetheringOnTestThread();
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index e756bd3..045c0cb 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -30,10 +30,12 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -51,27 +53,23 @@
import android.net.NetworkRequest;
import android.os.Build;
import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.internal.util.State;
-import com.android.internal.util.StateMachine;
import com.android.net.module.util.SharedLog;
import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
-import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
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;
@@ -84,8 +82,6 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UpstreamNetworkMonitorTest {
- private static final int EVENT_UNM_UPDATE = 1;
-
private static final boolean INCLUDES = true;
private static final boolean EXCLUDES = false;
@@ -102,12 +98,13 @@
@Mock private EntitlementManager mEntitleMgr;
@Mock private IConnectivityManager mCS;
@Mock private SharedLog mLog;
+ @Mock private UpstreamNetworkMonitor.EventListener mListener;
- private TestStateMachine mSM;
private TestConnectivityManager mCM;
private UpstreamNetworkMonitor mUNM;
private final TestLooper mLooper = new TestLooper();
+ private InOrder mCallbackOrder;
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -117,17 +114,11 @@
when(mLog.forSubComponent(anyString())).thenReturn(mLog);
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ mCallbackOrder = inOrder(mListener);
mCM = spy(new TestConnectivityManager(mContext, mCS));
when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mCM);
- mSM = new TestStateMachine(mLooper.getLooper());
- mUNM = new UpstreamNetworkMonitor(mContext, mSM, mLog, EVENT_UNM_UPDATE);
- }
-
- @After public void tearDown() throws Exception {
- if (mSM != null) {
- mSM.quit();
- mSM = null;
- }
+ mUNM = new UpstreamNetworkMonitor(mContext, new Handler(mLooper.getLooper()), mLog,
+ mListener);
}
@Test
@@ -603,14 +594,17 @@
mCM.makeDefaultNetwork(cellAgent);
mLooper.dispatchAll();
verifyCurrentLinkProperties(cellAgent);
- int messageIndex = mSM.messages.size() - 1;
+ verifyNotifyNetworkCapabilitiesChange(cellAgent.networkCapabilities);
+ verifyNotifyLinkPropertiesChange(cellLp);
+ verifyNotifyDefaultSwitch(cellAgent);
+ verifyNoMoreInteractions(mListener);
addLinkAddresses(cellLp, ipv6Addr1);
mCM.sendLinkProperties(cellAgent, false /* updateDefaultFirst */);
mLooper.dispatchAll();
verifyCurrentLinkProperties(cellAgent);
- verifyNotifyLinkPropertiesChange(messageIndex);
- messageIndex = mSM.messages.size() - 1;
+ verifyNotifyLinkPropertiesChange(cellLp);
+ verifyNoMoreInteractions(mListener);
removeLinkAddresses(cellLp, ipv6Addr1);
addLinkAddresses(cellLp, ipv6Addr2);
@@ -618,7 +612,8 @@
mLooper.dispatchAll();
assertEquals(cellAgent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
verifyCurrentLinkProperties(cellAgent);
- verifyNotifyLinkPropertiesChange(messageIndex);
+ verifyNotifyLinkPropertiesChange(cellLp);
+ verifyNoMoreInteractions(mListener);
}
private void verifyCurrentLinkProperties(TestNetworkAgent agent) {
@@ -626,12 +621,33 @@
assertEquals(agent.linkProperties, mUNM.getCurrentPreferredUpstream().linkProperties);
}
- private void verifyNotifyLinkPropertiesChange(int lastMessageIndex) {
- assertEquals(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES,
- mSM.messages.get(++lastMessageIndex).arg1);
- assertEquals(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES,
- mSM.messages.get(++lastMessageIndex).arg1);
- assertEquals(lastMessageIndex + 1, mSM.messages.size());
+ private void verifyNotifyNetworkCapabilitiesChange(final NetworkCapabilities cap) {
+ mCallbackOrder.verify(mListener).onUpstreamEvent(
+ eq(UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES),
+ argThat(uns -> uns instanceof UpstreamNetworkState
+ && cap.equals(((UpstreamNetworkState) uns).networkCapabilities)));
+
+ }
+
+ private void verifyNotifyLinkPropertiesChange(final LinkProperties lp) {
+ mCallbackOrder.verify(mListener).onUpstreamEvent(
+ eq(UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES),
+ argThat(uns -> uns instanceof UpstreamNetworkState
+ && lp.equals(((UpstreamNetworkState) uns).linkProperties)));
+
+ mCallbackOrder.verify(mListener).onUpstreamEvent(
+ eq(UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES), any());
+ }
+
+ private void verifyNotifyDefaultSwitch(TestNetworkAgent agent) {
+ mCallbackOrder.verify(mListener).onUpstreamEvent(
+ eq(UpstreamNetworkMonitor.EVENT_DEFAULT_SWITCHED),
+ argThat(uns ->
+ uns instanceof UpstreamNetworkState
+ && agent.networkId.equals(((UpstreamNetworkState) uns).network)
+ && agent.linkProperties.equals(((UpstreamNetworkState) uns).linkProperties)
+ && agent.networkCapabilities.equals(
+ ((UpstreamNetworkState) uns).networkCapabilities)));
}
private void addLinkAddresses(LinkProperties lp, String... addrs) {
@@ -673,33 +689,6 @@
return false;
}
- public static class TestStateMachine extends StateMachine {
- public final ArrayList<Message> messages = new ArrayList<>();
- private final State mLoggingState = new LoggingState();
-
- class LoggingState extends State {
- @Override public void enter() {
- messages.clear();
- }
-
- @Override public void exit() {
- messages.clear();
- }
-
- @Override public boolean processMessage(Message msg) {
- messages.add(msg);
- return true;
- }
- }
-
- public TestStateMachine(Looper looper) {
- super("UpstreamNetworkMonitor.TestStateMachine", looper);
- addState(mLoggingState);
- setInitialState(mLoggingState);
- super.start();
- }
- }
-
static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
final Set<String> expectedSet = new HashSet<>();
Collections.addAll(expectedSet, expected);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
new file mode 100644
index 0000000..f8e98e3
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/StateMachineShimTest.kt
@@ -0,0 +1,135 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.networkstack.tethering.util
+
+import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.util.State
+import com.android.networkstack.tethering.util.StateMachineShim.AsyncStateMachine
+import com.android.networkstack.tethering.util.StateMachineShim.Dependencies
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StateMachineShimTest {
+ private val mSyncSM = mock(SyncStateMachine::class.java)
+ private val mAsyncSM = mock(AsyncStateMachine::class.java)
+ private val mState1 = mock(State::class.java)
+ private val mState2 = mock(State::class.java)
+
+ inner class MyDependencies() : Dependencies() {
+
+ override fun makeSyncStateMachine(name: String, thread: Thread) = mSyncSM
+
+ override fun makeAsyncStateMachine(name: String, looper: Looper) = mAsyncSM
+ }
+
+ @Test
+ fun testUsingSyncStateMachine() {
+ val inOrder = inOrder(mSyncSM, mAsyncSM)
+ val shimUsingSyncSM = StateMachineShim("ShimTest", null, MyDependencies())
+ shimUsingSyncSM.start(mState1)
+ inOrder.verify(mSyncSM).start(mState1)
+
+ val allStates = ArrayList<StateInfo>()
+ allStates.add(StateInfo(mState1, null))
+ allStates.add(StateInfo(mState2, mState1))
+ shimUsingSyncSM.addAllStates(allStates)
+ inOrder.verify(mSyncSM).addAllStates(allStates)
+
+ shimUsingSyncSM.transitionTo(mState1)
+ inOrder.verify(mSyncSM).transitionTo(mState1)
+
+ val what = 10
+ shimUsingSyncSM.sendMessage(what)
+ inOrder.verify(mSyncSM).processMessage(what, 0, 0, null)
+ val obj = Object()
+ shimUsingSyncSM.sendMessage(what, obj)
+ inOrder.verify(mSyncSM).processMessage(what, 0, 0, obj)
+ val arg1 = 11
+ shimUsingSyncSM.sendMessage(what, arg1)
+ inOrder.verify(mSyncSM).processMessage(what, arg1, 0, null)
+ val arg2 = 12
+ shimUsingSyncSM.sendMessage(what, arg1, arg2, obj)
+ inOrder.verify(mSyncSM).processMessage(what, arg1, arg2, obj)
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingSyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+ }
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingSyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+ }
+
+ shimUsingSyncSM.sendSelfMessageToSyncSM(what, obj)
+ inOrder.verify(mSyncSM).sendSelfMessage(what, 0, 0, obj)
+
+ verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+ }
+
+ @Test
+ fun testUsingAsyncStateMachine() {
+ val inOrder = inOrder(mSyncSM, mAsyncSM)
+ val shimUsingAsyncSM = StateMachineShim("ShimTest", mock(Looper::class.java),
+ MyDependencies())
+ shimUsingAsyncSM.start(mState1)
+ inOrder.verify(mAsyncSM).setInitialState(mState1)
+ inOrder.verify(mAsyncSM).start()
+
+ val allStates = ArrayList<StateInfo>()
+ allStates.add(StateInfo(mState1, null))
+ allStates.add(StateInfo(mState2, mState1))
+ shimUsingAsyncSM.addAllStates(allStates)
+ inOrder.verify(mAsyncSM).addState(mState1, null)
+ inOrder.verify(mAsyncSM).addState(mState2, mState1)
+
+ shimUsingAsyncSM.transitionTo(mState1)
+ inOrder.verify(mAsyncSM).transitionTo(mState1)
+
+ val what = 10
+ shimUsingAsyncSM.sendMessage(what)
+ inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, null)
+ val obj = Object()
+ shimUsingAsyncSM.sendMessage(what, obj)
+ inOrder.verify(mAsyncSM).sendMessage(what, 0, 0, obj)
+ val arg1 = 11
+ shimUsingAsyncSM.sendMessage(what, arg1)
+ inOrder.verify(mAsyncSM).sendMessage(what, arg1, 0, null)
+ val arg2 = 12
+ shimUsingAsyncSM.sendMessage(what, arg1, arg2, obj)
+ inOrder.verify(mAsyncSM).sendMessage(what, arg1, arg2, obj)
+
+ shimUsingAsyncSM.sendMessageDelayedToAsyncSM(what, 1000 /* delayMillis */)
+ inOrder.verify(mAsyncSM).sendMessageDelayed(what, 1000)
+
+ shimUsingAsyncSM.sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+ inOrder.verify(mAsyncSM).sendMessageAtFrontOfQueueToAsyncSM(what, arg1)
+
+ assertFailsWith(IllegalStateException::class) {
+ shimUsingAsyncSM.sendSelfMessageToSyncSM(what, obj)
+ }
+
+ verifyNoMoreInteractions(mSyncSM, mAsyncSM)
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
new file mode 100644
index 0000000..3a57fdd
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/util/SyncStateMachineTest.kt
@@ -0,0 +1,294 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.networkstack.tethering.util
+
+import android.os.Message
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.util.State
+import com.android.networkstack.tethering.util.SyncStateMachine.StateInfo
+import java.util.ArrayDeque
+import java.util.ArrayList
+import kotlin.test.assertFailsWith
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+private const val MSG_INVALID = -1
+private const val MSG_1 = 1
+private const val MSG_2 = 2
+private const val MSG_3 = 3
+private const val MSG_4 = 4
+private const val MSG_5 = 5
+private const val MSG_6 = 6
+private const val MSG_7 = 7
+private const val ARG_1 = 100
+private const val ARG_2 = 200
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class SynStateMachineTest {
+ private val mState1 = spy(object : TestState(MSG_1) {})
+ private val mState2 = spy(object : TestState(MSG_2) {})
+ private val mState3 = spy(object : TestState(MSG_3) {})
+ private val mState4 = spy(object : TestState(MSG_4) {})
+ private val mState5 = spy(object : TestState(MSG_5) {})
+ private val mState6 = spy(object : TestState(MSG_6) {})
+ private val mState7 = spy(object : TestState(MSG_7) {})
+ private val mInOrder = inOrder(mState1, mState2, mState3, mState4, mState5, mState6, mState7)
+ // Lazy initialize to make sure running in test thread.
+ private val mSM by lazy {
+ SyncStateMachine("TestSyncStateMachine", Thread.currentThread(), true /* debug */)
+ }
+ private val mAllStates = ArrayList<StateInfo>()
+
+ private val mMsgProcessedResults = ArrayDeque<Pair<State, Int>>()
+
+ open inner class TestState(val expected: Int) : State() {
+ // Control destination state in obj field for testing.
+ override fun processMessage(msg: Message): Boolean {
+ mMsgProcessedResults.add(this to msg.what)
+ assertEquals(ARG_1, msg.arg1)
+ assertEquals(ARG_2, msg.arg2)
+
+ if (msg.what == expected) {
+ msg.obj?.let { mSM.transitionTo(it as State) }
+ return true
+ }
+
+ return false
+ }
+ }
+
+ private fun verifyNoMoreInteractions() {
+ verifyNoMoreInteractions(mState1, mState2, mState3, mState4, mState5, mState6)
+ }
+
+ private fun processMessage(what: Int, toState: State?) {
+ mSM.processMessage(what, ARG_1, ARG_2, toState)
+ }
+
+ private fun verifyMessageProcessedBy(what: Int, vararg processedStates: State) {
+ for (state in processedStates) {
+ // InOrder.verify can't check the Message content here because SyncSM will recycle the
+ // message after it's been processed. SyncSM reuses the same Message instance for all
+ // messages it processes. So, if using InOrder.verify to verify the content of a message
+ // after SyncSM has processed it, the content would be wrong.
+ mInOrder.verify(state).processMessage(any())
+ val (processedState, msgWhat) = mMsgProcessedResults.remove()
+ assertEquals(state, processedState)
+ assertEquals(what, msgWhat)
+ }
+ assertTrue(mMsgProcessedResults.isEmpty())
+ }
+
+ @Test
+ fun testInitialState() {
+ // mState1 -> initial
+ // |
+ // mState2
+ mAllStates.add(StateInfo(mState1, null))
+ mAllStates.add(StateInfo(mState2, mState1))
+ mSM.addAllStates(mAllStates)
+
+ mSM.start(mState1)
+ mInOrder.verify(mState1).enter()
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testStartFromLeafState() {
+ // mState1 -> initial
+ // |
+ // mState2
+ // |
+ // mState3
+ mAllStates.add(StateInfo(mState1, null))
+ mAllStates.add(StateInfo(mState2, mState1))
+ mAllStates.add(StateInfo(mState3, mState2))
+ mSM.addAllStates(mAllStates)
+
+ mSM.start(mState3)
+ mInOrder.verify(mState1).enter()
+ mInOrder.verify(mState2).enter()
+ mInOrder.verify(mState3).enter()
+ verifyNoMoreInteractions()
+ }
+
+ private fun verifyStart() {
+ mSM.addAllStates(mAllStates)
+ mSM.start(mState1)
+ mInOrder.verify(mState1).enter()
+ verifyNoMoreInteractions()
+ }
+
+ fun addState(state: State, parent: State? = null) {
+ mAllStates.add(StateInfo(state, parent))
+ }
+
+ @Test
+ fun testAddState() {
+ // Add duplicated states.
+ mAllStates.add(StateInfo(mState1, null))
+ mAllStates.add(StateInfo(mState1, null))
+ assertFailsWith(IllegalStateException::class) {
+ mSM.addAllStates(mAllStates)
+ }
+ }
+
+ @Test
+ fun testProcessMessage() {
+ // mState1
+ // |
+ // mState2
+ addState(mState1)
+ addState(mState2, mState1)
+ verifyStart()
+
+ processMessage(MSG_1, null)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testTwoStates() {
+ // mState1 <-initial, mState2
+ addState(mState1)
+ addState(mState2)
+ verifyStart()
+
+ // Test transition to mState2
+ processMessage(MSG_1, mState2)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ mInOrder.verify(mState1).exit()
+ mInOrder.verify(mState2).enter()
+ verifyNoMoreInteractions()
+
+ // If set destState to mState2 (current state), no state transition.
+ processMessage(MSG_2, mState2)
+ verifyMessageProcessedBy(MSG_2, mState2)
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testTwoStateTrees() {
+ // mState1 -> initial mState4
+ // / \ / \
+ // mState2 mState3 mState5 mState6
+ addState(mState1)
+ addState(mState2, mState1)
+ addState(mState3, mState1)
+ addState(mState4)
+ addState(mState5, mState4)
+ addState(mState6, mState4)
+ verifyStart()
+
+ // mState1 -> current mState4
+ // / \ / \
+ // mState2 mState3 -> dest mState5 mState6
+ processMessage(MSG_1, mState3)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ mInOrder.verify(mState3).enter()
+ verifyNoMoreInteractions()
+
+ // mState1 mState4
+ // / \ / \
+ // dest <- mState2 mState3 -> current mState5 mState6
+ processMessage(MSG_1, mState2)
+ verifyMessageProcessedBy(MSG_1, mState3, mState1)
+ mInOrder.verify(mState3).exit()
+ mInOrder.verify(mState2).enter()
+ verifyNoMoreInteractions()
+
+ // mState1 mState4
+ // / \ / \
+ // current <- mState2 mState3 mState5 mState6 -> dest
+ processMessage(MSG_2, mState6)
+ verifyMessageProcessedBy(MSG_2, mState2)
+ mInOrder.verify(mState2).exit()
+ mInOrder.verify(mState1).exit()
+ mInOrder.verify(mState4).enter()
+ mInOrder.verify(mState6).enter()
+ verifyNoMoreInteractions()
+ }
+
+ @Test
+ fun testMultiDepthTransition() {
+ // mState1 -> current
+ // | \
+ // mState2 mState6
+ // | \ |
+ // mState3 mState5 mState7
+ // |
+ // mState4
+ addState(mState1)
+ addState(mState2, mState1)
+ addState(mState6, mState1)
+ addState(mState3, mState2)
+ addState(mState5, mState2)
+ addState(mState7, mState6)
+ addState(mState4, mState3)
+ verifyStart()
+
+ // mState1 -> current
+ // | \
+ // mState2 mState6
+ // | \ |
+ // mState3 mState5 mState7
+ // |
+ // mState4 -> dest
+ processMessage(MSG_1, mState4)
+ verifyMessageProcessedBy(MSG_1, mState1)
+ mInOrder.verify(mState2).enter()
+ mInOrder.verify(mState3).enter()
+ mInOrder.verify(mState4).enter()
+ verifyNoMoreInteractions()
+
+ // mState1
+ // / \
+ // mState2 mState6
+ // | \ \
+ // mState3 mState5 -> dest mState7
+ // |
+ // mState4 -> current
+ processMessage(MSG_1, mState5)
+ verifyMessageProcessedBy(MSG_1, mState4, mState3, mState2, mState1)
+ mInOrder.verify(mState4).exit()
+ mInOrder.verify(mState3).exit()
+ mInOrder.verify(mState5).enter()
+ verifyNoMoreInteractions()
+
+ // mState1
+ // / \
+ // mState2 mState6
+ // | \ \
+ // mState3 mState5 -> current mState7 -> dest
+ // |
+ // mState4
+ processMessage(MSG_2, mState7)
+ verifyMessageProcessedBy(MSG_2, mState5, mState2)
+ mInOrder.verify(mState5).exit()
+ mInOrder.verify(mState2).exit()
+ mInOrder.verify(mState6).enter()
+ mInOrder.verify(mState7).enter()
+ verifyNoMoreInteractions()
+ }
+}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index b3f8ed6..cdf47e7 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -45,6 +45,7 @@
"com.android.tethering",
],
visibility: [
+ "//packages/modules/Connectivity/DnsResolver",
"//packages/modules/Connectivity/netd",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/service/native/libs/libclat",
diff --git a/bpf_progs/block.c b/bpf_progs/block.c
index d734b74..0a2b0b8 100644
--- a/bpf_progs/block.c
+++ b/bpf_progs/block.c
@@ -24,8 +24,8 @@
#include "bpf_helpers.h"
-#define ALLOW 1
-#define DISALLOW 0
+static const int ALLOW = 1;
+static const int DISALLOW = 0;
DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t,
1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM)
@@ -57,14 +57,18 @@
return ALLOW;
}
-DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM,
- bind4_block_port, KVER(5, 4, 0))
+// the program need to be accessible/loadable by netd (from netd updatable plugin)
+#define DEFINE_NETD_RO_BPF_PROG(SECTION_NAME, the_prog, min_kver) \
+ DEFINE_BPF_PROG_EXT(SECTION_NAME, AID_ROOT, AID_ROOT, the_prog, min_kver, KVER_INF, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+ "", "netd_readonly/", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
+
+DEFINE_NETD_RO_BPF_PROG("bind4/block_port", bind4_block_port, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
-DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM,
- bind6_block_port, KVER(5, 4, 0))
+DEFINE_NETD_RO_BPF_PROG("bind6/block_port", bind6_block_port, KVER_4_19)
(struct bpf_sock_addr *ctx) {
return block_port(ctx);
}
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index ed33cc9..f3c7de5 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -87,29 +87,18 @@
if (skb->data_end - skb->data < len) bpf_skb_pull_data(skb, len);
}
-// constants for passing in to 'bool egress'
-static const bool INGRESS = false;
-static const bool EGRESS = true;
+struct egress_bool { bool egress; };
+#define INGRESS ((struct egress_bool){ .egress = false })
+#define EGRESS ((struct egress_bool){ .egress = true })
-// constants for passing in to 'bool downstream'
-static const bool UPSTREAM = false;
-static const bool DOWNSTREAM = true;
+struct stream_bool { bool down; };
+#define UPSTREAM ((struct stream_bool){ .down = false })
+#define DOWNSTREAM ((struct stream_bool){ .down = true })
-// constants for passing in to 'bool is_ethernet'
-static const bool RAWIP = false;
-static const bool ETHER = true;
+struct rawip_bool { bool rawip; };
+#define ETHER ((struct rawip_bool){ .rawip = false })
+#define RAWIP ((struct rawip_bool){ .rawip = true })
-// constants for passing in to 'bool updatetime'
-static const bool NO_UPDATETIME = false;
-static const bool UPDATETIME = true;
-
-// constants for passing in to ignore_on_eng / ignore_on_user / ignore_on_userdebug
-// define's instead of static const due to tm-mainline-prod compiler static_assert limitations
-#define LOAD_ON_ENG false
-#define LOAD_ON_USER false
-#define LOAD_ON_USERDEBUG false
-#define IGNORE_ON_ENG true
-#define IGNORE_ON_USER true
-#define IGNORE_ON_USERDEBUG true
-
-#define KVER_4_14 KVER(4, 14, 0)
+struct updatetime_bool { bool updatetime; };
+#define NO_UPDATETIME ((struct updatetime_bool){ .updatetime = false })
+#define UPDATETIME ((struct updatetime_bool){ .updatetime = true })
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index 8f0ff84..addb02f 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -55,8 +55,10 @@
DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
static inline __always_inline int nat64(struct __sk_buff* skb,
- const bool is_ethernet,
- const unsigned kver) {
+ const struct rawip_bool rawip,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Require ethernet dst mac address to be our unicast address.
if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
@@ -115,7 +117,7 @@
if (proto == IPPROTO_FRAGMENT) {
// Fragment handling requires bpf_skb_adjust_room which is 4.14+
- if (kver < KVER_4_14) return TC_ACT_PIPE;
+ if (!KVER_IS_AT_LEAST(kver, 4, 14, 0)) return TC_ACT_PIPE;
// Must have (ethernet and) ipv6 header and ipv6 fragment extension header
if (data + l2_header_size + sizeof(*ip6) + sizeof(struct frag_hdr) > data_end)
@@ -233,7 +235,7 @@
//
// Note: we currently have no TreeHugger coverage for 4.9-T devices (there are no such
// Pixel or cuttlefish devices), so likely you won't notice for months if this breaks...
- if (kver >= KVER_4_14 && frag_off != htons(IP_DF)) {
+ if (KVER_IS_AT_LEAST(kver, 4, 14, 0) && frag_off != htons(IP_DF)) {
// If we're converting an IPv6 Fragment, we need to trim off 8 more bytes
// We're beyond recovery on error here... but hard to imagine how this could fail.
if (bpf_skb_adjust_room(skb, -(__s32)sizeof(struct frag_hdr), BPF_ADJ_ROOM_NET, /*flags*/0))
diff --git a/bpf_progs/dscpPolicy.c b/bpf_progs/dscpPolicy.c
index 88b50b5..e845a69 100644
--- a/bpf_progs/dscpPolicy.c
+++ b/bpf_progs/dscpPolicy.c
@@ -222,7 +222,7 @@
}
DEFINE_BPF_PROG_KVER("schedcls/set_dscp_ether", AID_ROOT, AID_SYSTEM, schedcls_set_dscp_ether,
- KVER(5, 15, 0))
+ KVER_5_15)
(struct __sk_buff* skb) {
if (skb->pkt_type != PACKET_HOST) return TC_ACT_PIPE;
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 256dd6a..f223dd1 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -59,18 +59,18 @@
#define TCP_FLAG8_OFF (TCP_FLAG32_OFF + 1)
// For maps netd does not need to access
-#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
- AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// For maps netd only needs read only access to
-#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
- AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
- LOAD_ON_USER, LOAD_ON_USERDEBUG)
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", \
+ PRIVATE, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// For maps netd needs to be able to read and write
#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -92,7 +92,7 @@
DEFINE_BPF_MAP_RO_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
-DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
DEFINE_BPF_MAP_RO_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
DEFINE_BPF_MAP_NO_NETD(ingress_discard_map, HASH, IngressDiscardKey, IngressDiscardValue,
INGRESS_DISCARD_MAP_SIZE)
@@ -102,16 +102,19 @@
// A single-element configuration array, packet tracing is enabled when 'true'.
DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
- AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+ AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG)
// A ring buffer on which packet information is pushed.
DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
- AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+ AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", PRIVATE,
BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, LOAD_ON_ENG,
LOAD_ON_USER, LOAD_ON_USERDEBUG);
+DEFINE_BPF_MAP_RO_NETD(data_saver_enabled_map, ARRAY, uint32_t, bool,
+ DATA_SAVER_ENABLED_MAP_SIZE)
+
// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
// selinux contexts, because even non-xt_bpf iptables mutations are implemented as
// a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -127,8 +130,8 @@
// which is loaded into netd and thus runs as netd uid/gid/selinux context)
#define DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, minKV, maxKV) \
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
- minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, false, \
- "fs_bpf_netd_readonly", "", false, false, false)
+ minKV, maxKV, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+ "fs_bpf_netd_readonly", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_NETD_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_NETD_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF)
@@ -139,14 +142,8 @@
// programs that only need to be usable by the system server
#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, false, "fs_bpf_net_shared", \
- "", false, false, false)
-
-static __always_inline int is_system_uid(uint32_t uid) {
- // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
- // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
- return (uid < AID_APP_START);
-}
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, MANDATORY, \
+ "fs_bpf_net_shared", "", LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
/*
* Note: this blindly assumes an MTU of 1500, and that packets > MTU are always TCP,
@@ -178,8 +175,8 @@
#define DEFINE_UPDATE_STATS(the_stats_map, TypeOfKey) \
static __always_inline inline void update_##the_stats_map(const struct __sk_buff* const skb, \
const TypeOfKey* const key, \
- const bool egress, \
- const unsigned kver) { \
+ const struct egress_bool egress, \
+ const struct kver_uint kver) { \
StatsValue* value = bpf_##the_stats_map##_lookup_elem(key); \
if (!value) { \
StatsValue newValue = {}; \
@@ -199,7 +196,7 @@
packets = (payload + mss - 1) / mss; \
bytes = tcp_overhead * packets + payload; \
} \
- if (egress) { \
+ if (egress.egress) { \
__sync_fetch_and_add(&value->txPackets, packets); \
__sync_fetch_and_add(&value->txBytes, bytes); \
} else { \
@@ -219,7 +216,7 @@
const int L3_off,
void* const to,
const int len,
- const unsigned kver) {
+ const struct kver_uint kver) {
// 'kver' (here and throughout) is the compile time guaranteed minimum kernel version,
// ie. we're building (a version of) the bpf program for kver (or newer!) kernels.
//
@@ -236,16 +233,16 @@
//
// For similar reasons this will fail with non-offloaded VLAN tags on < 4.19 kernels,
// since those extend the ethernet header from 14 to 18 bytes.
- return kver >= KVER(4, 19, 0)
+ return KVER_IS_AT_LEAST(kver, 4, 19, 0)
? bpf_skb_load_bytes_relative(skb, L3_off, to, len, BPF_HDR_START_NET)
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
static __always_inline inline void do_packet_tracing(
- const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
- const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+ const struct __sk_buff* const skb, const struct egress_bool egress, const uint32_t uid,
+ const uint32_t tag, const bool enable_tracing, const struct kver_uint kver) {
if (!enable_tracing) return;
- if (kver < KVER(5, 8, 0)) return;
+ if (!KVER_IS_AT_LEAST(kver, 5, 8, 0)) return;
uint32_t mapKey = 0;
bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
@@ -317,8 +314,8 @@
pkt->sport = sport;
pkt->dport = dport;
- pkt->egress = egress;
- pkt->wakeup = !egress && (skb->mark & 0x80000000); // Fwmark.ingress_cpu_wakeup
+ pkt->egress = egress.egress;
+ pkt->wakeup = !egress.egress && (skb->mark & 0x80000000); // Fwmark.ingress_cpu_wakeup
pkt->ipProto = proto;
pkt->tcpFlags = flags;
pkt->ipVersion = ipVersion;
@@ -326,8 +323,9 @@
bpf_packet_trace_ringbuf_submit(pkt);
}
-static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, bool egress,
- const unsigned kver) {
+static __always_inline inline bool skip_owner_match(struct __sk_buff* skb,
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
uint32_t flag = 0;
if (skb->protocol == htons(ETH_P_IP)) {
uint8_t proto;
@@ -358,7 +356,7 @@
return false;
}
// Always allow RST's, and additionally allow ingress FINs
- return flag & (TCP_FLAG_RST | (egress ? 0 : TCP_FLAG_FIN)); // false on read failure
+ return flag & (TCP_FLAG_RST | (egress.egress ? 0 : TCP_FLAG_FIN)); // false on read failure
}
static __always_inline inline BpfConfig getConfig(uint32_t configKey) {
@@ -372,11 +370,11 @@
}
static __always_inline inline bool ingress_should_discard(struct __sk_buff* skb,
- const unsigned kver) {
+ const struct kver_uint kver) {
// Require 4.19, since earlier kernels don't have bpf_skb_load_bytes_relative() which
// provides relative to L3 header reads. Without that we could fetch the wrong bytes.
// Additionally earlier bpf verifiers are much harder to please.
- if (kver < KVER(4, 19, 0)) return false;
+ if (!KVER_IS_AT_LEAST(kver, 4, 19, 0)) return false;
IngressDiscardKey k = {};
if (skb->protocol == htons(ETH_P_IP)) {
@@ -401,7 +399,8 @@
}
static __always_inline inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid,
- bool egress, const unsigned kver) {
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
if (is_system_uid(uid)) return PASS;
if (skip_owner_match(skb, egress, kver)) return PASS;
@@ -414,7 +413,7 @@
if (isBlockedByUidRules(enabledRules, uidRules)) return DROP;
- if (!egress && skb->ifindex != 1) {
+ if (!egress.egress && skb->ifindex != 1) {
if (ingress_should_discard(skb, kver)) return DROP;
if (uidRules & IIF_MATCH) {
if (allowed_iif && skb->ifindex != allowed_iif) {
@@ -434,8 +433,8 @@
static __always_inline inline void update_stats_with_config(const uint32_t selectedMap,
const struct __sk_buff* const skb,
const StatsKey* const key,
- const bool egress,
- const unsigned kver) {
+ const struct egress_bool egress,
+ const struct kver_uint kver) {
if (selectedMap == SELECT_MAP_A) {
update_stats_map_A(skb, key, egress, kver);
} else {
@@ -443,9 +442,10 @@
}
}
-static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb,
+ const struct egress_bool egress,
const bool enable_tracing,
- const unsigned kver) {
+ const struct kver_uint kver) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
uint64_t cookie = bpf_get_socket_cookie(skb);
UidTagValue* utag = bpf_cookie_tag_map_lookup_elem(&cookie);
@@ -462,7 +462,7 @@
// interface is accounted for and subject to usage restrictions.
// CLAT IPv6 TX sockets are *always* tagged with CLAT uid, see tagSocketAsClat()
// CLAT daemon receives via an untagged AF_PACKET socket.
- if (egress && uid == AID_CLAT) return PASS;
+ if (egress.egress && uid == AID_CLAT) return PASS;
int match = bpf_owner_match(skb, sock_uid, egress, kver);
@@ -478,7 +478,7 @@
}
// If an outbound packet is going to be dropped, we do not count that traffic.
- if (egress && (match == DROP)) return DROP;
+ if (egress.egress && (match == DROP)) return DROP;
StatsKey key = {.uid = uid, .tag = tag, .counterSet = 0, .ifaceIndex = skb->ifindex};
@@ -505,60 +505,64 @@
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace_user, KVER(5, 8, 0), KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, true,
- "fs_bpf_netd_readonly", "", true, false, true)
+ bpf_cgroup_ingress_trace_user, KVER_5_8, KVER_INF,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+ "fs_bpf_netd_readonly", "",
+ IGNORE_ON_ENG, LOAD_ON_USER, IGNORE_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
- "fs_bpf_netd_readonly", "", false, true, false)
+ bpf_cgroup_ingress_trace, KVER_5_8, KVER_INF,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+ "fs_bpf_netd_readonly", "",
+ LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER_5_8);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
+ bpf_cgroup_ingress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_4_19);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
+ bpf_cgroup_ingress_4_14, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
}
// This program is optional, and enables tracing on Android U+, 5.8+ on user builds.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace_user", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace_user, KVER(5, 8, 0), KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, true,
- "fs_bpf_netd_readonly", "", true, false, true)
+ bpf_cgroup_egress_trace_user, KVER_5_8, KVER_INF,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, OPTIONAL,
+ "fs_bpf_netd_readonly", "",
+ LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
// This program is required, and enables tracing on Android U+, 5.8+, userdebug/eng.
DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
- BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
- "fs_bpf_netd_readonly", "", false, true, false)
+ bpf_cgroup_egress_trace, KVER_5_8, KVER_INF,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, MANDATORY,
+ "fs_bpf_netd_readonly", "",
+ LOAD_ON_ENG, IGNORE_ON_USER, LOAD_ON_USERDEBUG)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER_5_8);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
+ bpf_cgroup_egress_4_19, KVER_4_19, KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_4_19);
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
- bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
+ bpf_cgroup_egress_4_14, KVER_NONE, KVER_4_19)
(struct __sk_buff* skb) {
return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
}
@@ -633,9 +637,7 @@
return BPF_NOMATCH;
}
-DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
- KVER(4, 14, 0))
-(struct bpf_sock* sk) {
+static __always_inline inline uint8_t get_app_permissions() {
uint64_t gid_uid = bpf_get_current_uid_gid();
/*
* A given app is guaranteed to have the same app ID in all the profiles in
@@ -645,13 +647,15 @@
*/
uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET; // == PER_USER_RANGE == 100000
uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
- if (!permissions) {
- // UID not in map. Default to just INTERNET permission.
- return 1;
- }
+ // if UID not in map, then default to just INTERNET permission.
+ return permissions ? *permissions : BPF_PERMISSION_INTERNET;
+}
+DEFINE_NETD_BPF_PROG_KVER("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create,
+ KVER_4_14)
+(struct bpf_sock* sk) {
// A return value of 1 means allow, everything else means deny.
- return (*permissions & BPF_PERMISSION_INTERNET) == BPF_PERMISSION_INTERNET;
+ return (get_app_permissions() & BPF_PERMISSION_INTERNET) ? 1 : 0;
}
LICENSE("Apache 2.0");
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index dd27bf9..64ed633 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -16,6 +16,7 @@
#pragma once
+#include <cutils/android_filesystem_config.h>
#include <linux/if.h>
#include <linux/if_ether.h>
#include <linux/in.h>
@@ -125,6 +126,7 @@
static const int UID_OWNER_MAP_SIZE = 4000;
static const int INGRESS_DISCARD_MAP_SIZE = 100;
static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
+static const int DATA_SAVER_ENABLED_MAP_SIZE = 1;
#ifdef __cplusplus
@@ -171,6 +173,7 @@
#define INGRESS_DISCARD_MAP_PATH BPF_NETD_PATH "map_netd_ingress_discard_map"
#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
+#define DATA_SAVER_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_data_saver_enabled_map"
#endif // __cplusplus
@@ -189,8 +192,9 @@
OEM_DENY_1_MATCH = (1 << 9),
OEM_DENY_2_MATCH = (1 << 10),
OEM_DENY_3_MATCH = (1 << 11),
+ BACKGROUND_MATCH = (1 << 12)
};
-// LINT.ThenChange(packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java)
+// LINT.ThenChange(../framework/src/android/net/BpfNetMapsConstants.java)
enum BpfPermissionMatch {
BPF_PERMISSION_INTERNET = 1 << 2,
@@ -233,13 +237,16 @@
#define UID_RULES_CONFIGURATION_KEY 0
// Entry in the configuration map that stores which stats map is currently in use.
#define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
+// Entry in the data saver enabled map that stores whether data saver is enabled or not.
+#define DATA_SAVER_ENABLED_KEY 0
#undef STRUCT_SIZE
// DROP_IF_SET is set of rules that DROP if rule is globally enabled, and per-uid bit is set
#define DROP_IF_SET (STANDBY_MATCH | OEM_DENY_1_MATCH | OEM_DENY_2_MATCH | OEM_DENY_3_MATCH)
// DROP_IF_UNSET is set of rules that should DROP if globally enabled, and per-uid bit is NOT set
-#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH | LOW_POWER_STANDBY_MATCH)
+#define DROP_IF_UNSET (DOZABLE_MATCH | POWERSAVE_MATCH | RESTRICTED_MATCH \
+ | LOW_POWER_STANDBY_MATCH | BACKGROUND_MATCH)
// Warning: funky bit-wise arithmetic: in parallel, for all DROP_IF_SET/UNSET rules
// check whether the rules are globally enabled, and if so whether the rules are
@@ -249,3 +256,9 @@
static inline bool isBlockedByUidRules(BpfConfig enabledRules, uint32_t uidRules) {
return enabledRules & (DROP_IF_SET | DROP_IF_UNSET) & (uidRules ^ DROP_IF_UNSET);
}
+
+static inline bool is_system_uid(uint32_t uid) {
+ // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+ // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+ return (uid < AID_APP_START);
+}
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index c752779..90f96a1 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -124,8 +124,12 @@
DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
TETHERING_GID)
-static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const unsigned kver) {
+static inline __always_inline int do_forward6(struct __sk_buff* skb,
+ const struct rawip_bool rawip,
+ const struct stream_bool stream,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Must be meta-ethernet IPv6 frame
if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_PIPE;
@@ -184,7 +188,7 @@
TC_PUNT(NON_GLOBAL_DST);
// In the upstream direction do not forward traffic within the same /64 subnet.
- if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+ if (!stream.down && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
TC_PUNT(LOCAL_SRC_DST);
TetherDownstream6Key kd = {
@@ -194,17 +198,18 @@
TetherUpstream6Key ku = {
.iif = skb->ifindex,
- .src64 = 0,
+ // Retrieve the first 64 bits of the source IPv6 address in network order
+ .src64 = *(uint64_t*)&(ip6->saddr.s6_addr32[0]),
};
- if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+ if (is_ethernet) __builtin_memcpy(stream.down ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
- Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
- : bpf_tether_upstream6_map_lookup_elem(&ku);
+ Tether6Value* v = stream.down ? bpf_tether_downstream6_map_lookup_elem(&kd)
+ : bpf_tether_upstream6_map_lookup_elem(&ku);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_PIPE;
- uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+ uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
@@ -249,7 +254,7 @@
// We do this even if TX interface is RAWIP and thus does not need an ethernet header,
// because this is easier and the kernel will strip extraneous ethernet header.
if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_PUNT(CHANGE_HEAD_FAILED);
}
@@ -261,7 +266,7 @@
// I do not believe this can ever happen, but keep the verifier happy...
if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_DROP(TOO_SHORT);
}
};
@@ -281,8 +286,8 @@
// (-ENOTSUPP) if it isn't.
bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
- __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
- __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
// Overwrite any mac header with the new one
// For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
@@ -324,26 +329,26 @@
//
// Hence, these mandatory (must load successfully) implementations for 4.14+ kernels:
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream6_rawip_4_14, KVER(4, 14, 0))
+ sched_cls_tether_downstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
- return do_forward6(skb, RAWIP, DOWNSTREAM, KVER(4, 14, 0));
+ return do_forward6(skb, RAWIP, DOWNSTREAM, KVER_4_14);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream6_rawip_4_14, KVER(4, 14, 0))
+ sched_cls_tether_upstream6_rawip_4_14, KVER_4_14)
(struct __sk_buff* skb) {
- return do_forward6(skb, RAWIP, UPSTREAM, KVER(4, 14, 0));
+ return do_forward6(skb, RAWIP, UPSTREAM, KVER_4_14);
}
// and define no-op stubs for pre-4.14 kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -356,9 +361,10 @@
static inline __always_inline int do_forward4_bottom(struct __sk_buff* skb,
const int l2_header_size, void* data, const void* data_end,
- struct ethhdr* eth, struct iphdr* ip, const bool is_ethernet,
- const bool downstream, const bool updatetime, const bool is_tcp,
- const unsigned kver) {
+ struct ethhdr* eth, struct iphdr* ip, const struct rawip_bool rawip,
+ const struct stream_bool stream, const struct updatetime_bool updatetime,
+ const bool is_tcp, const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
@@ -416,13 +422,13 @@
};
if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
- Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
- : bpf_tether_upstream4_map_lookup_elem(&k);
+ Tether4Value* v = stream.down ? bpf_tether_downstream4_map_lookup_elem(&k)
+ : bpf_tether_upstream4_map_lookup_elem(&k);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_PIPE;
- uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+ uint32_t stat_and_limit_k = stream.down ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
@@ -467,7 +473,7 @@
// We do this even if TX interface is RAWIP and thus does not need an ethernet header,
// because this is easier and the kernel will strip extraneous ethernet header.
if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_PUNT(CHANGE_HEAD_FAILED);
}
@@ -481,7 +487,7 @@
// I do not believe this can ever happen, but keep the verifier happy...
if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
- __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxErrors : &stat_v->txErrors, 1);
TC_DROP(TOO_SHORT);
}
};
@@ -533,10 +539,10 @@
// This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
// and backported to all Android Common Kernel 4.14+ trees.
- if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+ if (updatetime.updatetime) v->last_used = bpf_ktime_get_boot_ns();
- __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
- __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(stream.down ? &stat_v->rxBytes : &stat_v->txBytes, L3_bytes);
// Redirect to forwarded interface.
//
@@ -547,8 +553,13 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
-static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
- const bool downstream, const bool updatetime, const unsigned kver) {
+static inline __always_inline int do_forward4(struct __sk_buff* skb,
+ const struct rawip_bool rawip,
+ const struct stream_bool stream,
+ const struct updatetime_bool updatetime,
+ const struct kver_uint kver) {
+ const bool is_ethernet = !rawip.rawip;
+
// Require ethernet dst mac address to be our unicast address.
if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_PIPE;
@@ -606,16 +617,16 @@
// in such a situation we can only support TCP. This also has the added nice benefit of
// using a separate error counter, and thus making it obvious which version of the program
// is loaded.
- if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+ if (!updatetime.updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
// We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
// but no need to check this if !updatetime due to check immediately above.
- if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+ if (updatetime.updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
TC_PUNT(NON_TCP_UDP);
// We want to make sure that the compiler will, in the !updatetime case, entirely optimize
// out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
- const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+ const bool is_tcp = !updatetime.updatetime || (ip->protocol == IPPROTO_TCP);
// This is a bit of a hack to make things easier on the bpf verifier.
// (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
@@ -636,37 +647,37 @@
// if the underlying requisite kernel support (bpf_ktime_get_boot_ns) was backported.
if (is_tcp) {
return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
- is_ethernet, downstream, updatetime, /* is_tcp */ true, kver);
+ rawip, stream, updatetime, /* is_tcp */ true, kver);
} else {
return do_forward4_bottom(skb, l2_header_size, data, data_end, eth, ip,
- is_ethernet, downstream, updatetime, /* is_tcp */ false, kver);
+ rawip, stream, updatetime, /* is_tcp */ false, kver);
}
}
// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
+ sched_cls_tether_downstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
+ sched_cls_tether_upstream4_rawip_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
+ sched_cls_tether_downstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_5_8);
}
DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+ sched_cls_tether_upstream4_ether_5_8, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(5, 8, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_5_8);
}
// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
@@ -675,33 +686,33 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_rawip_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_rawip_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_ether_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_ether_opt,
- KVER(4, 14, 0), KVER(5, 8, 0))
+ KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, UPDATETIME, KVER_4_14);
}
// Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
@@ -719,15 +730,15 @@
// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+ sched_cls_tether_downstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_5_4);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+ sched_cls_tether_upstream4_rawip_5_4, KVER_5_4, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(5, 4, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_5_4);
}
// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
@@ -736,31 +747,31 @@
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_downstream4_rawip_4_14,
- KVER(4, 14, 0), KVER(5, 4, 0))
+ KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
TETHERING_UID, TETHERING_GID,
sched_cls_tether_upstream4_rawip_4_14,
- KVER(4, 14, 0), KVER(5, 4, 0))
+ KVER_4_14, KVER_5_4)
(struct __sk_buff* skb) {
- return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, RAWIP, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+ sched_cls_tether_downstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, DOWNSTREAM, NO_UPDATETIME, KVER_4_14);
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+ sched_cls_tether_upstream4_ether_4_14, KVER_4_14, KVER_5_8)
(struct __sk_buff* skb) {
- return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER(4, 14, 0));
+ return do_forward4(skb, ETHER, UPSTREAM, NO_UPDATETIME, KVER_4_14);
}
// Placeholder (no-op) implementations for older Q kernels
@@ -768,13 +779,13 @@
// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+ sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER_5_4)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+ sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER_5_4)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -782,13 +793,13 @@
// ETHER: 4.9-P/Q kernel
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", TETHERING_UID, TETHERING_GID,
- sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+ sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER_4_14)
(struct __sk_buff* skb) {
return TC_ACT_PIPE;
}
@@ -797,17 +808,18 @@
DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, TETHERING_GID)
-static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
- const bool downstream) {
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const struct rawip_bool rawip,
+ const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
- const bool downstream) {
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const struct rawip_bool rawip,
+ const struct stream_bool stream) {
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx,
+ const struct stream_bool stream) {
const void* data = (void*)(long)ctx->data;
const void* data_end = (void*)(long)ctx->data_end;
const struct ethhdr* eth = data;
@@ -816,15 +828,16 @@
if ((void*)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto == htons(ETH_P_IPV6))
- return do_xdp_forward6(ctx, ETHER, downstream);
+ return do_xdp_forward6(ctx, ETHER, stream);
if (eth->h_proto == htons(ETH_P_IP))
- return do_xdp_forward4(ctx, ETHER, downstream);
+ return do_xdp_forward4(ctx, ETHER, stream);
// Anything else we don't know how to handle...
return XDP_PASS;
}
-static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx,
+ const struct stream_bool stream) {
const void* data = (void*)(long)ctx->data;
const void* data_end = (void*)(long)ctx->data_end;
@@ -832,15 +845,15 @@
if (data_end - data < 1) return XDP_PASS;
const uint8_t v = (*(uint8_t*)data) >> 4;
- if (v == 6) return do_xdp_forward6(ctx, RAWIP, downstream);
- if (v == 4) return do_xdp_forward4(ctx, RAWIP, downstream);
+ if (v == 6) return do_xdp_forward6(ctx, RAWIP, stream);
+ if (v == 4) return do_xdp_forward4(ctx, RAWIP, stream);
// Anything else we don't know how to handle...
return XDP_PASS;
}
#define DEFINE_XDP_PROG(str, func) \
- DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+ DEFINE_BPF_PROG_KVER(str, TETHERING_UID, TETHERING_GID, func, KVER_5_9)(struct xdp_md *ctx)
DEFINE_XDP_PROG("xdp/tether_downstream_ether",
xdp_tether_downstream_ether) {
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index 68469c8..70b08b7 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -49,7 +49,7 @@
DEFINE_BPF_MAP_GRW(bitmap, ARRAY, int, uint64_t, 2, TETHERING_GID)
DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", TETHERING_UID, TETHERING_GID,
- xdp_test, KVER(5, 9, 0))
+ xdp_test, KVER_5_9)
(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
diff --git a/common/Android.bp b/common/Android.bp
index ff4de11..1d73a46 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,6 +19,13 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+build = ["TrunkStable.bp"]
+
+// This is a placeholder comment to avoid merge conflicts
+// as the above target may not exist
+// depending on the branch
+
+// The library requires the final artifact to contain net-utils-device-common-struct.
java_library {
name: "connectivity-net-module-utils-bpf",
srcs: [
@@ -34,8 +41,9 @@
libs: [
"androidx.annotation_annotation",
"framework-connectivity.stubs.module_lib",
- ],
- static_libs: [
+ // For libraries which are statically linked in framework-connectivity, do not
+ // statically link here because callers of this library might already have a static
+ // version linked.
"net-utils-device-common-struct",
],
apex_available: [
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
new file mode 100644
index 0000000..56938fc
--- /dev/null
+++ b/common/TrunkStable.bp
@@ -0,0 +1,26 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+aconfig_declarations {
+ name: "com.android.net.flags-aconfig",
+ package: "com.android.net.flags",
+ srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "connectivity_flags_aconfig_lib",
+ aconfig_declarations: "com.android.net.flags-aconfig",
+}
diff --git a/common/flags.aconfig b/common/flags.aconfig
new file mode 100644
index 0000000..ad78d62
--- /dev/null
+++ b/common/flags.aconfig
@@ -0,0 +1,43 @@
+package: "com.android.net.flags"
+
+flag {
+ name: "track_multiple_network_activities"
+ namespace: "android_core_networking"
+ description: "NetworkActivityTracker tracks multiple networks including non default networks"
+ bug: "267870186"
+}
+
+flag {
+ name: "forbidden_capability"
+ namespace: "android_core_networking"
+ description: "This flag controls the forbidden capability API"
+ bug: "302997505"
+}
+
+flag {
+ name: "set_data_saver_via_cm"
+ namespace: "android_core_networking"
+ description: "Set data saver through ConnectivityManager API"
+ bug: "297836825"
+}
+
+flag {
+ name: "support_is_uid_networking_blocked"
+ namespace: "android_core_networking"
+ description: "This flag controls whether isUidNetworkingBlocked is supported"
+ bug: "297836825"
+}
+
+flag {
+ name: "basic_background_restrictions_enabled"
+ namespace: "android_core_networking"
+ description: "Block network access for apps in a low importance background state"
+ bug: "304347838"
+}
+
+flag {
+ name: "register_nsd_offload_engine"
+ namespace: "android_core_networking"
+ description: "The flag controls the access for registerOffloadEngine API in NsdManager"
+ bug: "294777050"
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
new file mode 100644
index 0000000..9fefb52
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardKey.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.InetAddressUtils;
+import com.android.net.module.util.Struct;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/** Key type for ingress discard map */
+public class IngressDiscardKey extends Struct {
+ // The destination ip of the incoming packet. IPv4 uses IPv4-mapped IPv6 address.
+ @Field(order = 0, type = Type.Ipv6Address)
+ public final Inet6Address dstAddr;
+
+ public IngressDiscardKey(final Inet6Address dstAddr) {
+ this.dstAddr = dstAddr;
+ }
+
+ private static Inet6Address getInet6Address(final InetAddress addr) {
+ return (addr instanceof Inet4Address)
+ ? InetAddressUtils.v4MappedV6Address((Inet4Address) addr)
+ : (Inet6Address) addr;
+ }
+
+ public IngressDiscardKey(final InetAddress dstAddr) {
+ this(getInet6Address(dstAddr));
+ }
+}
diff --git a/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
new file mode 100644
index 0000000..7df3620
--- /dev/null
+++ b/common/src/com/android/net/module/util/bpf/IngressDiscardValue.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.bpf;
+
+import com.android.net.module.util.Struct;
+
+/** Value type for ingress discard map */
+public class IngressDiscardValue extends Struct {
+ // Allowed interface indexes.
+ // Use the same value for iif1 and iif2 if there is only a single allowed interface index.
+ @Field(order = 0, type = Type.S32)
+ public final int iif1;
+ @Field(order = 1, type = Type.S32)
+ public final int iif2;
+
+ public IngressDiscardValue(final int iif1, final int iif2) {
+ this.iif1 = iif1;
+ this.iif2 = iif2;
+ }
+}
diff --git a/framework-t/api/OWNERS b/framework-t/api/OWNERS
index 607f85a..8ef735c 100644
--- a/framework-t/api/OWNERS
+++ b/framework-t/api/OWNERS
@@ -1,2 +1,3 @@
file:platform/packages/modules/Connectivity:main:/nearby/OWNERS
file:platform/packages/modules/Connectivity:main:/remoteauth/OWNERS
+file:platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/framework-t/api/module-lib-lint-baseline.txt b/framework-t/api/module-lib-lint-baseline.txt
index 3158bd4..6f954df 100644
--- a/framework-t/api/module-lib-lint-baseline.txt
+++ b/framework-t/api/module-lib-lint-baseline.txt
@@ -5,3 +5,17 @@
Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
BannedThrow: android.app.usage.NetworkStatsManager#queryTaggedSummary(android.net.NetworkTemplate, long, long):
Methods must not mention RuntimeException subclasses in throws clauses (was `java.lang.SecurityException`)
+
+
+MissingPermission: android.net.IpSecManager#startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress):
+ Feature field FEATURE_IPSEC_TUNNEL_MIGRATION required by method android.net.IpSecManager.startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress) is hidden or removed
+
+
+RequiresPermission: android.app.usage.NetworkStatsManager#registerUsageCallback(android.net.NetworkTemplate, long, java.util.concurrent.Executor, android.app.usage.NetworkStatsManager.UsageCallback):
+ Method 'registerUsageCallback' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#disableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'disableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#enableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'enableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#updateConfiguration(String, android.net.EthernetNetworkUpdateRequest, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'updateConfiguration' documentation mentions permissions already declared by @RequiresPermission
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index ea465aa..05cf9e8 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -59,12 +59,30 @@
}
public class NearbyManager {
+ method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
}
+ public final class OffloadCapability implements android.os.Parcelable {
+ method public int describeContents();
+ method public long getVersion();
+ method public boolean isFastPairSupported();
+ method public boolean isNearbyShareSupported();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.nearby.OffloadCapability> CREATOR;
+ }
+
+ public static final class OffloadCapability.Builder {
+ ctor public OffloadCapability.Builder();
+ method @NonNull public android.nearby.OffloadCapability build();
+ method @NonNull public android.nearby.OffloadCapability.Builder setFastPairSupported(boolean);
+ method @NonNull public android.nearby.OffloadCapability.Builder setNearbyShareSupported(boolean);
+ method @NonNull public android.nearby.OffloadCapability.Builder setVersion(long);
+ }
+
public final class PresenceBroadcastRequest extends android.nearby.BroadcastRequest implements android.os.Parcelable {
method public int describeContents();
method @NonNull public java.util.List<java.lang.Integer> getActions();
@@ -175,8 +193,14 @@
public interface ScanCallback {
method public void onDiscovered(@NonNull android.nearby.NearbyDevice);
+ method public default void onError(int);
method public void onLost(@NonNull android.nearby.NearbyDevice);
method public void onUpdated(@NonNull android.nearby.NearbyDevice);
+ field public static final int ERROR_INVALID_ARGUMENT = 2; // 0x2
+ field public static final int ERROR_PERMISSION_DENIED = 3; // 0x3
+ field public static final int ERROR_RESOURCE_EXHAUSTED = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ field public static final int ERROR_UNSUPPORTED = 1; // 0x1
}
public abstract class ScanFilter {
@@ -191,6 +215,7 @@
method public int getScanType();
method @NonNull public android.os.WorkSource getWorkSource();
method public boolean isBleEnabled();
+ method public boolean isOffloadOnly();
method public static boolean isValidScanMode(int);
method public static boolean isValidScanType(int);
method @NonNull public static String scanModeToString(int);
@@ -209,6 +234,7 @@
method @NonNull public android.nearby.ScanRequest.Builder addScanFilter(@NonNull android.nearby.ScanFilter);
method @NonNull public android.nearby.ScanRequest build();
method @NonNull public android.nearby.ScanRequest.Builder setBleEnabled(boolean);
+ method @NonNull public android.nearby.ScanRequest.Builder setOffloadOnly(boolean);
method @NonNull public android.nearby.ScanRequest.Builder setScanMode(int);
method @NonNull public android.nearby.ScanRequest.Builder setScanType(int);
method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public android.nearby.ScanRequest.Builder setWorkSource(@Nullable android.os.WorkSource);
@@ -279,6 +305,7 @@
ctor public NetworkStats(long, int);
method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
+ method public android.net.NetworkStats clone();
method public int describeContents();
method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
@@ -390,12 +417,125 @@
package android.net.thread {
- public class ThreadNetworkController {
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ActiveOperationalDataset implements android.os.Parcelable {
+ method public int describeContents();
+ method @NonNull public static android.net.thread.ActiveOperationalDataset fromThreadTlvs(@NonNull byte[]);
+ method @NonNull public android.net.thread.OperationalDatasetTimestamp getActiveTimestamp();
+ method @IntRange(from=0, to=65535) public int getChannel();
+ method @NonNull @Size(min=1) public android.util.SparseArray<byte[]> getChannelMask();
+ method @IntRange(from=0, to=255) public int getChannelPage();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) public byte[] getExtendedPanId();
+ method @NonNull public android.net.IpPrefix getMeshLocalPrefix();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) public byte[] getNetworkKey();
+ method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) public String getNetworkName();
+ method @IntRange(from=0, to=65534) public int getPanId();
+ method @NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) public byte[] getPskc();
+ method @NonNull public android.net.thread.ActiveOperationalDataset.SecurityPolicy getSecurityPolicy();
+ method @NonNull public byte[] toThreadTlvs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CHANNEL_MAX_24_GHZ = 26; // 0x1a
+ field public static final int CHANNEL_MIN_24_GHZ = 11; // 0xb
+ field public static final int CHANNEL_PAGE_24_GHZ = 0; // 0x0
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ActiveOperationalDataset> CREATOR;
+ field public static final int LENGTH_EXTENDED_PAN_ID = 8; // 0x8
+ field public static final int LENGTH_MAX_DATASET_TLVS = 254; // 0xfe
+ field public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16; // 0x10
+ field public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64; // 0x40
+ field public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1; // 0x1
+ field public static final int LENGTH_NETWORK_KEY = 16; // 0x10
+ field public static final int LENGTH_PSKC = 16; // 0x10
+ }
+
+ public static final class ActiveOperationalDataset.Builder {
+ ctor public ActiveOperationalDataset.Builder(@NonNull android.net.thread.ActiveOperationalDataset);
+ ctor public ActiveOperationalDataset.Builder();
+ method @NonNull public android.net.thread.ActiveOperationalDataset build();
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setActiveTimestamp(@NonNull android.net.thread.OperationalDatasetTimestamp);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannel(@IntRange(from=0, to=255) int, @IntRange(from=0, to=65535) int);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setChannelMask(@NonNull @Size(min=1) android.util.SparseArray<byte[]>);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setExtendedPanId(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setMeshLocalPrefix(@NonNull android.net.IpPrefix);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkKey(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setNetworkName(@NonNull @Size(min=android.net.thread.ActiveOperationalDataset.LENGTH_MIN_NETWORK_NAME_BYTES, max=android.net.thread.ActiveOperationalDataset.LENGTH_MAX_NETWORK_NAME_BYTES) String);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPanId(@IntRange(from=0, to=65534) int);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setPskc(@NonNull @Size(android.net.thread.ActiveOperationalDataset.LENGTH_PSKC) byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset.Builder setSecurityPolicy(@NonNull android.net.thread.ActiveOperationalDataset.SecurityPolicy);
+ }
+
+ public static final class ActiveOperationalDataset.SecurityPolicy {
+ ctor public ActiveOperationalDataset.SecurityPolicy(@IntRange(from=1, to=65535) int, @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[]);
+ method @NonNull @Size(min=android.net.thread.ActiveOperationalDataset.SecurityPolicy.LENGTH_MIN_SECURITY_POLICY_FLAGS) public byte[] getFlags();
+ method @IntRange(from=1, to=65535) public int getRotationTimeHours();
+ field public static final int DEFAULT_ROTATION_TIME_HOURS = 672; // 0x2a0
+ field public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1; // 0x1
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class OperationalDatasetTimestamp {
+ ctor public OperationalDatasetTimestamp(@IntRange(from=0, to=281474976710655L) long, @IntRange(from=0, to=32767) int, boolean);
+ method @NonNull public static android.net.thread.OperationalDatasetTimestamp fromInstant(@NonNull java.time.Instant);
+ method @IntRange(from=0, to=281474976710655L) public long getSeconds();
+ method @IntRange(from=0, to=32767) public int getTicks();
+ method public boolean isAuthoritativeSource();
+ method @NonNull public java.time.Instant toInstant();
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class PendingOperationalDataset implements android.os.Parcelable {
+ ctor public PendingOperationalDataset(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull android.net.thread.OperationalDatasetTimestamp, @NonNull java.time.Duration);
+ method public int describeContents();
+ method @NonNull public static android.net.thread.PendingOperationalDataset fromThreadTlvs(@NonNull byte[]);
+ method @NonNull public android.net.thread.ActiveOperationalDataset getActiveOperationalDataset();
+ method @NonNull public java.time.Duration getDelayTimer();
+ method @NonNull public android.net.thread.OperationalDatasetTimestamp getPendingTimestamp();
+ method @NonNull public byte[] toThreadTlvs();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.PendingOperationalDataset> CREATOR;
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
+ method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
method public int getThreadVersion();
+ method public static boolean isAttached(int);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void join(@NonNull android.net.thread.ActiveOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void leave(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void registerOperationalDatasetCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
+ method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+ method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void unregisterStateCallback(@NonNull android.net.thread.ThreadNetworkController.StateCallback);
+ field public static final int DEVICE_ROLE_CHILD = 2; // 0x2
+ field public static final int DEVICE_ROLE_DETACHED = 1; // 0x1
+ field public static final int DEVICE_ROLE_LEADER = 4; // 0x4
+ field public static final int DEVICE_ROLE_ROUTER = 3; // 0x3
+ field public static final int DEVICE_ROLE_STOPPED = 0; // 0x0
field public static final int THREAD_VERSION_1_3 = 4; // 0x4
}
- public class ThreadNetworkManager {
+ public static interface ThreadNetworkController.OperationalDatasetCallback {
+ method public void onActiveOperationalDatasetChanged(@Nullable android.net.thread.ActiveOperationalDataset);
+ method public default void onPendingOperationalDatasetChanged(@Nullable android.net.thread.PendingOperationalDataset);
+ }
+
+ public static interface ThreadNetworkController.StateCallback {
+ method public void onDeviceRoleChanged(int);
+ method public default void onPartitionIdChanged(long);
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public class ThreadNetworkException extends java.lang.Exception {
+ ctor public ThreadNetworkException(int, @NonNull String);
+ method public int getErrorCode();
+ field public static final int ERROR_ABORTED = 2; // 0x2
+ field public static final int ERROR_BUSY = 5; // 0x5
+ field public static final int ERROR_FAILED_PRECONDITION = 6; // 0x6
+ field public static final int ERROR_INTERNAL_ERROR = 1; // 0x1
+ field public static final int ERROR_REJECTED_BY_PEER = 8; // 0x8
+ field public static final int ERROR_RESOURCE_EXHAUSTED = 10; // 0xa
+ field public static final int ERROR_RESPONSE_BAD_FORMAT = 9; // 0x9
+ field public static final int ERROR_TIMEOUT = 3; // 0x3
+ field public static final int ERROR_UNAVAILABLE = 4; // 0x4
+ field public static final int ERROR_UNSUPPORTED_CHANNEL = 7; // 0x7
+ }
+
+ @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkManager {
method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
}
diff --git a/framework-t/api/system-lint-baseline.txt b/framework-t/api/system-lint-baseline.txt
index 9baf991..4f7af87 100644
--- a/framework-t/api/system-lint-baseline.txt
+++ b/framework-t/api/system-lint-baseline.txt
@@ -5,3 +5,161 @@
GenericException: android.net.IpSecManager.IpSecTunnelInterface#finalize():
Methods must not throw generic exceptions (`java.lang.Throwable`)
+
+
+MissingPermission: android.net.IpSecManager#startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress):
+ Feature field FEATURE_IPSEC_TUNNEL_MIGRATION required by method android.net.IpSecManager.startTunnelModeTransformMigration(android.net.IpSecTransform, java.net.InetAddress, java.net.InetAddress) is hidden or removed
+
+
+RequiresPermission: android.net.EthernetManager#disableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'disableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#enableInterface(String, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'enableInterface' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.EthernetManager#updateConfiguration(String, android.net.EthernetNetworkUpdateRequest, java.util.concurrent.Executor, android.os.OutcomeReceiver<java.lang.String,android.net.EthernetNetworkManagementException>):
+ Method 'updateConfiguration' documentation mentions permissions already declared by @RequiresPermission
+
+
+UnflaggedApi: android.nearby.CredentialElement#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.equals(Object)
+UnflaggedApi: android.nearby.CredentialElement#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.CredentialElement.hashCode()
+UnflaggedApi: android.nearby.DataElement#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.DataElement.equals(Object)
+UnflaggedApi: android.nearby.DataElement#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.DataElement.hashCode()
+UnflaggedApi: android.nearby.NearbyDevice#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.equals(Object)
+UnflaggedApi: android.nearby.NearbyDevice#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.hashCode()
+UnflaggedApi: android.nearby.NearbyDevice#toString():
+ New API must be flagged with @FlaggedApi: method android.nearby.NearbyDevice.toString()
+UnflaggedApi: android.nearby.OffloadCapability#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.equals(Object)
+UnflaggedApi: android.nearby.OffloadCapability#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.hashCode()
+UnflaggedApi: android.nearby.OffloadCapability#toString():
+ New API must be flagged with @FlaggedApi: method android.nearby.OffloadCapability.toString()
+UnflaggedApi: android.nearby.PresenceCredential#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.PresenceCredential.equals(Object)
+UnflaggedApi: android.nearby.PresenceCredential#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.PresenceCredential.hashCode()
+UnflaggedApi: android.nearby.PublicCredential#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.PublicCredential.equals(Object)
+UnflaggedApi: android.nearby.PublicCredential#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.PublicCredential.hashCode()
+UnflaggedApi: android.nearby.ScanRequest#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.equals(Object)
+UnflaggedApi: android.nearby.ScanRequest#hashCode():
+ New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.hashCode()
+UnflaggedApi: android.nearby.ScanRequest#toString():
+ New API must be flagged with @FlaggedApi: method android.nearby.ScanRequest.toString()
+UnflaggedApi: android.net.EthernetNetworkManagementException#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkManagementException.equals(Object)
+UnflaggedApi: android.net.EthernetNetworkManagementException#hashCode():
+ New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkManagementException.hashCode()
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.equals(Object)
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#hashCode():
+ New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.hashCode()
+UnflaggedApi: android.net.EthernetNetworkUpdateRequest#toString():
+ New API must be flagged with @FlaggedApi: method android.net.EthernetNetworkUpdateRequest.toString()
+UnflaggedApi: android.net.IpSecManager.IpSecTunnelInterface#finalize():
+ New API must be flagged with @FlaggedApi: method android.net.IpSecManager.IpSecTunnelInterface.finalize()
+UnflaggedApi: android.net.IpSecManager.IpSecTunnelInterface#toString():
+ New API must be flagged with @FlaggedApi: method android.net.IpSecManager.IpSecTunnelInterface.toString()
+UnflaggedApi: android.net.IpSecTransform.Builder#buildTunnelModeTransform(java.net.InetAddress, android.net.IpSecManager.SecurityParameterIndex):
+ New API must be flagged with @FlaggedApi: method android.net.IpSecTransform.Builder.buildTunnelModeTransform(java.net.InetAddress,android.net.IpSecManager.SecurityParameterIndex)
+UnflaggedApi: android.net.NetworkStats.Entry#toString():
+ New API must be flagged with @FlaggedApi: method android.net.NetworkStats.Entry.toString()
+UnflaggedApi: android.net.nsd.NsdManager#registerOffloadEngine(String, long, long, java.util.concurrent.Executor, android.net.nsd.OffloadEngine):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.NsdManager.registerOffloadEngine(String,long,long,java.util.concurrent.Executor,android.net.nsd.OffloadEngine)
+UnflaggedApi: android.net.nsd.NsdManager#unregisterOffloadEngine(android.net.nsd.OffloadEngine):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.NsdManager.unregisterOffloadEngine(android.net.nsd.OffloadEngine)
+UnflaggedApi: android.net.nsd.OffloadEngine:
+ New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadEngine
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_FILTER_QUERIES:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_QUERIES
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_FILTER_REPLIES:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_FILTER_REPLIES
+UnflaggedApi: android.net.nsd.OffloadEngine#OFFLOAD_TYPE_REPLY:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadEngine.OFFLOAD_TYPE_REPLY
+UnflaggedApi: android.net.nsd.OffloadEngine#onOffloadServiceRemoved(android.net.nsd.OffloadServiceInfo):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadEngine.onOffloadServiceRemoved(android.net.nsd.OffloadServiceInfo)
+UnflaggedApi: android.net.nsd.OffloadEngine#onOffloadServiceUpdated(android.net.nsd.OffloadServiceInfo):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadEngine.onOffloadServiceUpdated(android.net.nsd.OffloadServiceInfo)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo:
+ New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadServiceInfo
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#CONTENTS_FILE_DESCRIPTOR:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.CONTENTS_FILE_DESCRIPTOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#CREATOR:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.CREATOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#OffloadServiceInfo(android.net.nsd.OffloadServiceInfo.Key, java.util.List<java.lang.String>, String, byte[], int, long):
+ New API must be flagged with @FlaggedApi: constructor android.net.nsd.OffloadServiceInfo(android.net.nsd.OffloadServiceInfo.Key,java.util.List<java.lang.String>,String,byte[],int,long)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_STABILITY_LOCAL:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_STABILITY_LOCAL
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_STABILITY_VINTF:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_STABILITY_VINTF
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#PARCELABLE_WRITE_RETURN_VALUE:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.PARCELABLE_WRITE_RETURN_VALUE
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#describeContents():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.describeContents()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.equals(Object)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getHostname():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getHostname()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getKey():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getKey()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getOffloadPayload():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getOffloadPayload()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getOffloadType():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getOffloadType()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getPriority():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getPriority()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#getSubtypes():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.getSubtypes()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#hashCode():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.hashCode()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#toString():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.toString()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo#writeToParcel(android.os.Parcel, int):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.writeToParcel(android.os.Parcel,int)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key:
+ New API must be flagged with @FlaggedApi: class android.net.nsd.OffloadServiceInfo.Key
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#CONTENTS_FILE_DESCRIPTOR:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.CONTENTS_FILE_DESCRIPTOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#CREATOR:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.CREATOR
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#Key(String, String):
+ New API must be flagged with @FlaggedApi: constructor android.net.nsd.OffloadServiceInfo.Key(String,String)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_STABILITY_LOCAL:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_STABILITY_LOCAL
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_STABILITY_VINTF:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_STABILITY_VINTF
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#PARCELABLE_WRITE_RETURN_VALUE:
+ New API must be flagged with @FlaggedApi: field android.net.nsd.OffloadServiceInfo.Key.PARCELABLE_WRITE_RETURN_VALUE
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#describeContents():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.describeContents()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#equals(Object):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.equals(Object)
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#getServiceName():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.getServiceName()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#getServiceType():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.getServiceType()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#hashCode():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.hashCode()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#toString():
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.toString()
+UnflaggedApi: android.net.nsd.OffloadServiceInfo.Key#writeToParcel(android.os.Parcel, int):
+ New API must be flagged with @FlaggedApi: method android.net.nsd.OffloadServiceInfo.Key.writeToParcel(android.os.Parcel,int)
+UnflaggedApi: android.net.thread.ThreadNetworkController:
+ New API must be flagged with @FlaggedApi: class android.net.thread.ThreadNetworkController
+UnflaggedApi: android.net.thread.ThreadNetworkController#THREAD_VERSION_1_3:
+ New API must be flagged with @FlaggedApi: field android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3
+UnflaggedApi: android.net.thread.ThreadNetworkController#getThreadVersion():
+ New API must be flagged with @FlaggedApi: method android.net.thread.ThreadNetworkController.getThreadVersion()
+UnflaggedApi: android.net.thread.ThreadNetworkManager:
+ New API must be flagged with @FlaggedApi: class android.net.thread.ThreadNetworkManager
+UnflaggedApi: android.net.thread.ThreadNetworkManager#getAllThreadNetworkControllers():
+ New API must be flagged with @FlaggedApi: method android.net.thread.ThreadNetworkManager.getAllThreadNetworkControllers()
diff --git a/framework-t/src/android/net/EthernetNetworkSpecifier.java b/framework-t/src/android/net/EthernetNetworkSpecifier.java
index e4d6e24..90c0361 100644
--- a/framework-t/src/android/net/EthernetNetworkSpecifier.java
+++ b/framework-t/src/android/net/EthernetNetworkSpecifier.java
@@ -26,8 +26,6 @@
/**
* A {@link NetworkSpecifier} used to identify ethernet interfaces.
- *
- * @see EthernetManager
*/
public final class EthernetNetworkSpecifier extends NetworkSpecifier implements Parcelable {
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index e23faa4..20c5f30 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -31,6 +31,7 @@
import static android.net.NetworkTemplate.MATCH_BLUETOOTH;
import static android.net.NetworkTemplate.MATCH_ETHERNET;
import static android.net.NetworkTemplate.MATCH_MOBILE;
+import static android.net.NetworkTemplate.MATCH_PROXY;
import static android.net.NetworkTemplate.MATCH_WIFI;
import static android.net.TrafficStats.UID_REMOVED;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
@@ -784,6 +785,7 @@
dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_WIFI).build(), "wifi");
dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_ETHERNET).build(), "eth");
dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_BLUETOOTH).build(), "bt");
+ dumpCheckin(pw, start, end, new NetworkTemplate.Builder(MATCH_PROXY).build(), "proxy");
}
/**
diff --git a/framework-t/src/android/net/NetworkTemplate.java b/framework-t/src/android/net/NetworkTemplate.java
index 33bd884..77b166c 100644
--- a/framework-t/src/android/net/NetworkTemplate.java
+++ b/framework-t/src/android/net/NetworkTemplate.java
@@ -1170,7 +1170,7 @@
* @param matchRule the target match rule to be checked.
*/
private static void assertRequestableMatchRule(final int matchRule) {
- if (!isKnownMatchRule(matchRule) || matchRule == MATCH_PROXY) {
+ if (!isKnownMatchRule(matchRule)) {
throw new IllegalArgumentException("Invalid match rule: "
+ getMatchRuleName(matchRule));
}
diff --git a/framework-t/src/android/net/nsd/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java
index c11e60c..c7ded25 100644
--- a/framework-t/src/android/net/nsd/MDnsManager.java
+++ b/framework-t/src/android/net/nsd/MDnsManager.java
@@ -51,7 +51,7 @@
public void startDaemon() {
try {
mMdns.startDaemon();
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Start mdns failed.", e);
}
}
@@ -62,7 +62,7 @@
public void stopDaemon() {
try {
mMdns.stopDaemon();
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Stop mdns failed.", e);
}
}
@@ -85,7 +85,7 @@
registrationType, port, txtRecord, interfaceIdx);
try {
mMdns.registerService(info);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Register service failed.", e);
return false;
}
@@ -105,7 +105,7 @@
registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET);
try {
mMdns.discover(info);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Discover service failed.", e);
return false;
}
@@ -129,7 +129,7 @@
new byte[0] /* txtRecord */, interfaceIdx);
try {
mMdns.resolve(info);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Resolve service failed.", e);
return false;
}
@@ -149,7 +149,7 @@
"" /* address */, interfaceIdx, NETID_UNSET);
try {
mMdns.getServiceAddress(info);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Get service address failed.", e);
return false;
}
@@ -165,7 +165,7 @@
public boolean stopOperation(int id) {
try {
mMdns.stopOperation(id);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Stop operation failed.", e);
return false;
}
@@ -180,7 +180,7 @@
public void registerEventListener(@NonNull IMDnsEventListener listener) {
try {
mMdns.registerEventListener(listener);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Register listener failed.", e);
}
}
@@ -193,7 +193,7 @@
public void unregisterEventListener(@NonNull IMDnsEventListener listener) {
try {
mMdns.unregisterEventListener(listener);
- } catch (RemoteException | ServiceSpecificException e) {
+ } catch (RemoteException | ServiceSpecificException | UnsupportedOperationException e) {
Log.e(TAG, "Unregister listener failed.", e);
}
}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index ef0e34b..dae8914 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -34,10 +34,10 @@
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityThread;
import android.net.Network;
import android.net.NetworkRequest;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
@@ -632,10 +632,9 @@
*/
public NsdManager(Context context, INsdManager service) {
mContext = context;
-
- HandlerThread t = new HandlerThread("NsdManager");
- t.start();
- mHandler = new ServiceHandler(t.getLooper());
+ // Use a common singleton thread ConnectivityThread to be shared among all nsd tasks.
+ // Instead of launching separate threads to handle tasks from the various instances.
+ mHandler = new ServiceHandler(ConnectivityThread.getInstanceLooper());
try {
mService = service.connect(new NsdCallbackImpl(mHandler), CompatChanges.isChangeEnabled(
diff --git a/framework-t/udc-extended-api/system-current.txt b/framework-t/udc-extended-api/system-current.txt
index 1549089..6f0119e 100644
--- a/framework-t/udc-extended-api/system-current.txt
+++ b/framework-t/udc-extended-api/system-current.txt
@@ -305,6 +305,7 @@
ctor public NetworkStats(long, int);
method @NonNull public android.net.NetworkStats add(@NonNull android.net.NetworkStats);
method @NonNull public android.net.NetworkStats addEntry(@NonNull android.net.NetworkStats.Entry);
+ method public android.net.NetworkStats clone();
method public int describeContents();
method @NonNull public java.util.Iterator<android.net.NetworkStats.Entry> iterator();
method @NonNull public android.net.NetworkStats subtract(@NonNull android.net.NetworkStats);
diff --git a/framework/Android.bp b/framework/Android.bp
index 182c558..10acbd0 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -94,14 +94,16 @@
"framework-wifi.stubs.module_lib",
],
static_libs: [
- "mdns_aidl_interface-lateststable-java",
+ // Not using the latest stable version because all functions in the latest version of
+ // mdns_aidl_interface are deprecated.
+ "mdns_aidl_interface-V1-java",
"modules-utils-backgroundthread",
"modules-utils-build",
"modules-utils-preconditions",
"framework-connectivity-javastream-protos",
],
impl_only_static_libs: [
- "net-utils-device-common-struct",
+ "net-utils-device-common-bpf",
],
libs: [
"androidx.annotation_annotation",
@@ -130,7 +132,7 @@
// to generate the SDK stubs.
// Even if the library is included in "impl_only_static_libs" of defaults. This is still
// needed because java_library which doesn't understand "impl_only_static_libs".
- "net-utils-device-common-struct",
+ "net-utils-device-common-bpf",
],
libs: [
// This cannot be in the defaults clause above because if it were, it would be used
@@ -266,6 +268,7 @@
":framework-connectivity-t-pre-jarjar{.jar}",
":framework-connectivity.stubs.module_lib{.jar}",
":framework-connectivity-t.stubs.module_lib{.jar}",
+ ":framework-connectivity-module-api-stubs-including-flagged{.jar}",
"jarjar-excludes.txt",
],
tools: [
@@ -278,6 +281,7 @@
"--prefix android.net.connectivity " +
"--apistubs $(location :framework-connectivity.stubs.module_lib{.jar}) " +
"--apistubs $(location :framework-connectivity-t.stubs.module_lib{.jar}) " +
+ "--apistubs $(location :framework-connectivity-module-api-stubs-including-flagged{.jar}) " +
// Make a ":"-separated list. There will be an extra ":" but empty items are ignored.
"--unsupportedapi $$(printf ':%s' $(locations :connectivity-hiddenapi-files)) " +
"--excludes $(location jarjar-excludes.txt) " +
@@ -289,21 +293,51 @@
],
}
+droidstubs {
+ name: "framework-connectivity-module-api-stubs-including-flagged-droidstubs",
+ srcs: [
+ ":framework-connectivity-sources",
+ ":framework-connectivity-tiramisu-updatable-sources",
+ ":framework-nearby-java-sources",
+ ":framework-thread-sources",
+ ],
+ flags: [
+ "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
+ "\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\)",
+ "--show-for-stub-purposes-annotation android.annotation.SystemApi" +
+ "\\(client=android.annotation.SystemApi.Client.MODULE_LIBRARIES\\)",
+ ],
+ aidl: {
+ include_dirs: [
+ "packages/modules/Connectivity/framework/aidl-export",
+ "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+ ],
+ },
+}
+
+java_library {
+ name: "framework-connectivity-module-api-stubs-including-flagged",
+ srcs: [":framework-connectivity-module-api-stubs-including-flagged-droidstubs"],
+}
+
// Library providing limited APIs within the connectivity module, so that R+ components like
// Tethering have a controlled way to depend on newer components like framework-connectivity that
// are not loaded on R.
+// Note that this target needs to have access to hidden classes, and as such needs to list
+// the full libraries instead of the .impl lib (which only expose API classes).
java_library {
name: "connectivity-internal-api-util",
sdk_version: "module_current",
libs: [
"androidx.annotation_annotation",
- "framework-connectivity.impl",
+ "framework-connectivity-pre-jarjar",
],
jarjar_rules: ":framework-connectivity-jarjar-rules",
srcs: [
- // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.TIRAMISU),
- // so that API checks are enforced for R+ users of this library
- "src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java",
+ // Files listed here MUST all be annotated with @RequiresApi(Build.VERSION_CODES.S)
+ // or above as appropriate so that API checks are enforced for R+ users of this library
+ "src/android/net/RoutingCoordinatorManager.java",
+ "src/android/net/connectivity/ConnectivityInternalApiUtil.java",
],
visibility: [
"//packages/modules/Connectivity/Tethering:__subpackages__",
diff --git a/framework/aidl-export/android/net/LocalNetworkConfig.aidl b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
new file mode 100644
index 0000000..e2829a5
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkConfig.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+@JavaOnlyStableParcelable parcelable LocalNetworkConfig;
diff --git a/framework/aidl-export/android/net/LocalNetworkInfo.aidl b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
new file mode 100644
index 0000000..fa0bc41
--- /dev/null
+++ b/framework/aidl-export/android/net/LocalNetworkInfo.aidl
@@ -0,0 +1,20 @@
+/**
+ *
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable LocalNetworkInfo;
diff --git a/framework/api/lint-baseline.txt b/framework/api/lint-baseline.txt
index 2f4004a..4465bcb 100644
--- a/framework/api/lint-baseline.txt
+++ b/framework/api/lint-baseline.txt
@@ -1,4 +1,19 @@
// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+ Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+ Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+
+
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
index 193bd92..026d8a9 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -14,6 +14,7 @@
method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties getRedactedLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities getRedactedNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
+ method @FlaggedApi("com.android.net.flags.support_is_uid_networking_blocked") @RequiresPermission(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK) public boolean isUidNetworkingBlocked(int, boolean);
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, android.Manifest.permission.NETWORK_SETUP_WIZARD, android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS}) 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 removeUidFromMeteredNetworkAllowList(int);
@@ -24,6 +25,7 @@
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 @FlaggedApi("com.android.net.flags.set_data_saver_via_cm") @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setDataSaverEnabled(boolean);
method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void setFirewallChainEnabled(int, boolean);
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);
@@ -43,6 +45,7 @@
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 @FlaggedApi("com.android.net.flags.basic_background_restrictions_enabled") public static final int BLOCKED_REASON_APP_BACKGROUND = 64; // 0x40
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
@@ -50,6 +53,7 @@
field public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 32; // 0x20
field public static final int BLOCKED_REASON_NONE = 0; // 0x0
field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
+ field @FlaggedApi("com.android.net.flags.basic_background_restrictions_enabled") public static final int FIREWALL_CHAIN_BACKGROUND = 6; // 0x6
field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
@@ -231,6 +235,7 @@
public final class VpnTransportInfo implements android.os.Parcelable android.net.TransportInfo {
ctor @Deprecated public VpnTransportInfo(int, @Nullable String);
+ method public long getApplicableRedactions();
method @Nullable public String getSessionId();
method @NonNull public android.net.VpnTransportInfo makeCopy(long);
}
diff --git a/framework/api/module-lib-lint-baseline.txt b/framework/api/module-lib-lint-baseline.txt
new file mode 100644
index 0000000..53a8c5e
--- /dev/null
+++ b/framework/api/module-lib-lint-baseline.txt
@@ -0,0 +1,33 @@
+// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+ Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#isTetheringSupported():
+ Method 'isTetheringSupported' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestRouteToHostAddress(int, java.net.InetAddress):
+ Method 'requestRouteToHostAddress' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalApiUrl():
+ Method 'getCaptivePortalApiUrl' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalData():
+ Method 'getCaptivePortalData' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+ Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getUnderlyingNetworks():
+ Method 'getUnderlyingNetworks' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setAllowedUids(java.util.Set<java.lang.Integer>):
+ Method 'setAllowedUids' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setUnderlyingNetworks(java.util.List<android.net.Network>):
+ Method 'setUnderlyingNetworks' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkRequest.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 4a2ed8a..e812024 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -94,6 +94,7 @@
}
public final class DscpPolicy implements android.os.Parcelable {
+ method public int describeContents();
method @Nullable public java.net.InetAddress getDestinationAddress();
method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
method public int getDscpValue();
@@ -101,6 +102,7 @@
method public int getProtocol();
method @Nullable public java.net.InetAddress getSourceAddress();
method public int getSourcePort();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
field public static final int PROTOCOL_ANY = -1; // 0xffffffff
field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
diff --git a/framework/api/system-lint-baseline.txt b/framework/api/system-lint-baseline.txt
index 9a97707..3ac97c0 100644
--- a/framework/api/system-lint-baseline.txt
+++ b/framework/api/system-lint-baseline.txt
@@ -1 +1,29 @@
// Baseline format: 1.0
+BroadcastBehavior: android.net.ConnectivityManager#ACTION_BACKGROUND_DATA_SETTING_CHANGED:
+ Field 'ACTION_BACKGROUND_DATA_SETTING_CHANGED' is missing @BroadcastBehavior
+
+
+RequiresPermission: android.net.ConnectivityManager#isTetheringSupported():
+ Method 'isTetheringSupported' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.app.PendingIntent):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest, android.net.ConnectivityManager.NetworkCallback):
+ Method 'requestNetwork' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalApiUrl():
+ Method 'getCaptivePortalApiUrl' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.LinkProperties#getCaptivePortalData():
+ Method 'getCaptivePortalData' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getOwnerUid():
+ Method 'getOwnerUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities#getUnderlyingNetworks():
+ Method 'getUnderlyingNetworks' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkCapabilities.Builder#setUnderlyingNetworks(java.util.List<android.net.Network>):
+ Method 'setUnderlyingNetworks' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.NetworkRequest.Builder#setSignalStrength(int):
+ Method 'setSignalStrength' documentation mentions permissions already declared by @RequiresPermission
+RequiresPermission: android.net.http.BidirectionalStream.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
+RequiresPermission: android.net.http.UrlRequest.Builder#setTrafficStatsUid(int):
+ Method 'setTrafficStatsUid' documentation mentions permissions without declaring @RequiresPermission
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
index 1ac5e8e..09abd17 100644
--- a/framework/jarjar-excludes.txt
+++ b/framework/jarjar-excludes.txt
@@ -14,6 +14,16 @@
# TODO: move files to android.net.connectivity.visiblefortesting
android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+# Classes used by tethering as a hidden API are compiled as a lib in target
+# connectivity-internal-api-util. Because it's used by tethering, it can't
+# be jarjared. Classes in android.net.connectivity are exempt from being
+# listed here because they are already in the target package and as such
+# are already not jarjared.
+# Because Tethering can be installed on R without Connectivity, any use
+# of these classes must be protected by a check for >= S SDK.
+# It's unlikely anybody else declares a hidden class with this name ?
+android\.net\.RoutingCoordinatorManager(\$.+)?
+android\.net\.LocalNetworkInfo(\$.+)?
# KeepaliveUtils is used by ConnectivityManager CTS
# TODO: move into service-connectivity so framework-connectivity stops using
@@ -28,9 +38,3 @@
# This is required since android.net.http contains api classes and hidden classes.
# TODO: Remove this after hidden classes are moved to different package
android\.net\.http\..+
-
-# TODO: OffloadServiceInfo is being added as an API, but wasn't an API yet in the first module
-# versions targeting U. Do not jarjar it such versions so that tests do not have to cover both
-# cases. This will be removed in an upcoming change marking it as API.
-android\.net\.nsd\.OffloadServiceInfo(\$.+)?
-android\.net\.nsd\.OffloadEngine(\$.+)?
diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml
new file mode 100644
index 0000000..f68aad7
--- /dev/null
+++ b/framework/lint-baseline.xml
@@ -0,0 +1,367 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.IpSecManager.UdpEncapsulationSocket#getResourceId`"
+ errorLine1=" return new NattSocketKeepalive(mService, network, dup, socket.getResourceId(), source,"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="2456"
+ column="71"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.Proxy#setHttpProxyConfiguration`"
+ errorLine1=" Proxy.setHttpProxyConfiguration(getInstance().getDefaultProxy());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5323"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java"
+ line="1072"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int end = nextUser.getUid(0 /* appId */) - 1;"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="50"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int start = user.getUid(0 /* appId */);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/UidRange.java"
+ line="49"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.provider.Settings#checkAndNoteWriteSettingsOperation`"
+ errorLine1=" return Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPackage,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="2799"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#clearDnsCache`"
+ errorLine1=" InetAddress.clearDnsCache();"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5329"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getAllByNameOnNet`"
+ errorLine1=" return InetAddress.getAllByNameOnNet(host, getNetIdForResolv());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="145"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#getByNameOnNet`"
+ errorLine1=" return InetAddress.getByNameOnNet(host, getNetIdForResolv());"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="158"
+ column="28"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(is);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="168"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="216"
+ column="45"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="241"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="254"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" if (failed) IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="272"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bis);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="391"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(bos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="406"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="181"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(socket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/util/DnsUtils.java"
+ line="373"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(zos);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/core/java/com/android/internal/util/FileRotator.java"
+ line="175"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#isNumericAddress`"
+ errorLine1=" return InetAddressUtils.isNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="46"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.InetAddressUtils#parseNumericAddress`"
+ errorLine1=" return InetAddressUtils.parseNumericAddress(address);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/InetAddresses.java"
+ line="63"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#dispatchNetworkConfigurationChange`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5332"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.event.NetworkEventDispatcher#getInstance`"
+ errorLine1=" NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange();"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java"
+ line="5332"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#createInstance`"
+ errorLine1=" HttpURLConnectionFactory urlConnectionFactory = HttpURLConnectionFactory.createInstance();"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="302"
+ column="82"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#openConnection`"
+ errorLine1=" return urlConnectionFactory.openConnection(url, socketFactory, proxy);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="372"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setDns`"
+ errorLine1=" urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="303"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.net.http.HttpURLConnectionFactory#setNewConnectionPool`"
+ errorLine1=" urlConnectionFactory.setNewConnectionPool(httpMaxConnections,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/Network.java"
+ line="305"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="525"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" return setNetworkSpecifier(new EthernetNetworkSpecifier(networkSpecifier));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkRequest.java"
+ line="525"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+ errorLine1=" (EpsBearerQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1421"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.EpsBearerQosSessionAttributes`"
+ errorLine1=" if (attributes instanceof EpsBearerQosSessionAttributes) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1418"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+ errorLine1=" (NrQosSessionAttributes)attributes));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1425"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.telephony.data.NrQosSessionAttributes`"
+ errorLine1=" } else if (attributes instanceof NrQosSessionAttributes) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/framework/src/android/net/NetworkAgent.java"
+ line="1422"
+ column="42"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/framework/src/android/net/BpfNetMapsConstants.java b/framework/src/android/net/BpfNetMapsConstants.java
index 2191682..5d0fe73 100644
--- a/framework/src/android/net/BpfNetMapsConstants.java
+++ b/framework/src/android/net/BpfNetMapsConstants.java
@@ -16,6 +16,16 @@
package android.net;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
+
import android.util.Pair;
import com.android.net.module.util.Struct;
@@ -43,8 +53,16 @@
"/sys/fs/bpf/netd_shared/map_netd_uid_permission_map";
public static final String COOKIE_TAG_MAP_PATH =
"/sys/fs/bpf/netd_shared/map_netd_cookie_tag_map";
+ public static final String DATA_SAVER_ENABLED_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_data_saver_enabled_map";
+ public static final String INGRESS_DISCARD_MAP_PATH =
+ "/sys/fs/bpf/netd_shared/map_netd_ingress_discard_map";
public static final Struct.S32 UID_RULES_CONFIGURATION_KEY = new Struct.S32(0);
public static final Struct.S32 CURRENT_STATS_MAP_CONFIGURATION_KEY = new Struct.S32(1);
+ public static final Struct.S32 DATA_SAVER_ENABLED_KEY = new Struct.S32(0);
+
+ public static final short DATA_SAVER_DISABLED = 0;
+ public static final short DATA_SAVER_ENABLED = 1;
// LINT.IfChange(match_type)
public static final long NO_MATCH = 0;
@@ -60,7 +78,7 @@
public static final long OEM_DENY_1_MATCH = (1 << 9);
public static final long OEM_DENY_2_MATCH = (1 << 10);
public static final long OEM_DENY_3_MATCH = (1 << 11);
- // LINT.ThenChange(packages/modules/Connectivity/bpf_progs/netd.h)
+ public static final long BACKGROUND_MATCH = (1 << 12);
public static final List<Pair<Long, String>> MATCH_LIST = Arrays.asList(
Pair.create(HAPPY_BOX_MATCH, "HAPPY_BOX_MATCH"),
@@ -74,6 +92,33 @@
Pair.create(LOCKDOWN_VPN_MATCH, "LOCKDOWN_VPN_MATCH"),
Pair.create(OEM_DENY_1_MATCH, "OEM_DENY_1_MATCH"),
Pair.create(OEM_DENY_2_MATCH, "OEM_DENY_2_MATCH"),
- Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH")
+ Pair.create(OEM_DENY_3_MATCH, "OEM_DENY_3_MATCH"),
+ Pair.create(BACKGROUND_MATCH, "BACKGROUND_MATCH")
);
+
+ /**
+ * List of all firewall allow chains.
+ *
+ * Allow chains mean the firewall denies all uids by default, uids must be explicitly allowed.
+ */
+ public static final List<Integer> ALLOW_CHAINS = List.of(
+ FIREWALL_CHAIN_DOZABLE,
+ FIREWALL_CHAIN_POWERSAVE,
+ FIREWALL_CHAIN_RESTRICTED,
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_BACKGROUND
+ );
+
+ /**
+ * List of all firewall deny chains.
+ *
+ * Deny chains mean the firewall allows all uids by default, uids must be explicitly denied.
+ */
+ public static final List<Integer> DENY_CHAINS = List.of(
+ FIREWALL_CHAIN_STANDBY,
+ FIREWALL_CHAIN_OEM_DENY_1,
+ FIREWALL_CHAIN_OEM_DENY_2,
+ FIREWALL_CHAIN_OEM_DENY_3
+ );
+ // LINT.ThenChange(../../../../bpf_progs/netd.h)
}
diff --git a/framework/src/android/net/BpfNetMapsReader.java b/framework/src/android/net/BpfNetMapsReader.java
new file mode 100644
index 0000000..4ab6d3e
--- /dev/null
+++ b/framework/src/android/net/BpfNetMapsReader.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
+import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
+import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
+import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
+import static android.net.BpfNetMapsUtils.isFirewallAllowList;
+import static android.net.BpfNetMapsUtils.throwIfPreT;
+import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.BpfMap;
+import com.android.net.module.util.IBpfMap;
+import com.android.net.module.util.Struct.S32;
+import com.android.net.module.util.Struct.U32;
+import com.android.net.module.util.Struct.U8;
+
+/**
+ * A helper class to *read* java BpfMaps.
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+public class BpfNetMapsReader {
+ private static final String TAG = BpfNetMapsReader.class.getSimpleName();
+
+ // Locally store the handle of bpf maps. The FileDescriptors are statically cached inside the
+ // BpfMap implementation.
+
+ // Bpf map to store various networking configurations, the format of the value is different
+ // for different keys. See BpfNetMapsConstants#*_CONFIGURATION_KEY for keys.
+ private final IBpfMap<S32, U32> mConfigurationMap;
+ // Bpf map to store per uid traffic control configurations.
+ // See {@link UidOwnerValue} for more detail.
+ private final IBpfMap<S32, UidOwnerValue> mUidOwnerMap;
+ private final IBpfMap<S32, U8> mDataSaverEnabledMap;
+ private final Dependencies mDeps;
+
+ // Bitmaps for calculating whether a given uid is blocked by firewall chains.
+ private static final long sMaskDropIfSet;
+ private static final long sMaskDropIfUnset;
+
+ static {
+ long maskDropIfSet = 0L;
+ long maskDropIfUnset = 0L;
+
+ for (int chain : BpfNetMapsConstants.ALLOW_CHAINS) {
+ final long match = getMatchByFirewallChain(chain);
+ maskDropIfUnset |= match;
+ }
+ for (int chain : BpfNetMapsConstants.DENY_CHAINS) {
+ final long match = getMatchByFirewallChain(chain);
+ maskDropIfSet |= match;
+ }
+ sMaskDropIfSet = maskDropIfSet;
+ sMaskDropIfUnset = maskDropIfUnset;
+ }
+
+ private static class SingletonHolder {
+ static final BpfNetMapsReader sInstance = new BpfNetMapsReader();
+ }
+
+ @NonNull
+ public static BpfNetMapsReader getInstance() {
+ return SingletonHolder.sInstance;
+ }
+
+ private BpfNetMapsReader() {
+ this(new Dependencies());
+ }
+
+ // While the production code uses the singleton to optimize for performance and deal with
+ // concurrent access, the test needs to use a non-static approach for dependency injection and
+ // mocking virtual bpf maps.
+ @VisibleForTesting
+ public BpfNetMapsReader(@NonNull Dependencies deps) {
+ if (!SdkLevel.isAtLeastT()) {
+ throw new UnsupportedOperationException(
+ BpfNetMapsReader.class.getSimpleName() + " is not supported below Android T");
+ }
+ mDeps = deps;
+ mConfigurationMap = mDeps.getConfigurationMap();
+ mUidOwnerMap = mDeps.getUidOwnerMap();
+ mDataSaverEnabledMap = mDeps.getDataSaverEnabledMap();
+ }
+
+ /**
+ * Dependencies of BpfNetMapReader, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /** Get the configuration map. */
+ public IBpfMap<S32, U32> getConfigurationMap() {
+ try {
+ return new BpfMap<>(CONFIGURATION_MAP_PATH, BpfMap.BPF_F_RDONLY,
+ S32.class, U32.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open configuration map", e);
+ }
+ }
+
+ /** Get the uid owner map. */
+ public IBpfMap<S32, UidOwnerValue> getUidOwnerMap() {
+ try {
+ return new BpfMap<>(UID_OWNER_MAP_PATH, BpfMap.BPF_F_RDONLY,
+ S32.class, UidOwnerValue.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open uid owner map", e);
+ }
+ }
+
+ /** Get the data saver enabled map. */
+ public IBpfMap<S32, U8> getDataSaverEnabledMap() {
+ try {
+ return new BpfMap<>(DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDONLY, S32.class,
+ U8.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open data saver enabled map", e);
+ }
+ }
+ }
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param chain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean isChainEnabled(final int chain) {
+ return isChainEnabled(mConfigurationMap, chain);
+ }
+
+ /**
+ * Get firewall rule of specified firewall chain on specified uid.
+ *
+ * @param chain target chain
+ * @param uid target uid
+ * @return either {@link ConnectivityManager#FIREWALL_RULE_ALLOW} or
+ * {@link ConnectivityManager#FIREWALL_RULE_DENY}.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public int getUidRule(final int chain, final int uid) {
+ return getUidRule(mUidOwnerMap, chain, uid);
+ }
+
+ /**
+ * Get the specified firewall chain's status.
+ *
+ * @param configurationMap target configurationMap
+ * @param chain target chain
+ * @return {@code true} if chain is enabled, {@code false} if chain is not enabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static boolean isChainEnabled(
+ final IBpfMap<S32, U32> configurationMap, final int chain) {
+ throwIfPreT("isChainEnabled is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(chain);
+ try {
+ final U32 config = configurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ return (config.val & match) != 0;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Get firewall rule of specified firewall chain on specified uid.
+ *
+ * @param uidOwnerMap target uidOwnerMap.
+ * @param chain target chain.
+ * @param uid target uid.
+ * @return either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public static int getUidRule(final IBpfMap<S32, UidOwnerValue> uidOwnerMap,
+ final int chain, final int uid) {
+ throwIfPreT("getUidRule is not available on pre-T devices");
+
+ final long match = getMatchByFirewallChain(chain);
+ final boolean isAllowList = isFirewallAllowList(chain);
+ try {
+ final UidOwnerValue uidMatch = uidOwnerMap.getValue(new S32(uid));
+ final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
+ return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get uid rule status: " + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Return whether the network is blocked by firewall chains for the given uid.
+ *
+ * @param uid The target uid.
+ * @param isNetworkMetered Whether the target network is metered.
+ * @param isDataSaverEnabled Whether the data saver is enabled.
+ *
+ * @return True if the network is blocked. Otherwise, false.
+ * @throws ServiceSpecificException if the read fails.
+ *
+ * @hide
+ */
+ public boolean isUidNetworkingBlocked(final int uid, boolean isNetworkMetered,
+ boolean isDataSaverEnabled) {
+ throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
+
+ final long uidRuleConfig;
+ final long uidMatch;
+ try {
+ uidRuleConfig = mConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).val;
+ final UidOwnerValue value = mUidOwnerMap.getValue(new S32(uid));
+ uidMatch = (value != null) ? value.rule : 0L;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to get firewall chain status: " + Os.strerror(e.errno));
+ }
+
+ final boolean blockedByAllowChains = 0 != (uidRuleConfig & ~uidMatch & sMaskDropIfUnset);
+ final boolean blockedByDenyChains = 0 != (uidRuleConfig & uidMatch & sMaskDropIfSet);
+ if (blockedByAllowChains || blockedByDenyChains) {
+ return true;
+ }
+
+ if (!isNetworkMetered) return false;
+ if ((uidMatch & PENALTY_BOX_MATCH) != 0) return true;
+ if ((uidMatch & HAPPY_BOX_MATCH) != 0) return false;
+ return isDataSaverEnabled;
+ }
+
+ /**
+ * Get Data Saver enabled or disabled
+ *
+ * @return whether Data Saver is enabled or disabled.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public boolean getDataSaverEnabled() {
+ throwIfPreT("getDataSaverEnabled is not available on pre-T devices");
+
+ // Note that this is not expected to be called until V given that it relies on the
+ // counterpart platform solution to set data saver status to bpf.
+ // See {@code NetworkManagementService#setDataSaverModeEnabled}.
+ if (!SdkLevel.isAtLeastV()) {
+ Log.wtf(TAG, "getDataSaverEnabled is not expected to be called on pre-V devices");
+ }
+
+ try {
+ return mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val == DATA_SAVER_ENABLED;
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, "Unable to get data saver: "
+ + Os.strerror(e.errno));
+ }
+ }
+}
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index d464e3d..11d610c 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -16,6 +16,9 @@
package android.net;
+import static android.net.BpfNetMapsConstants.ALLOW_CHAINS;
+import static android.net.BpfNetMapsConstants.BACKGROUND_MATCH;
+import static android.net.BpfNetMapsConstants.DENY_CHAINS;
import static android.net.BpfNetMapsConstants.DOZABLE_MATCH;
import static android.net.BpfNetMapsConstants.LOW_POWER_STANDBY_MATCH;
import static android.net.BpfNetMapsConstants.MATCH_LIST;
@@ -26,6 +29,7 @@
import static android.net.BpfNetMapsConstants.POWERSAVE_MATCH;
import static android.net.BpfNetMapsConstants.RESTRICTED_MATCH;
import static android.net.BpfNetMapsConstants.STANDBY_MATCH;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
@@ -39,6 +43,8 @@
import android.os.ServiceSpecificException;
import android.util.Pair;
+import com.android.modules.utils.build.SdkLevel;
+
import java.util.StringJoiner;
/**
@@ -66,6 +72,8 @@
return POWERSAVE_MATCH;
case FIREWALL_CHAIN_RESTRICTED:
return RESTRICTED_MATCH;
+ case FIREWALL_CHAIN_BACKGROUND:
+ return BACKGROUND_MATCH;
case FIREWALL_CHAIN_LOW_POWER_STANDBY:
return LOW_POWER_STANDBY_MATCH;
case FIREWALL_CHAIN_OEM_DENY_1:
@@ -80,26 +88,18 @@
}
/**
- * Get if the chain is allow list or not.
+ * Get whether the chain is an allow-list or a deny-list.
*
* ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
- * DENYLIST means the firewall allows all by default, uids must be explicitly denyed
+ * DENYLIST means the firewall allows all by default, uids must be explicitly denied
*/
public static boolean isFirewallAllowList(final int chain) {
- switch (chain) {
- case FIREWALL_CHAIN_DOZABLE:
- case FIREWALL_CHAIN_POWERSAVE:
- case FIREWALL_CHAIN_RESTRICTED:
- case FIREWALL_CHAIN_LOW_POWER_STANDBY:
- return true;
- case FIREWALL_CHAIN_STANDBY:
- case FIREWALL_CHAIN_OEM_DENY_1:
- case FIREWALL_CHAIN_OEM_DENY_2:
- case FIREWALL_CHAIN_OEM_DENY_3:
- return false;
- default:
- throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
+ if (ALLOW_CHAINS.contains(chain)) {
+ return true;
+ } else if (DENY_CHAINS.contains(chain)) {
+ return false;
}
+ throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
}
/**
@@ -124,4 +124,15 @@
}
return sj.toString();
}
+
+ public static final boolean PRE_T = !SdkLevel.isAtLeastT();
+
+ /**
+ * Throw UnsupportedOperationException if SdkLevel is before T.
+ */
+ public static void throwIfPreT(final String msg) {
+ if (PRE_T) {
+ throw new UnsupportedOperationException(msg);
+ }
+ }
}
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 2315521..a934ddb 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,6 +16,8 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
+import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
import static android.net.NetworkRequest.Type.LISTEN;
@@ -25,22 +27,30 @@
import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
import static android.net.QosCallback.QosCallbackRegistrationException;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresApi;
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.annotation.TargetApi;
+import android.app.Application;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.compat.annotation.UnsupportedAppUsage;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.SocketKeepalive.Callback;
@@ -72,6 +82,8 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -93,6 +105,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class that answers queries about the state of network connectivity. It also
@@ -115,6 +128,18 @@
private static final String TAG = "ConnectivityManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String SET_DATA_SAVER_VIA_CM =
+ "com.android.net.flags.set_data_saver_via_cm";
+ static final String SUPPORT_IS_UID_NETWORKING_BLOCKED =
+ "com.android.net.flags.support_is_uid_networking_blocked";
+ static final String BASIC_BACKGROUND_RESTRICTIONS_ENABLED =
+ "com.android.net.flags.basic_background_restrictions_enabled";
+ }
+
/**
* A change in network connectivity has occurred. A default connection has either
* been established or lost. The NetworkInfo for the affected network is
@@ -886,6 +911,16 @@
public static final int BLOCKED_REASON_LOW_POWER_STANDBY = 1 << 5;
/**
+ * Flag to indicate that an app is subject to default background restrictions that would
+ * result in its network access being blocked.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ public static final int BLOCKED_REASON_APP_BACKGROUND = 1 << 6;
+
+ /**
* Flag to indicate that an app is subject to Data saver restrictions that would
* result in its metered network access being blocked.
*
@@ -924,6 +959,7 @@
BLOCKED_REASON_RESTRICTED_MODE,
BLOCKED_REASON_LOCKDOWN_VPN,
BLOCKED_REASON_LOW_POWER_STANDBY,
+ BLOCKED_REASON_APP_BACKGROUND,
BLOCKED_METERED_REASON_DATA_SAVER,
BLOCKED_METERED_REASON_USER_RESTRICTED,
BLOCKED_METERED_REASON_ADMIN_DISABLED,
@@ -941,7 +977,6 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
private final IConnectivityManager mService;
- // LINT.IfChange(firewall_chain)
/**
* Firewall chain for device idle (doze mode).
* Allowlist of apps that have network access in device idle.
@@ -983,6 +1018,16 @@
public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
/**
+ * Firewall chain used for always-on default background restrictions.
+ * Allowlist of apps that have access because either they are in the foreground or they are
+ * exempted for specific situations while in the background.
+ * @hide
+ */
+ @FlaggedApi(Flags.BASIC_BACKGROUND_RESTRICTIONS_ENABLED)
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final int FIREWALL_CHAIN_BACKGROUND = 6;
+
+ /**
* Firewall chain used for OEM-specific application restrictions.
*
* Denylist of apps that will not have network access due to OEM-specific restrictions. If an
@@ -1041,12 +1086,12 @@
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_BACKGROUND,
FIREWALL_CHAIN_OEM_DENY_1,
FIREWALL_CHAIN_OEM_DENY_2,
FIREWALL_CHAIN_OEM_DENY_3
})
public @interface FirewallChain {}
- // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
/**
* A firewall rule which allows or drops packets depending on existing policy.
@@ -3811,11 +3856,28 @@
@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) {
+ public Network registerNetworkAgent(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+ @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, int providerId) {
+ return registerNetworkAgent(na, ni, lp, nc, null /* localNetworkConfig */, score, config,
+ providerId);
+ }
+
+ /**
+ * @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(@NonNull INetworkAgent na, @NonNull NetworkInfo ni,
+ @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+ @NonNull NetworkAgentConfig config, int providerId) {
try {
- return mService.registerNetworkAgent(na, ni, lp, nc, score, config, providerId);
+ return mService.registerNetworkAgent(na, ni, lp, nc, score, localNetworkConfig, config,
+ providerId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -3924,16 +3986,21 @@
* @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 localInfo The {@link LocalNetworkInfo} of the satisfying network, or null
+ * if this network is not a local 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) {
+ @NonNull LinkProperties linkProperties,
+ @Nullable LocalNetworkInfo localInfo,
+ @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);
+ if (null != localInfo) onLocalNetworkInfoChanged(network, localInfo);
onBlockedStatusChanged(network, blocked);
}
@@ -3950,7 +4017,8 @@
*/
public void onAvailable(@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities,
- @NonNull LinkProperties linkProperties, boolean blocked) {
+ @NonNull LinkProperties linkProperties,
+ boolean blocked) {
onAvailable(network);
if (!networkCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) {
@@ -4077,6 +4145,19 @@
@NonNull LinkProperties linkProperties) {}
/**
+ * Called when there is a change in the {@link LocalNetworkInfo} for this network.
+ *
+ * This is only called for local networks, that is those with the
+ * NET_CAPABILITY_LOCAL_NETWORK network capability.
+ *
+ * @param network the {@link Network} whose local network info has changed.
+ * @param localNetworkInfo the new {@link LocalNetworkInfo} for this network.
+ * @hide
+ */
+ public void onLocalNetworkInfoChanged(@NonNull Network network,
+ @NonNull LocalNetworkInfo localNetworkInfo) {}
+
+ /**
* Called when the network the framework connected to for this request suspends data
* transmission temporarily.
*
@@ -4170,27 +4251,29 @@
}
/** @hide */
- public static final int CALLBACK_PRECHECK = 1;
+ public static final int CALLBACK_PRECHECK = 1;
/** @hide */
- public static final int CALLBACK_AVAILABLE = 2;
+ public static final int CALLBACK_AVAILABLE = 2;
/** @hide arg1 = TTL */
- public static final int CALLBACK_LOSING = 3;
+ public static final int CALLBACK_LOSING = 3;
/** @hide */
- public static final int CALLBACK_LOST = 4;
+ public static final int CALLBACK_LOST = 4;
/** @hide */
- public static final int CALLBACK_UNAVAIL = 5;
+ public static final int CALLBACK_UNAVAIL = 5;
/** @hide */
- public static final int CALLBACK_CAP_CHANGED = 6;
+ public static final int CALLBACK_CAP_CHANGED = 6;
/** @hide */
- public static final int CALLBACK_IP_CHANGED = 7;
+ public static final int CALLBACK_IP_CHANGED = 7;
/** @hide obj = NetworkCapabilities, arg1 = seq number */
- private static final int EXPIRE_LEGACY_REQUEST = 8;
+ private static final int EXPIRE_LEGACY_REQUEST = 8;
/** @hide */
- public static final int CALLBACK_SUSPENDED = 9;
+ public static final int CALLBACK_SUSPENDED = 9;
/** @hide */
- public static final int CALLBACK_RESUMED = 10;
+ public static final int CALLBACK_RESUMED = 10;
/** @hide */
- public static final int CALLBACK_BLK_CHANGED = 11;
+ public static final int CALLBACK_BLK_CHANGED = 11;
+ /** @hide */
+ public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
/** @hide */
public static String getCallbackName(int whichCallback) {
@@ -4206,6 +4289,7 @@
case CALLBACK_SUSPENDED: return "CALLBACK_SUSPENDED";
case CALLBACK_RESUMED: return "CALLBACK_RESUMED";
case CALLBACK_BLK_CHANGED: return "CALLBACK_BLK_CHANGED";
+ case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: return "CALLBACK_LOCAL_NETWORK_INFO_CHANGED";
default:
return Integer.toString(whichCallback);
}
@@ -4260,7 +4344,8 @@
case CALLBACK_AVAILABLE: {
NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
LinkProperties lp = getObject(message, LinkProperties.class);
- callback.onAvailable(network, cap, lp, message.arg1);
+ LocalNetworkInfo lni = getObject(message, LocalNetworkInfo.class);
+ callback.onAvailable(network, cap, lp, lni, message.arg1);
break;
}
case CALLBACK_LOSING: {
@@ -4285,6 +4370,11 @@
callback.onLinkPropertiesChanged(network, lp);
break;
}
+ case CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+ final LocalNetworkInfo info = getObject(message, LocalNetworkInfo.class);
+ callback.onLocalNetworkInfoChanged(network, info);
+ break;
+ }
case CALLBACK_SUSPENDED: {
callback.onNetworkSuspended(network);
break;
@@ -5941,6 +6031,28 @@
}
/**
+ * Sets data saver switch.
+ *
+ * @param enable True if enable.
+ * @throws IllegalStateException if failed.
+ * @hide
+ */
+ @FlaggedApi(Flags.SET_DATA_SAVER_VIA_CM)
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK
+ })
+ public void setDataSaverEnabled(final boolean enable) {
+ try {
+ mService.setDataSaverEnabled(enable);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Adds the specified UID to the list of UIds that are allowed to use data on metered networks
* even when background data is restricted. The deny list takes precedence over the allow list.
*
@@ -6149,6 +6261,128 @@
}
}
+ /**
+ * Helper class to track data saver status.
+ *
+ * The class will fetch current data saver status from {@link NetworkPolicyManager} when
+ * initialized, and listening for status changed intent to cache the latest status.
+ *
+ * @hide
+ */
+ @TargetApi(Build.VERSION_CODES.TIRAMISU) // RECEIVER_NOT_EXPORTED requires T.
+ @VisibleForTesting(visibility = PRIVATE)
+ public static class DataSaverStatusTracker extends BroadcastReceiver {
+ private static final Object sDataSaverStatusTrackerLock = new Object();
+
+ private static volatile DataSaverStatusTracker sInstance;
+
+ /**
+ * Gets a static instance of the class.
+ *
+ * @param context A {@link Context} for initialization. Note that since the data saver
+ * status is global on a device, passing any context is equivalent.
+ * @return The static instance of a {@link DataSaverStatusTracker}.
+ */
+ public static DataSaverStatusTracker getInstance(@NonNull Context context) {
+ if (sInstance == null) {
+ synchronized (sDataSaverStatusTrackerLock) {
+ if (sInstance == null) {
+ sInstance = new DataSaverStatusTracker(context);
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ private final NetworkPolicyManager mNpm;
+ // The value updates on the caller's binder thread or UI thread.
+ private final AtomicBoolean mIsDataSaverEnabled;
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public DataSaverStatusTracker(final Context context) {
+ // To avoid leaks, take the application context.
+ final Context appContext;
+ if (context instanceof Application) {
+ appContext = context;
+ } else {
+ appContext = context.getApplicationContext();
+ }
+
+ if ((appContext.getApplicationInfo().flags & FLAG_PERSISTENT) == 0
+ && (appContext.getApplicationInfo().flags & FLAG_SYSTEM) == 0) {
+ throw new IllegalStateException("Unexpected caller: "
+ + appContext.getApplicationInfo().packageName);
+ }
+
+ mNpm = appContext.getSystemService(NetworkPolicyManager.class);
+ final IntentFilter filter = new IntentFilter(
+ ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
+ // The receiver should not receive broadcasts from other Apps.
+ appContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
+ mIsDataSaverEnabled = new AtomicBoolean();
+ updateDataSaverEnabled();
+ }
+
+ // Runs on caller's UI thread.
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)) {
+ updateDataSaverEnabled();
+ } else {
+ throw new IllegalStateException("Unexpected intent " + intent);
+ }
+ }
+
+ public boolean getDataSaverEnabled() {
+ return mIsDataSaverEnabled.get();
+ }
+
+ private void updateDataSaverEnabled() {
+ // Uid doesn't really matter, but use a fixed UID to make things clearer.
+ final int dataSaverForCallerUid = mNpm.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+ mIsDataSaverEnabled.set(dataSaverForCallerUid
+ != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+ }
+ }
+
+ /**
+ * Return whether the network is blocked for the given uid and metered condition.
+ *
+ * Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
+ * maps and therefore considerably faster. For use by the NetworkStack process only.
+ *
+ * @param uid The target uid.
+ * @param isNetworkMetered Whether the target network is metered.
+ *
+ * @return True if all networking with the given condition is blocked. Otherwise, false.
+ * @throws IllegalStateException if the map cannot be opened.
+ * @throws ServiceSpecificException if the read fails.
+ * @hide
+ */
+ // This isn't protected by a standard Android permission since it can't
+ // afford to do IPC for performance reasons. Instead, the access control
+ // is provided by linux file group permission AID_NET_BW_ACCT and the
+ // selinux context fs_bpf_net*.
+ // Only the system server process and the network stack have access.
+ @FlaggedApi(Flags.SUPPORT_IS_UID_NETWORKING_BLOCKED)
+ @SystemApi(client = MODULE_LIBRARIES)
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU) // BPF maps were only mainlined in T
+ @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
+ final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
+
+ final boolean isDataSaverEnabled;
+ if (SdkLevel.isAtLeastV()) {
+ isDataSaverEnabled = reader.getDataSaverEnabled();
+ } else {
+ final DataSaverStatusTracker dataSaverStatusTracker =
+ DataSaverStatusTracker.getInstance(mContext);
+ isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
+ }
+
+ return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
+ }
+
/** @hide */
public IBinder getCompanionDeviceManagerProxyService() {
try {
@@ -6157,4 +6391,24 @@
throw e.rethrowFromSystemServer();
}
}
+
+ private static final Object sRoutingCoordinatorManagerLock = new Object();
+ @GuardedBy("sRoutingCoordinatorManagerLock")
+ private static RoutingCoordinatorManager sRoutingCoordinatorManager = null;
+ /** @hide */
+ @RequiresApi(Build.VERSION_CODES.S)
+ public RoutingCoordinatorManager getRoutingCoordinatorManager() {
+ try {
+ synchronized (sRoutingCoordinatorManagerLock) {
+ if (null == sRoutingCoordinatorManager) {
+ sRoutingCoordinatorManager = new RoutingCoordinatorManager(mContext,
+ IRoutingCoordinator.Stub.asInterface(
+ mService.getRoutingCoordinatorService()));
+ }
+ return sRoutingCoordinatorManager;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index ebe8bca..d3a02b9 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -27,6 +27,7 @@
import android.net.IQosCallback;
import android.net.ISocketKeepaliveCallback;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.Network;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
@@ -146,7 +147,8 @@
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 NetworkCapabilities nc, in NetworkScore score,
+ in LocalNetworkConfig localNetworkConfig, in NetworkAgentConfig config,
in int factorySerialNumber);
NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
@@ -238,6 +240,8 @@
void setTestAllowBadWifiUntil(long timeMs);
+ void setDataSaverEnabled(boolean enable);
+
void updateMeteredNetworkAllowList(int uid, boolean add);
void updateMeteredNetworkDenyList(int uid, boolean add);
@@ -257,4 +261,6 @@
void setVpnNetworkPreference(String session, in UidRange[] ranges);
void setTestLowTcpPollingTimerForKeepalive(long timeMs);
+
+ IBinder getRoutingCoordinatorService();
}
diff --git a/framework/src/android/net/INetworkAgentRegistry.aidl b/framework/src/android/net/INetworkAgentRegistry.aidl
index b375b7b..61b27b5 100644
--- a/framework/src/android/net/INetworkAgentRegistry.aidl
+++ b/framework/src/android/net/INetworkAgentRegistry.aidl
@@ -17,6 +17,7 @@
import android.net.DscpPolicy;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
@@ -34,6 +35,7 @@
void sendLinkProperties(in LinkProperties lp);
// TODO: consider replacing this by "markConnected()" and removing
void sendNetworkInfo(in NetworkInfo info);
+ void sendLocalNetworkConfig(in LocalNetworkConfig config);
void sendScore(in NetworkScore score);
void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial);
void sendSocketKeepaliveEvent(int slot, int reason);
diff --git a/framework/src/android/net/IRoutingCoordinator.aidl b/framework/src/android/net/IRoutingCoordinator.aidl
new file mode 100644
index 0000000..cf02ec4
--- /dev/null
+++ b/framework/src/android/net/IRoutingCoordinator.aidl
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.RouteInfo;
+
+/** @hide */
+interface IRoutingCoordinator {
+ /**
+ * Add a route for specific network
+ *
+ * @param netId the network to add the route to
+ * @param route the route to add
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void addRoute(int netId, in RouteInfo route);
+
+ /**
+ * Remove a route for specific network
+ *
+ * @param netId the network to remove the route from
+ * @param route the route to remove
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void removeRoute(int netId, in RouteInfo route);
+
+ /**
+ * Update a route for specific network
+ *
+ * @param netId the network to update the route for
+ * @param route parcelable with route information
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void updateRoute(int netId, in RouteInfo route);
+
+ /**
+ * Adds an interface to a network. The interface must not be assigned to any network, including
+ * the specified network.
+ *
+ * @param netId the network to add the interface to.
+ * @param iface the name of the interface to add.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ void addInterfaceToNetwork(int netId, in String iface);
+
+ /**
+ * Removes an interface from a network. The interface must be assigned to the specified network.
+ *
+ * @param netId the network to remove the interface from.
+ * @param iface the name of the interface to remove.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ void removeInterfaceFromNetwork(int netId, in String iface);
+
+ /**
+ * Add forwarding ip rule
+ *
+ * @param fromIface interface name to add forwarding ip rule
+ * @param toIface interface name to add forwarding ip rule
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void addInterfaceForward(in String fromIface, in String toIface);
+
+ /**
+ * Remove forwarding ip rule
+ *
+ * @param fromIface interface name to remove forwarding ip rule
+ * @param toIface interface name to remove forwarding ip rule
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ void removeInterfaceForward(in String fromIface, in String toIface);
+}
diff --git a/framework/src/android/net/LocalNetworkConfig.java b/framework/src/android/net/LocalNetworkConfig.java
new file mode 100644
index 0000000..17b1064
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkConfig.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class to communicate configuration info about a local network through {@link NetworkAgent}.
+ * @hide
+ */
+// TODO : @SystemApi
+public final class LocalNetworkConfig implements Parcelable {
+ @Nullable
+ private final NetworkRequest mUpstreamSelector;
+
+ @NonNull
+ private final MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+ @NonNull
+ private final MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+ private LocalNetworkConfig(@Nullable final NetworkRequest upstreamSelector,
+ @Nullable final MulticastRoutingConfig upstreamConfig,
+ @Nullable final MulticastRoutingConfig downstreamConfig) {
+ mUpstreamSelector = upstreamSelector;
+ if (null != upstreamConfig) {
+ mUpstreamMulticastRoutingConfig = upstreamConfig;
+ } else {
+ mUpstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+ }
+ if (null != downstreamConfig) {
+ mDownstreamMulticastRoutingConfig = downstreamConfig;
+ } else {
+ mDownstreamMulticastRoutingConfig = MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+ }
+ }
+
+ /**
+ * Get the request choosing which network traffic from this network is forwarded to and from.
+ *
+ * This may be null if the local network doesn't forward the traffic anywhere.
+ */
+ @Nullable
+ public NetworkRequest getUpstreamSelector() {
+ return mUpstreamSelector;
+ }
+
+ /**
+ * Get the upstream multicast routing config
+ */
+ @NonNull
+ public MulticastRoutingConfig getUpstreamMulticastRoutingConfig() {
+ return mUpstreamMulticastRoutingConfig;
+ }
+
+ /**
+ * Get the downstream multicast routing config
+ */
+ @NonNull
+ public MulticastRoutingConfig getDownstreamMulticastRoutingConfig() {
+ return mDownstreamMulticastRoutingConfig;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+ dest.writeParcelable(mUpstreamSelector, flags);
+ dest.writeParcelable(mUpstreamMulticastRoutingConfig, flags);
+ dest.writeParcelable(mDownstreamMulticastRoutingConfig, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "LocalNetworkConfig{"
+ + "UpstreamSelector=" + mUpstreamSelector
+ + ", UpstreamMulticastConfig=" + mUpstreamMulticastRoutingConfig
+ + ", DownstreamMulticastConfig=" + mDownstreamMulticastRoutingConfig
+ + '}';
+ }
+
+ public static final @NonNull Creator<LocalNetworkConfig> CREATOR = new Creator<>() {
+ public LocalNetworkConfig createFromParcel(Parcel in) {
+ final NetworkRequest upstreamSelector = in.readParcelable(null);
+ final MulticastRoutingConfig upstreamConfig = in.readParcelable(null);
+ final MulticastRoutingConfig downstreamConfig = in.readParcelable(null);
+ return new LocalNetworkConfig(
+ upstreamSelector, upstreamConfig, downstreamConfig);
+ }
+
+ @Override
+ public LocalNetworkConfig[] newArray(final int size) {
+ return new LocalNetworkConfig[size];
+ }
+ };
+
+
+ public static final class Builder {
+ @Nullable
+ private NetworkRequest mUpstreamSelector;
+
+ @Nullable
+ private MulticastRoutingConfig mUpstreamMulticastRoutingConfig;
+
+ @Nullable
+ private MulticastRoutingConfig mDownstreamMulticastRoutingConfig;
+
+ /**
+ * Create a Builder
+ */
+ public Builder() {
+ }
+
+ /**
+ * Set to choose where this local network should forward its traffic to.
+ *
+ * The system will automatically choose the best network matching the request as an
+ * upstream, and set up forwarding between this local network and the chosen upstream.
+ * If no network matches the request, there is no upstream and the traffic is not forwarded.
+ * The caller can know when this changes by listening to link properties changes of
+ * this network with the {@link android.net.LinkProperties#getForwardedNetwork()} getter.
+ *
+ * Set this to null if the local network shouldn't be forwarded. Default is null.
+ */
+ @NonNull
+ public Builder setUpstreamSelector(@Nullable NetworkRequest upstreamSelector) {
+ mUpstreamSelector = upstreamSelector;
+ return this;
+ }
+
+ /**
+ * Set the upstream multicast routing config.
+ *
+ * If null, don't route multicast packets upstream. This is equivalent to a
+ * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+ */
+ @NonNull
+ public Builder setUpstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+ mUpstreamMulticastRoutingConfig = cfg;
+ return this;
+ }
+
+ /**
+ * Set the downstream multicast routing config.
+ *
+ * If null, don't route multicast packets downstream. This is equivalent to a
+ * MulticastRoutingConfig in mode FORWARD_NONE. The default is null.
+ */
+ @NonNull
+ public Builder setDownstreamMulticastRoutingConfig(@Nullable MulticastRoutingConfig cfg) {
+ mDownstreamMulticastRoutingConfig = cfg;
+ return this;
+ }
+
+ /**
+ * Build the LocalNetworkConfig object.
+ */
+ @NonNull
+ public LocalNetworkConfig build() {
+ return new LocalNetworkConfig(mUpstreamSelector,
+ mUpstreamMulticastRoutingConfig,
+ mDownstreamMulticastRoutingConfig);
+ }
+ }
+}
diff --git a/framework/src/android/net/LocalNetworkInfo.java b/framework/src/android/net/LocalNetworkInfo.java
new file mode 100644
index 0000000..f945133
--- /dev/null
+++ b/framework/src/android/net/LocalNetworkInfo.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * Information about a local network.
+ *
+ * This is sent to ConnectivityManager.NetworkCallback.
+ * @hide
+ */
+// TODO : make public
+public final class LocalNetworkInfo implements Parcelable {
+ @Nullable private final Network mUpstreamNetwork;
+
+ public LocalNetworkInfo(@Nullable final Network upstreamNetwork) {
+ this.mUpstreamNetwork = upstreamNetwork;
+ }
+
+ /**
+ * Return the upstream network, or null if none.
+ */
+ @Nullable
+ public Network getUpstreamNetwork() {
+ return mUpstreamNetwork;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel dest, final int flags) {
+ dest.writeParcelable(mUpstreamNetwork, flags);
+ }
+
+ @Override
+ public String toString() {
+ return "LocalNetworkInfo { upstream=" + mUpstreamNetwork + " }";
+ }
+
+ public static final @NonNull Creator<LocalNetworkInfo> CREATOR = new Creator<>() {
+ public LocalNetworkInfo createFromParcel(Parcel in) {
+ final Network upstreamNetwork = in.readParcelable(null);
+ return new LocalNetworkInfo(upstreamNetwork);
+ }
+
+ @Override
+ public LocalNetworkInfo[] newArray(final int size) {
+ return new LocalNetworkInfo[size];
+ }
+ };
+
+ /**
+ * Builder for LocalNetworkInfo
+ */
+ public static final class Builder {
+ @Nullable private Network mUpstreamNetwork;
+
+ /**
+ * Set the upstream network, or null if none.
+ * @return the builder
+ */
+ @NonNull public Builder setUpstreamNetwork(@Nullable final Network network) {
+ mUpstreamNetwork = network;
+ return this;
+ }
+
+ /**
+ * Build the LocalNetworkInfo
+ */
+ @NonNull public LocalNetworkInfo build() {
+ return new LocalNetworkInfo(mUpstreamNetwork);
+ }
+ }
+}
diff --git a/framework/src/android/net/MulticastRoutingConfig.java b/framework/src/android/net/MulticastRoutingConfig.java
new file mode 100644
index 0000000..4a3e1be
--- /dev/null
+++ b/framework/src/android/net/MulticastRoutingConfig.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * A class representing a configuration for multicast routing.
+ *
+ * Internal usage to Connectivity
+ * @hide
+ */
+// @SystemApi(client = MODULE_LIBRARIES)
+public final class MulticastRoutingConfig implements Parcelable {
+ private static final String TAG = MulticastRoutingConfig.class.getSimpleName();
+
+ /** Do not forward any multicast packets. */
+ public static final int FORWARD_NONE = 0;
+ /**
+ * Forward only multicast packets with destination in the list of listening addresses.
+ * Ignore the min scope.
+ */
+ public static final int FORWARD_SELECTED = 1;
+ /**
+ * Forward all multicast packets with scope greater or equal than the min scope.
+ * Ignore the list of listening addresses.
+ */
+ public static final int FORWARD_WITH_MIN_SCOPE = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "FORWARD_" }, value = {
+ FORWARD_NONE,
+ FORWARD_SELECTED,
+ FORWARD_WITH_MIN_SCOPE
+ })
+ public @interface MulticastForwardingMode {}
+
+ /**
+ * Not a multicast scope, for configurations that do not use the min scope.
+ */
+ public static final int MULTICAST_SCOPE_NONE = -1;
+
+ /** @hide */
+ public static final MulticastRoutingConfig CONFIG_FORWARD_NONE =
+ new MulticastRoutingConfig(FORWARD_NONE, MULTICAST_SCOPE_NONE, null);
+
+ @MulticastForwardingMode
+ private final int mForwardingMode;
+
+ private final int mMinScope;
+
+ @NonNull
+ private final Set<Inet6Address> mListeningAddresses;
+
+ private MulticastRoutingConfig(@MulticastForwardingMode final int mode, final int scope,
+ @Nullable final Set<Inet6Address> addresses) {
+ mForwardingMode = mode;
+ mMinScope = scope;
+ if (null != addresses) {
+ mListeningAddresses = Collections.unmodifiableSet(new ArraySet<>(addresses));
+ } else {
+ mListeningAddresses = Collections.emptySet();
+ }
+ }
+
+ /**
+ * Returns the forwarding mode.
+ */
+ @MulticastForwardingMode
+ public int getForwardingMode() {
+ return mForwardingMode;
+ }
+
+ /**
+ * Returns the minimal group address scope that is allowed for forwarding.
+ * If the forwarding mode is not FORWARD_WITH_MIN_SCOPE, will be MULTICAST_SCOPE_NONE.
+ */
+ public int getMinimumScope() {
+ return mMinScope;
+ }
+
+ /**
+ * Returns the list of group addresses listened by the outgoing interface.
+ * The list will be empty if the forwarding mode is not FORWARD_SELECTED.
+ */
+ @NonNull
+ public Set<Inet6Address> getListeningAddresses() {
+ return mListeningAddresses;
+ }
+
+ private MulticastRoutingConfig(Parcel in) {
+ mForwardingMode = in.readInt();
+ mMinScope = in.readInt();
+ final int count = in.readInt();
+ final ArraySet<Inet6Address> listeningAddresses = new ArraySet<>(count);
+ final byte[] buffer = new byte[16]; // Size of an Inet6Address
+ for (int i = 0; i < count; ++i) {
+ in.readByteArray(buffer);
+ try {
+ listeningAddresses.add((Inet6Address) Inet6Address.getByAddress(buffer));
+ } catch (UnknownHostException e) {
+ Log.wtf(TAG, "Can't read inet6address : " + Arrays.toString(buffer));
+ }
+ }
+ mListeningAddresses = Collections.unmodifiableSet(listeningAddresses);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mForwardingMode);
+ dest.writeInt(mMinScope);
+ dest.writeInt(mListeningAddresses.size());
+ for (final Inet6Address addr : mListeningAddresses) {
+ dest.writeByteArray(addr.getAddress());
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<MulticastRoutingConfig> CREATOR = new Creator<>() {
+ @Override
+ public MulticastRoutingConfig createFromParcel(@NonNull Parcel in) {
+ return new MulticastRoutingConfig(in);
+ }
+
+ @Override
+ public MulticastRoutingConfig[] newArray(int size) {
+ return new MulticastRoutingConfig[size];
+ }
+ };
+
+ private static String forwardingModeToString(final int forwardingMode) {
+ switch (forwardingMode) {
+ case FORWARD_NONE: return "NONE";
+ case FORWARD_SELECTED: return "SELECTED";
+ case FORWARD_WITH_MIN_SCOPE: return "WITH_MIN_SCOPE";
+ default: return "UNKNOWN";
+ }
+ }
+
+ public static final class Builder {
+ @MulticastForwardingMode
+ private final int mForwardingMode;
+ private int mMinScope;
+ private final ArraySet<Inet6Address> mListeningAddresses;
+
+ // The two constructors with runtime checks for the mode and scope are arguably
+ // less convenient than three static factory methods, but API guidelines mandates
+ // that Builders are built with a constructor and not factory methods.
+ /**
+ * Create a new builder for forwarding mode FORWARD_NONE or FORWARD_SELECTED.
+ *
+ * <p>On a Builder for FORWARD_NONE, no properties can be set.
+ * <p>On a Builder for FORWARD_SELECTED, listening addresses can be added and removed
+ * but the minimum scope can't be set.
+ *
+ * @param mode {@link #FORWARD_NONE} or {@link #FORWARD_SELECTED}. Any other
+ * value will result in IllegalArgumentException.
+ * @see #Builder(int, int)
+ */
+ public Builder(@MulticastForwardingMode final int mode) {
+ if (FORWARD_NONE != mode && FORWARD_SELECTED != mode) {
+ if (FORWARD_WITH_MIN_SCOPE == mode) {
+ throw new IllegalArgumentException("FORWARD_WITH_MIN_SCOPE requires "
+ + "passing the scope as a second argument");
+ } else {
+ throw new IllegalArgumentException("Unknown forwarding mode : " + mode);
+ }
+ }
+ mForwardingMode = mode;
+ mMinScope = MULTICAST_SCOPE_NONE;
+ mListeningAddresses = new ArraySet<>();
+ }
+
+ /**
+ * Create a new builder for forwarding mode FORWARD_WITH_MIN_SCOPE.
+ *
+ * <p>On this Builder the scope can be set with {@link #setMinimumScope}, but
+ * listening addresses can't be added or removed.
+ *
+ * @param mode Must be {@link #FORWARD_WITH_MIN_SCOPE}.
+ * @param scope the minimum scope for this multicast routing config.
+ * @see Builder#Builder(int)
+ */
+ public Builder(@MulticastForwardingMode final int mode, int scope) {
+ if (FORWARD_WITH_MIN_SCOPE != mode) {
+ throw new IllegalArgumentException("Forwarding with a min scope must "
+ + "use forward mode FORWARD_WITH_MIN_SCOPE");
+ }
+ mForwardingMode = mode;
+ mMinScope = scope;
+ mListeningAddresses = new ArraySet<>();
+ }
+
+ /**
+ * Sets the minimum scope for this multicast routing config.
+ * This is only meaningful (indeed, allowed) for configs in FORWARD_WITH_MIN_SCOPE mode.
+ * @return this builder
+ */
+ @NonNull
+ public Builder setMinimumScope(final int scope) {
+ if (FORWARD_WITH_MIN_SCOPE != mForwardingMode) {
+ throw new IllegalArgumentException("Can't set the scope on a builder in mode "
+ + modeToString(mForwardingMode));
+ }
+ mMinScope = scope;
+ return this;
+ }
+
+ /**
+ * Add an address to the set of listening addresses.
+ *
+ * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+ * If this address was already added, this is a no-op.
+ * @return this builder
+ */
+ @NonNull
+ public Builder addListeningAddress(@NonNull final Inet6Address address) {
+ if (FORWARD_SELECTED != mForwardingMode) {
+ throw new IllegalArgumentException("Can't add an address on a builder in mode "
+ + modeToString(mForwardingMode));
+ }
+ // TODO : should we check that this is a multicast address ?
+ mListeningAddresses.add(address);
+ return this;
+ }
+
+ /**
+ * Remove an address from the set of listening addresses.
+ *
+ * This is only meaningful (indeed, allowed) for configs in FORWARD_SELECTED mode.
+ * If this address was not added, or was already removed, this is a no-op.
+ * @return this builder
+ */
+ @NonNull
+ public Builder clearListeningAddress(@NonNull final Inet6Address address) {
+ if (FORWARD_SELECTED != mForwardingMode) {
+ throw new IllegalArgumentException("Can't remove an address on a builder in mode "
+ + modeToString(mForwardingMode));
+ }
+ mListeningAddresses.remove(address);
+ return this;
+ }
+
+ /**
+ * Build the config.
+ */
+ @NonNull
+ public MulticastRoutingConfig build() {
+ return new MulticastRoutingConfig(mForwardingMode, mMinScope, mListeningAddresses);
+ }
+ }
+
+ private static String modeToString(@MulticastForwardingMode final int mode) {
+ switch (mode) {
+ case FORWARD_NONE: return "FORWARD_NONE";
+ case FORWARD_SELECTED: return "FORWARD_SELECTED";
+ case FORWARD_WITH_MIN_SCOPE: return "FORWARD_WITH_MIN_SCOPE";
+ default: return "unknown multicast routing mode " + mode;
+ }
+ }
+
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof MulticastRoutingConfig)) {
+ return false;
+ } else {
+ final MulticastRoutingConfig otherConfig = (MulticastRoutingConfig) other;
+ return mForwardingMode == otherConfig.mForwardingMode
+ && mMinScope == otherConfig.mMinScope
+ && mListeningAddresses.equals(otherConfig.mListeningAddresses);
+ }
+ }
+
+ public int hashCode() {
+ return Objects.hash(mForwardingMode, mMinScope, mListeningAddresses);
+ }
+
+ public String toString() {
+ final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+
+ resultJoiner.add("ForwardingMode:");
+ resultJoiner.add(modeToString(mForwardingMode));
+
+ if (mForwardingMode == FORWARD_WITH_MIN_SCOPE) {
+ resultJoiner.add("MinScope:");
+ resultJoiner.add(Integer.toString(mMinScope));
+ }
+
+ if (mForwardingMode == FORWARD_SELECTED && !mListeningAddresses.isEmpty()) {
+ resultJoiner.add("ListeningAddresses: [");
+ resultJoiner.add(TextUtils.join(",", mListeningAddresses));
+ resultJoiner.add("]");
+ }
+
+ return resultJoiner.toString();
+ }
+}
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 177f7e3..4e9087c 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -151,7 +151,7 @@
/**
* Sent by the NetworkAgent to ConnectivityService to pass the current
- * NetworkCapabilties.
+ * NetworkCapabilities.
* obj = NetworkCapabilities
* @hide
*/
@@ -443,6 +443,14 @@
public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
/**
+ * Sent by the NetworkAgent to ConnectivityService to pass the new value of the local
+ * network agent config.
+ * obj = {@code Pair<NetworkAgentInfo, LocalNetworkConfig>}
+ * @hide
+ */
+ public static final int EVENT_LOCAL_NETWORK_CONFIG_CHANGED = BASE + 30;
+
+ /**
* DSCP policy was successfully added.
*/
public static final int DSCP_POLICY_STATUS_SUCCESS = 0;
@@ -517,20 +525,47 @@
@NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
@NonNull NetworkScore score, @NonNull NetworkAgentConfig config,
@Nullable NetworkProvider provider) {
- this(looper, context, logTag, nc, lp, score, config,
+ this(context, looper, logTag, nc, lp, null /* localNetworkConfig */, score, 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 localNetworkConfig the initial {@link LocalNetworkConfig} of this
+ * network. Update with sendLocalNetworkConfig. Must be
+ * non-null iff the nc have NET_CAPABILITY_LOCAL_NETWORK.
+ * @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.
+ * @hide
+ */
+ // TODO : expose
+ public NetworkAgent(@NonNull Context context, @NonNull Looper looper, @NonNull String logTag,
+ @NonNull NetworkCapabilities nc, @NonNull LinkProperties lp,
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+ @NonNull NetworkAgentConfig config, @Nullable NetworkProvider provider) {
+ this(looper, context, logTag, nc, lp, localNetworkConfig, 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;
+ @NonNull public final Context context;
+ @NonNull public final NetworkCapabilities capabilities;
+ @NonNull public final LinkProperties properties;
+ @NonNull public final NetworkScore score;
+ @NonNull public final NetworkAgentConfig config;
+ @NonNull public final NetworkInfo info;
+ @Nullable public final LocalNetworkConfig localNetworkConfig;
InitialConfiguration(@NonNull Context context, @NonNull NetworkCapabilities capabilities,
- @NonNull LinkProperties properties, @NonNull NetworkScore score,
+ @NonNull LinkProperties properties,
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
@NonNull NetworkAgentConfig config, @NonNull NetworkInfo info) {
this.context = context;
this.capabilities = capabilities;
@@ -538,14 +573,15 @@
this.score = score;
this.config = config;
this.info = info;
+ this.localNetworkConfig = localNetworkConfig;
}
}
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) {
+ @Nullable LocalNetworkConfig localNetworkConfig, @NonNull NetworkScore score,
+ @NonNull NetworkAgentConfig config, int providerId, @NonNull NetworkInfo ni) {
mHandler = new NetworkAgentHandler(looper);
LOG_TAG = logTag;
mNetworkInfo = new NetworkInfo(ni);
@@ -556,7 +592,7 @@
mInitialConfiguration = new InitialConfiguration(context,
new NetworkCapabilities(nc, NetworkCapabilities.REDACT_NONE),
- new LinkProperties(lp), score, config, ni);
+ new LinkProperties(lp), localNetworkConfig, score, config, ni);
}
private class NetworkAgentHandler extends Handler {
@@ -723,7 +759,8 @@
mNetwork = cm.registerNetworkAgent(new NetworkAgentBinder(mHandler),
new NetworkInfo(mInitialConfiguration.info),
mInitialConfiguration.properties, mInitialConfiguration.capabilities,
- mInitialConfiguration.score, mInitialConfiguration.config, providerId);
+ mInitialConfiguration.localNetworkConfig, mInitialConfiguration.score,
+ mInitialConfiguration.config, providerId);
mInitialConfiguration = null; // All this memory can now be GC'd
}
return mNetwork;
@@ -1099,6 +1136,18 @@
}
/**
+ * Must be called by the agent when the network's {@link LocalNetworkConfig} changes.
+ * @param config the new LocalNetworkConfig
+ * @hide
+ */
+ public void sendLocalNetworkConfig(@NonNull LocalNetworkConfig config) {
+ Objects.requireNonNull(config);
+ // If the agent doesn't have NET_CAPABILITY_LOCAL_NETWORK, this will be ignored by
+ // ConnectivityService with a Log.wtf.
+ queueOrSendMessage(reg -> reg.sendLocalNetworkConfig(config));
+ }
+
+ /**
* Must be called by the agent to update the score of this network.
*
* @param score the new score.
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index abda1fa..efae754 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -29,6 +29,9 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ConnectivityManager.NetworkCallback;
+// Can't be imported because aconfig tooling doesn't exist on udc-mainline-prod yet
+// See inner class Flags which mimics this for the time being
+// import android.net.flags.Flags;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
@@ -121,6 +124,14 @@
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
+ // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+ // available here
+ /** @hide */
+ public static class Flags {
+ static final String FLAG_FORBIDDEN_CAPABILITY =
+ "com.android.net.flags.forbidden_capability";
+ }
+
/**
* Mechanism to support redaction of fields in NetworkCapabilities that are guarded by specific
* app permissions.
@@ -442,6 +453,7 @@
NET_CAPABILITY_MMTEL,
NET_CAPABILITY_PRIORITIZE_LATENCY,
NET_CAPABILITY_PRIORITIZE_BANDWIDTH,
+ NET_CAPABILITY_LOCAL_NETWORK,
})
public @interface NetCapability { }
@@ -703,7 +715,21 @@
*/
public static final int NET_CAPABILITY_PRIORITIZE_BANDWIDTH = 35;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PRIORITIZE_BANDWIDTH;
+ /**
+ * This is a local network, e.g. a tethering downstream or a P2P direct network.
+ *
+ * <p>
+ * Note that local networks are not sent to callbacks by default. To receive callbacks about
+ * them, the {@link NetworkRequest} instance must be prepared to see them, either by
+ * adding the capability with {@link NetworkRequest.Builder#addCapability}, by removing
+ * this forbidden capability with {@link NetworkRequest.Builder#removeForbiddenCapability},
+ * or by clearing all capabilites with {@link NetworkRequest.Builder#clearCapabilities()}.
+ * </p>
+ * @hide
+ */
+ public static final int NET_CAPABILITY_LOCAL_NETWORK = 36;
+
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_LOCAL_NETWORK;
// Set all bits up to the MAX_NET_CAPABILITY-th bit
private static final long ALL_VALID_CAPABILITIES = (2L << MAX_NET_CAPABILITY) - 1;
@@ -793,6 +819,10 @@
* 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.
+ * <p>
+ * If the capability was previously added to the list of forbidden capabilities (either
+ * by default or added using {@link #addForbiddenCapability(int)}), then it will be removed
+ * from the list of forbidden capabilities as well.
*
* @param capability the capability to be added.
* @return This NetworkCapabilities instance, to facilitate chaining.
@@ -801,8 +831,7 @@
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.
+ // TODO: Add forbidden capabilities to the public API
checkValidCapability(capability);
mNetworkCapabilities |= 1L << capability;
// remove from forbidden capability list
@@ -845,7 +874,7 @@
}
/**
- * Removes (if found) the given forbidden capability from this {@code NetworkCapability}
+ * Removes (if found) the given forbidden capability from this {@link NetworkCapabilities}
* instance that were added via addForbiddenCapability(int) or setCapabilities(int[], int[]).
*
* @param capability the capability to be removed.
@@ -859,6 +888,16 @@
}
/**
+ * Removes all forbidden capabilities from this {@link NetworkCapabilities} instance.
+ * @return This NetworkCapabilities instance, to facilitate chaining.
+ * @hide
+ */
+ public @NonNull NetworkCapabilities removeAllForbiddenCapabilities() {
+ mForbiddenNetworkCapabilities = 0;
+ return this;
+ }
+
+ /**
* Sets (or clears) the given capability on this {@link NetworkCapabilities}
* instance.
* @hide
@@ -901,6 +940,8 @@
* @return an array of forbidden capability values for this instance.
* @hide
*/
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public @NetCapability int[] getForbiddenCapabilities() {
return BitUtils.unpackBits(mForbiddenNetworkCapabilities);
}
@@ -1000,7 +1041,7 @@
/**
* Tests for the presence of a capability on this instance.
*
- * @param capability the capabilities to be tested for.
+ * @param capability the capability to be tested for.
* @return {@code true} if set on this instance.
*/
public boolean hasCapability(@NetCapability int capability) {
@@ -1008,19 +1049,27 @@
&& ((mNetworkCapabilities & (1L << capability)) != 0);
}
- /** @hide */
+ /**
+ * Tests for the presence of a forbidden capability on this instance.
+ *
+ * @param capability the capability to be tested for.
+ * @return {@code true} if this capability is set forbidden on this instance.
+ * @hide
+ */
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public boolean hasForbiddenCapability(@NetCapability int capability) {
return isValidCapability(capability)
&& ((mForbiddenNetworkCapabilities & (1L << capability)) != 0);
}
/**
- * Check if this NetworkCapabilities has system managed capabilities or not.
+ * Check if this NetworkCapabilities has connectivity-managed capabilities or not.
* @hide
*/
public boolean hasConnectivityManagedCapability() {
- return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0);
+ return (mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0
+ || mForbiddenNetworkCapabilities != 0;
}
/**
@@ -1408,6 +1457,18 @@
return mTransportTypes == (1 << transportType);
}
+ /**
+ * Returns true iff this NC has the specified transport and no other, ignoring TRANSPORT_TEST.
+ *
+ * If this NC has the passed transport and no other, this method returns true.
+ * If this NC has the passed transport, TRANSPORT_TEST and no other, this method returns true.
+ * Otherwise, this method returns false.
+ * @hide
+ */
+ public boolean hasSingleTransportBesidesTest(@Transport int transportType) {
+ return (mTransportTypes & ~(1 << TRANSPORT_TEST)) == (1 << transportType);
+ }
+
private boolean satisfiedByTransportTypes(NetworkCapabilities nc) {
return ((this.mTransportTypes == 0)
|| ((this.mTransportTypes & nc.mTransportTypes) != 0));
@@ -2500,6 +2561,7 @@
case NET_CAPABILITY_MMTEL: return "MMTEL";
case NET_CAPABILITY_PRIORITIZE_LATENCY: return "PRIORITIZE_LATENCY";
case NET_CAPABILITY_PRIORITIZE_BANDWIDTH: return "PRIORITIZE_BANDWIDTH";
+ case NET_CAPABILITY_LOCAL_NETWORK: return "LOCAL_NETWORK";
default: return Integer.toString(capability);
}
}
@@ -2889,6 +2951,44 @@
}
/**
+ * Adds the given capability to the list of forbidden capabilities.
+ *
+ * A network with a capability will not match a {@link NetworkCapabilities} or
+ * {@link NetworkRequest} which has said capability set as forbidden. For example, if
+ * a request has NET_CAPABILITY_INTERNET in the list of forbidden capabilities, networks
+ * with NET_CAPABILITY_INTERNET will not match the request.
+ *
+ * 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.
+ *
+ * @param capability the capability
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+ public Builder addForbiddenCapability(@NetCapability final int capability) {
+ mCaps.addForbiddenCapability(capability);
+ return this;
+ }
+
+ /**
+ * Removes the given capability from the list of forbidden capabilities.
+ *
+ * @see #addForbiddenCapability(int)
+ * @param capability the capability
+ * @return this builder
+ * @hide
+ */
+ @NonNull
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
+ public Builder removeForbiddenCapability(@NetCapability final int capability) {
+ mCaps.removeForbiddenCapability(capability);
+ return this;
+ }
+
+ /**
* Adds the given enterprise capability identifier.
* Note that when searching for a network to satisfy a request, all capabilities identifier
* requested must be satisfied. Enterprise capability identifier is applicable only
@@ -3235,4 +3335,4 @@
return new NetworkCapabilities(mCaps);
}
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 6c351d0..9824faa 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@
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_LOCAL_NETWORK;
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;
@@ -39,6 +40,8 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
+// TODO : replace with android.net.flags.Flags when aconfig is supported on udc-mainline-prod
+// import android.net.NetworkCapabilities.Flags;
import android.net.NetworkCapabilities.NetCapability;
import android.net.NetworkCapabilities.Transport;
import android.os.Build;
@@ -281,6 +284,15 @@
NET_CAPABILITY_TRUSTED,
NET_CAPABILITY_VALIDATED);
+ /**
+ * Capabilities that are forbidden by default.
+ * Forbidden capabilities only make sense in NetworkRequest, not for network agents.
+ * Therefore these capabilities are only in NetworkRequest.
+ */
+ private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
+ NET_CAPABILITY_LOCAL_NETWORK
+ };
+
private final NetworkCapabilities mNetworkCapabilities;
// A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when
@@ -296,6 +308,16 @@
// it for apps that do not have the NETWORK_SETTINGS permission.
mNetworkCapabilities = new NetworkCapabilities();
mNetworkCapabilities.setSingleUid(Process.myUid());
+ // Default forbidden capabilities are foremost meant to help with backward
+ // compatibility. When adding new types of network identified by a capability that
+ // might confuse older apps, a default forbidden capability will have apps not see
+ // these networks unless they explicitly ask for it.
+ // If the app called clearCapabilities() it will see everything, but then it
+ // can be argued that it's fair to send them too, since it asked for everything
+ // explicitly.
+ for (final int forbiddenCap : DEFAULT_FORBIDDEN_CAPABILITIES) {
+ mNetworkCapabilities.addForbiddenCapability(forbiddenCap);
+ }
}
/**
@@ -408,6 +430,7 @@
@NonNull
@SuppressLint("MissingGetterMatchingBuilder")
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public Builder addForbiddenCapability(@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.addForbiddenCapability(capability);
return this;
@@ -424,6 +447,7 @@
@NonNull
@SuppressLint("BuilderSetStyle")
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public
public Builder removeForbiddenCapability(
@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.removeForbiddenCapability(capability);
@@ -433,6 +457,7 @@
/**
* Completely clears all the {@code NetworkCapabilities} from this builder instance,
* removing even the capabilities that are set by default when the object is constructed.
+ * Also removes any set forbidden capabilities.
*
* @return The builder to facilitate chaining.
*/
@@ -721,6 +746,7 @@
* @hide
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
public boolean hasForbiddenCapability(@NetCapability int capability) {
return networkCapabilities.hasForbiddenCapability(capability);
}
@@ -843,6 +869,7 @@
*/
@NonNull
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ // TODO : @FlaggedApi(Flags.FLAG_FORBIDDEN_CAPABILITY) and public instead of @SystemApi
public @NetCapability int[] getForbiddenCapabilities() {
// No need to make a defensive copy here as NC#getForbiddenCapabilities() already returns
// a new array.
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 815e2b0..935dea1 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -44,7 +44,9 @@
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {
KEEP_CONNECTED_NONE,
- KEEP_CONNECTED_FOR_HANDOVER
+ KEEP_CONNECTED_FOR_HANDOVER,
+ KEEP_CONNECTED_FOR_TEST,
+ KEEP_CONNECTED_LOCAL_NETWORK
})
public @interface KeepConnectedReason { }
@@ -57,6 +59,18 @@
* is being considered for handover.
*/
public static final int KEEP_CONNECTED_FOR_HANDOVER = 1;
+ /**
+ * Keep this network connected even if there is no outstanding request for it, because it
+ * is used in a test and it's not necessarily easy to file the right request for it.
+ * @hide
+ */
+ public static final int KEEP_CONNECTED_FOR_TEST = 2;
+ /**
+ * Keep this network connected even if there is no outstanding request for it, because
+ * it is a local network.
+ * @hide
+ */
+ public static final int KEEP_CONNECTED_LOCAL_NETWORK = 3;
// Agent-managed policies
// This network should lose to a wifi that has ever been validated
diff --git a/framework/src/android/net/QosSession.java b/framework/src/android/net/QosSession.java
index 25f3965..d1edae9 100644
--- a/framework/src/android/net/QosSession.java
+++ b/framework/src/android/net/QosSession.java
@@ -22,6 +22,9 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Provides identifying information of a QoS session. Sent to an application through
* {@link QosCallback}.
@@ -107,6 +110,7 @@
TYPE_EPS_BEARER,
TYPE_NR_BEARER,
})
+ @Retention(RetentionPolicy.SOURCE)
@interface QosSessionType {}
private QosSession(final Parcel in) {
diff --git a/framework/src/android/net/RouteInfo.java b/framework/src/android/net/RouteInfo.java
index df5f151..e8ebf81 100644
--- a/framework/src/android/net/RouteInfo.java
+++ b/framework/src/android/net/RouteInfo.java
@@ -584,7 +584,7 @@
}
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)
+ // consider the scope ID, but the route IPCs (e.g., RoutingCoordinatorManager#addRoute)
// 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)
diff --git a/framework/src/android/net/RoutingCoordinatorManager.java b/framework/src/android/net/RoutingCoordinatorManager.java
new file mode 100644
index 0000000..a9e7eef
--- /dev/null
+++ b/framework/src/android/net/RoutingCoordinatorManager.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * A manager class for talking to the routing coordinator service.
+ *
+ * This class should only be used by the connectivity and tethering module. This is enforced
+ * by the build rules. Do not change build rules to gain access to this class from elsewhere.
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class RoutingCoordinatorManager {
+ @NonNull final Context mContext;
+ @NonNull final IRoutingCoordinator mService;
+
+ public RoutingCoordinatorManager(@NonNull final Context context,
+ @NonNull final IRoutingCoordinator service) {
+ mContext = context;
+ mService = service;
+ }
+
+ /**
+ * Add a route for specific network
+ *
+ * @param netId the network to add the route to
+ * @param route the route to add
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addRoute(final int netId, final RouteInfo route) {
+ try {
+ mService.addRoute(netId, route);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove a route for specific network
+ *
+ * @param netId the network to remove the route from
+ * @param route the route to remove
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeRoute(final int netId, final RouteInfo route) {
+ try {
+ mService.removeRoute(netId, route);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Update a route for specific network
+ *
+ * @param netId the network to update the route for
+ * @param route parcelable with route information
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void updateRoute(final int netId, final RouteInfo route) {
+ try {
+ mService.updateRoute(netId, route);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Adds an interface to a network. The interface must not be assigned to any network, including
+ * the specified network.
+ *
+ * @param netId the network to add the interface to.
+ * @param iface the name of the interface to add.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ public void addInterfaceToNetwork(final int netId, final String iface) {
+ try {
+ mService.addInterfaceToNetwork(netId, iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes an interface from a network. The interface must be assigned to the specified network.
+ *
+ * @param netId the network to remove the interface from.
+ * @param iface the name of the interface to remove.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ public void removeInterfaceFromNetwork(final int netId, final String iface) {
+ try {
+ mService.removeInterfaceFromNetwork(netId, iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add forwarding ip rule
+ *
+ * @param fromIface interface name to add forwarding ip rule
+ * @param toIface interface name to add forwarding ip rule
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addInterfaceForward(final String fromIface, final String toIface) {
+ try {
+ mService.addInterfaceForward(fromIface, toIface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Remove forwarding ip rule
+ *
+ * @param fromIface interface name to remove forwarding ip rule
+ * @param toIface interface name to remove forwarding ip rule
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeInterfaceForward(final String fromIface, final String toIface) {
+ try {
+ mService.removeInterfaceForward(fromIface, toIface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/service/src/com/android/server/UidOwnerValue.java b/framework/src/android/net/UidOwnerValue.java
similarity index 86%
rename from service/src/com/android/server/UidOwnerValue.java
rename to framework/src/android/net/UidOwnerValue.java
index d6c0e0d..e8ae604 100644
--- a/service/src/com/android/server/UidOwnerValue.java
+++ b/framework/src/android/net/UidOwnerValue.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-package com.android.server;
+package android.net;
import com.android.net.module.util.Struct;
-/** Value type for per uid traffic control configuration map */
+/**
+ * Value type for per uid traffic control configuration map.
+ *
+ * @hide
+ */
public class UidOwnerValue extends Struct {
// Allowed interface index. Only applicable if IIF_MATCH is set in the rule bitmask below.
@Field(order = 0, type = Type.S32)
diff --git a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
similarity index 74%
rename from framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
rename to framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
index d65858f..79f1f65 100644
--- a/framework/src/android/net/connectivity/TiramisuConnectivityInternalApiUtil.java
+++ b/framework/src/android/net/connectivity/ConnectivityInternalApiUtil.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.RoutingCoordinatorManager;
import android.os.Build;
import android.os.IBinder;
@@ -34,15 +35,27 @@
* linter).
* @hide
*/
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
-public class TiramisuConnectivityInternalApiUtil {
+@RequiresApi(Build.VERSION_CODES.S)
+public class ConnectivityInternalApiUtil {
/**
* Get a service binder token for
* {@link com.android.server.connectivity.wear.CompanionDeviceManagerProxyService}.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public static IBinder getCompanionDeviceManagerProxyService(Context ctx) {
final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
return cm.getCompanionDeviceManagerProxyService();
}
+
+ /**
+ * Obtain a routing coordinator manager from a context, possibly cross-module.
+ * @param ctx the context
+ * @return an instance of the coordinator manager
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ public static RoutingCoordinatorManager getRoutingCoordinatorManager(Context ctx) {
+ final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+ return cm.getRoutingCoordinatorManager();
+ }
}
diff --git a/framework/udc-extended-api/system-current.txt b/framework/udc-extended-api/system-current.txt
index 4a2ed8a..e812024 100644
--- a/framework/udc-extended-api/system-current.txt
+++ b/framework/udc-extended-api/system-current.txt
@@ -94,6 +94,7 @@
}
public final class DscpPolicy implements android.os.Parcelable {
+ method public int describeContents();
method @Nullable public java.net.InetAddress getDestinationAddress();
method @Nullable public android.util.Range<java.lang.Integer> getDestinationPortRange();
method public int getDscpValue();
@@ -101,6 +102,7 @@
method public int getProtocol();
method @Nullable public java.net.InetAddress getSourceAddress();
method public int getSourcePort();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.DscpPolicy> CREATOR;
field public static final int PROTOCOL_ANY = -1; // 0xffffffff
field public static final int SOURCE_PORT_ANY = -1; // 0xffffffff
diff --git a/nearby/README.md b/nearby/README.md
index 6925dc4..8451882 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -29,6 +29,20 @@
$ aidegen .
# This will launch Intellij project for Nearby module.
```
+Note, the setup above may fail to index classes defined in proto, such
+that all classes defined in proto shows red in IDE and cannot be auto-completed.
+To fix, you can mannually add jar files generated from proto to the class path
+as below. First, find the jar file of presence proto with
+```sh
+ls $ANDROID_BUILD_TOP/out/soong/.intermediates/packages/modules/Connectivity/nearby/service/proto/presence-lite-protos/android_common/combined/presence-lite-protos.jar
+```
+Then, add the jar in IDE as below.
+1. Menu: File > Project Structure
+2. Select Modules at the left panel and select the Dependencies tab.
+3. Select the + icon and select 1 JARs or Directories option.
+4. Select the JAR file found above, make sure it is checked in the beginning square.
+5. Click the OK button.
+6. Restart the IDE to re-index.
## Build and Install
@@ -40,3 +54,23 @@
--output /tmp/tethering.apex
$ adb install -r /tmp/tethering.apex
```
+
+## Build and Install from tm-mainline-prod branch
+When build and flash the APEX from tm-mainline-prod, you may see the error below.
+```
+[INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade of APEX package com.google.android.tethering is not allowed. Active version: 990090000 attempted: 339990000])
+```
+This is because the device is flashed with AOSP built from master or other branches, which has
+prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
+built from tm-mainline-prod as below.
+1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+2. cp tethering.next.apex com.google.android.tethering.apex
+3. adb push com.google.android.tethering.apex /system/apex/
+4. adb reboot
+After the steps above, the APEX can be reinstalled with adb install -r.
+(More APEX background can be found in https://source.android.com/docs/core/ota/apex#using-an-apex.)
+
+## Build APEX to support multiple platforms
+If you need to flash the APEX to different devices, Pixel 6, Pixel 7, or even devices from OEM, you
+can share the APEX by ```source build/envsetup.sh && lunch aosp_arm64-userdebug```. This can avoid
+ re-compiling for different targets.
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index f329295..4bb9efd 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -26,6 +26,7 @@
],
path: "java",
visibility: [
+ "//packages/modules/Connectivity/framework:__subpackages__",
"//packages/modules/Connectivity/framework-t:__subpackages__",
],
}
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.java b/nearby/framework/java/android/nearby/BroadcastRequest.java
index 90f4d0f..6d6357d 100644
--- a/nearby/framework/java/android/nearby/BroadcastRequest.java
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.java
@@ -88,6 +88,7 @@
* @hide
*/
@IntDef({MEDIUM_BLE})
+ @Retention(RetentionPolicy.SOURCE)
public @interface Medium {}
/**
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 6fa5fb5..10c1132 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -16,13 +16,17 @@
package android.nearby;
+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 com.android.internal.util.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
/**
* Represents a data element in Nearby Presence.
@@ -35,11 +39,95 @@
private final int mKey;
private final byte[] mValue;
+ /** @hide */
+ @IntDef({
+ DataType.BLE_SERVICE_DATA,
+ DataType.BLE_ADDRESS,
+ DataType.SALT,
+ DataType.PRIVATE_IDENTITY,
+ DataType.TRUSTED_IDENTITY,
+ DataType.PUBLIC_IDENTITY,
+ DataType.PROVISIONED_IDENTITY,
+ DataType.TX_POWER,
+ DataType.ACTION,
+ DataType.MODEL_ID,
+ DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
+ DataType.ACCOUNT_KEY_DATA,
+ DataType.CONNECTION_STATUS,
+ DataType.BATTERY,
+ DataType.SCAN_MODE,
+ DataType.TEST_DE_BEGIN,
+ DataType.TEST_DE_END
+ })
+ public @interface DataType {
+ int BLE_SERVICE_DATA = 100;
+ int BLE_ADDRESS = 101;
+ // This is to indicate if the scan is offload only
+ int SCAN_MODE = 102;
+ int SALT = 0;
+ int PRIVATE_IDENTITY = 1;
+ int TRUSTED_IDENTITY = 2;
+ int PUBLIC_IDENTITY = 3;
+ int PROVISIONED_IDENTITY = 4;
+ int TX_POWER = 5;
+ int ACTION = 6;
+ int MODEL_ID = 7;
+ int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
+ int ACCOUNT_KEY_DATA = 9;
+ int CONNECTION_STATUS = 10;
+ int BATTERY = 11;
+ // Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
+ // to {@link DataElement.DataType#TEST_DE_END}, inclusive.
+ // Reserves 128 Test DEs.
+ int TEST_DE_BEGIN = Integer.MAX_VALUE - 127; // 2147483520
+ int TEST_DE_END = Integer.MAX_VALUE; // 2147483647
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean isValidType(int type) {
+ return type == DataType.BLE_SERVICE_DATA
+ || type == DataType.ACCOUNT_KEY_DATA
+ || type == DataType.BLE_ADDRESS
+ || type == DataType.SCAN_MODE
+ || type == DataType.SALT
+ || type == DataType.PRIVATE_IDENTITY
+ || type == DataType.TRUSTED_IDENTITY
+ || type == DataType.PUBLIC_IDENTITY
+ || type == DataType.PROVISIONED_IDENTITY
+ || type == DataType.TX_POWER
+ || type == DataType.ACTION
+ || type == DataType.MODEL_ID
+ || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
+ || type == DataType.CONNECTION_STATUS
+ || type == DataType.BATTERY;
+ }
+
+ /**
+ * @return {@code true} if this is identity type.
+ * @hide
+ */
+ public boolean isIdentityDataType() {
+ return mKey == DataType.PRIVATE_IDENTITY
+ || mKey == DataType.TRUSTED_IDENTITY
+ || mKey == DataType.PUBLIC_IDENTITY
+ || mKey == DataType.PROVISIONED_IDENTITY;
+ }
+
+ /**
+ * @return {@code true} if this is test data element type.
+ * @hide
+ */
+ public static boolean isTestDeType(int type) {
+ return type >= DataType.TEST_DE_BEGIN && type <= DataType.TEST_DE_END;
+ }
+
/**
* Constructs a {@link DataElement}.
*/
public DataElement(int key, @NonNull byte[] value) {
- Preconditions.checkState(value != null, "value cannot be null");
+ Preconditions.checkArgument(value != null, "value cannot be null");
mKey = key;
mValue = value;
}
@@ -61,6 +149,20 @@
};
@Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof DataElement) {
+ return mKey == ((DataElement) obj).mKey
+ && Arrays.equals(mValue, ((DataElement) obj).mValue);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mKey, Arrays.hashCode(mValue));
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 0291fff..7af271e 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
import android.nearby.IScanListener;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
/**
* Interface for communicating with the nearby services.
@@ -37,4 +38,6 @@
in IBroadcastListener callback, String packageName, @nullable String attributionTag);
void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
+
+ void queryOffloadCapability(in IOffloadCallback callback) ;
}
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
index 3e3b107..80563b7 100644
--- a/nearby/framework/java/android/nearby/IScanListener.aidl
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -34,5 +34,5 @@
void onLost(in NearbyDeviceParcelable nearbyDeviceParcelable);
/** Reports when there is an error during scanning. */
- void onError();
+ void onError(in int errorCode);
}
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index 538940c..e7db0c5 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -21,11 +21,15 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.util.ArraySet;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* A class represents a device that can be discovered by multiple mediums.
@@ -123,13 +127,17 @@
@Override
public boolean equals(Object other) {
- if (other instanceof NearbyDevice) {
- NearbyDevice otherDevice = (NearbyDevice) other;
- return Objects.equals(mName, otherDevice.mName)
- && mMediums == otherDevice.mMediums
- && mRssi == otherDevice.mRssi;
+ if (!(other instanceof NearbyDevice)) {
+ return false;
}
- return false;
+ NearbyDevice otherDevice = (NearbyDevice) other;
+ Set<Integer> mediumSet = new ArraySet<>(mMediums);
+ Set<Integer> otherMediumSet = new ArraySet<>(otherDevice.mMediums);
+ if (!mediumSet.equals(otherMediumSet)) {
+ return false;
+ }
+
+ return Objects.equals(mName, otherDevice.mName) && mRssi == otherDevice.mRssi;
}
@Override
@@ -143,6 +151,7 @@
* @hide
*/
@IntDef({Medium.BLE, Medium.BLUETOOTH})
+ @Retention(RetentionPolicy.SOURCE)
public @interface Medium {
int BLE = 1;
int BLUETOOTH = 2;
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index 8f44091..8fb9650 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -46,6 +46,7 @@
@Override
public NearbyDeviceParcelable createFromParcel(Parcel in) {
Builder builder = new Builder();
+ builder.setDeviceId(in.readLong());
builder.setScanType(in.readInt());
if (in.readInt() == 1) {
builder.setName(in.readString());
@@ -76,6 +77,17 @@
in.readByteArray(salt);
builder.setData(salt);
}
+ if (in.readInt() == 1) {
+ builder.setPresenceDevice(in.readParcelable(
+ PresenceDevice.class.getClassLoader(),
+ PresenceDevice.class));
+ }
+ if (in.readInt() == 1) {
+ int encryptionKeyTagLength = in.readInt();
+ byte[] keyTag = new byte[encryptionKeyTagLength];
+ in.readByteArray(keyTag);
+ builder.setData(keyTag);
+ }
return builder.build();
}
@@ -85,6 +97,7 @@
}
};
+ private final long mDeviceId;
@ScanRequest.ScanType int mScanType;
@Nullable private final String mName;
@NearbyDevice.Medium private final int mMedium;
@@ -96,8 +109,11 @@
@Nullable private final String mFastPairModelId;
@Nullable private final byte[] mData;
@Nullable private final byte[] mSalt;
+ @Nullable private final PresenceDevice mPresenceDevice;
+ @Nullable private final byte[] mEncryptionKeyTag;
private NearbyDeviceParcelable(
+ long deviceId,
@ScanRequest.ScanType int scanType,
@Nullable String name,
int medium,
@@ -108,7 +124,10 @@
@Nullable String fastPairModelId,
@Nullable String bluetoothAddress,
@Nullable byte[] data,
- @Nullable byte[] salt) {
+ @Nullable byte[] salt,
+ @Nullable PresenceDevice presenceDevice,
+ @Nullable byte[] encryptionKeyTag) {
+ mDeviceId = deviceId;
mScanType = scanType;
mName = name;
mMedium = medium;
@@ -120,6 +139,8 @@
mBluetoothAddress = bluetoothAddress;
mData = data;
mSalt = salt;
+ mPresenceDevice = presenceDevice;
+ mEncryptionKeyTag = encryptionKeyTag;
}
/** No special parcel contents. */
@@ -136,6 +157,7 @@
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeLong(mDeviceId);
dest.writeInt(mScanType);
dest.writeInt(mName == null ? 0 : 1);
if (mName != null) {
@@ -164,13 +186,24 @@
dest.writeInt(mSalt.length);
dest.writeByteArray(mSalt);
}
+ dest.writeInt(mPresenceDevice == null ? 0 : 1);
+ if (mPresenceDevice != null) {
+ dest.writeParcelable(mPresenceDevice, /* parcelableFlags= */ 0);
+ }
+ dest.writeInt(mEncryptionKeyTag == null ? 0 : 1);
+ if (mEncryptionKeyTag != null) {
+ dest.writeInt(mEncryptionKeyTag.length);
+ dest.writeByteArray(mEncryptionKeyTag);
+ }
}
/** Returns a string representation of this ScanRequest. */
@Override
public String toString() {
return "NearbyDeviceParcelable["
- + "scanType="
+ + "deviceId="
+ + mDeviceId
+ + ", scanType="
+ mScanType
+ ", name="
+ mName
@@ -197,20 +230,25 @@
public boolean equals(Object other) {
if (other instanceof NearbyDeviceParcelable) {
NearbyDeviceParcelable otherNearbyDeviceParcelable = (NearbyDeviceParcelable) other;
- return mScanType == otherNearbyDeviceParcelable.mScanType
+ return mDeviceId == otherNearbyDeviceParcelable.mDeviceId
+ && mScanType == otherNearbyDeviceParcelable.mScanType
&& (Objects.equals(mName, otherNearbyDeviceParcelable.mName))
&& (mMedium == otherNearbyDeviceParcelable.mMedium)
&& (mTxPower == otherNearbyDeviceParcelable.mTxPower)
&& (mRssi == otherNearbyDeviceParcelable.mRssi)
&& (mAction == otherNearbyDeviceParcelable.mAction)
&& (Objects.equals(
- mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
+ mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
&& (Objects.equals(
- mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
+ mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
&& (Objects.equals(
- mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
+ mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
&& (Arrays.equals(mData, otherNearbyDeviceParcelable.mData))
- && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt));
+ && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt))
+ && (Objects.equals(
+ mPresenceDevice, otherNearbyDeviceParcelable.mPresenceDevice))
+ && (Arrays.equals(
+ mEncryptionKeyTag, otherNearbyDeviceParcelable.mEncryptionKeyTag));
}
return false;
}
@@ -218,6 +256,7 @@
@Override
public int hashCode() {
return Objects.hash(
+ mDeviceId,
mScanType,
mName,
mMedium,
@@ -227,7 +266,19 @@
mBluetoothAddress,
mFastPairModelId,
Arrays.hashCode(mData),
- Arrays.hashCode(mSalt));
+ Arrays.hashCode(mSalt),
+ mPresenceDevice,
+ Arrays.hashCode(mEncryptionKeyTag));
+ }
+
+ /**
+ * The id of the device.
+ * <p>This id is not a hardware id. It may rotate based on the remote device's broadcasts.
+ *
+ * @hide
+ */
+ public long getDeviceId() {
+ return mDeviceId;
}
/**
@@ -351,8 +402,29 @@
return mSalt;
}
+ /**
+ * Gets the {@link PresenceDevice} Nearby Presence device. This field is
+ * for Fast Pair client only.
+ */
+ @Nullable
+ public PresenceDevice getPresenceDevice() {
+ return mPresenceDevice;
+ }
+
+ /**
+ * Gets the encryption key tag calculated from advertisement
+ * Returns {@code null} if the data is not encrypted or this is not a Presence device.
+ *
+ * Used in Presence.
+ */
+ @Nullable
+ public byte[] getEncryptionKeyTag() {
+ return mEncryptionKeyTag;
+ }
+
/** Builder class for {@link NearbyDeviceParcelable}. */
public static final class Builder {
+ private long mDeviceId = -1;
@Nullable private String mName;
@NearbyDevice.Medium private int mMedium;
private int mTxPower;
@@ -364,6 +436,14 @@
@Nullable private String mBluetoothAddress;
@Nullable private byte[] mData;
@Nullable private byte[] mSalt;
+ @Nullable private PresenceDevice mPresenceDevice;
+ @Nullable private byte[] mEncryptionKeyTag;
+
+ /** Sets the id of the device. */
+ public Builder setDeviceId(long deviceId) {
+ this.mDeviceId = deviceId;
+ return this;
+ }
/**
* Sets the scan type of the NearbyDeviceParcelable.
@@ -469,7 +549,7 @@
/**
* Sets the scanned raw data.
*
- * @param data Data the scan. For example, {@link ScanRecord#getServiceData()} if scanned by
+ * @param data raw data scanned, like {@link ScanRecord#getServiceData()} if scanned by
* Bluetooth.
*/
@NonNull
@@ -479,6 +559,17 @@
}
/**
+ * Sets the encryption key tag calculated from the advertisement.
+ *
+ * @param encryptionKeyTag calculated from identity scanned from the advertisement
+ */
+ @NonNull
+ public Builder setEncryptionKeyTag(@Nullable byte[] encryptionKeyTag) {
+ mEncryptionKeyTag = encryptionKeyTag;
+ return this;
+ }
+
+ /**
* Sets the slat in the advertisement from the Nearby Presence device.
*
* @param salt in the advertisement from the Nearby Presence device.
@@ -489,10 +580,22 @@
return this;
}
+ /**
+ * Sets the {@link PresenceDevice} if there is any.
+ * The current {@link NearbyDeviceParcelable} can be seen as the wrapper of the
+ * {@link PresenceDevice}.
+ */
+ @Nullable
+ public Builder setPresenceDevice(@Nullable PresenceDevice presenceDevice) {
+ mPresenceDevice = presenceDevice;
+ return this;
+ }
+
/** Builds a ScanResult. */
@NonNull
public NearbyDeviceParcelable build() {
return new NearbyDeviceParcelable(
+ mDeviceId,
mScanType,
mName,
mMedium,
@@ -503,7 +606,9 @@
mFastPairModelId,
mBluetoothAddress,
mData,
- mSalt);
+ mSalt,
+ mPresenceDevice,
+ mEncryptionKeyTag);
}
}
}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 106c290..070a2b6 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -26,6 +26,7 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
+import android.nearby.aidl.IOffloadCallback;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
@@ -33,10 +34,13 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -61,8 +65,9 @@
ScanStatus.SUCCESS,
ScanStatus.ERROR,
})
+ @Retention(RetentionPolicy.SOURCE)
public @interface ScanStatus {
- // Default, invalid state.
+ // The undetermined status, some modules may be initializing. Retry is suggested.
int UNKNOWN = 0;
// The successful state.
int SUCCESS = 1;
@@ -73,6 +78,7 @@
private static final String TAG = "NearbyManager";
/**
+ * TODO(b/286137024): Remove this when CTS R5 is rolled out.
* Whether allows Fast Pair to scan.
*
* (0 = disabled, 1 = enabled)
@@ -103,6 +109,9 @@
mService = service;
}
+ // This can be null when NearbyDeviceParcelable field not set for Presence device
+ // or the scan type is not recognized.
+ @Nullable
private static NearbyDevice toClientNearbyDevice(
NearbyDeviceParcelable nearbyDeviceParcelable,
@ScanRequest.ScanType int scanType) {
@@ -118,23 +127,12 @@
}
if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) {
- PublicCredential publicCredential = nearbyDeviceParcelable.getPublicCredential();
- if (publicCredential == null) {
- return null;
+ PresenceDevice presenceDevice = nearbyDeviceParcelable.getPresenceDevice();
+ if (presenceDevice == null) {
+ Log.e(TAG,
+ "Cannot find any Presence device in discovered NearbyDeviceParcelable");
}
- byte[] salt = nearbyDeviceParcelable.getSalt();
- if (salt == null) {
- salt = new byte[0];
- }
- return new PresenceDevice.Builder(
- // Use the public credential hash as the device Id.
- String.valueOf(publicCredential.hashCode()),
- salt,
- publicCredential.getSecretId(),
- publicCredential.getEncryptedMetadata())
- .setRssi(nearbyDeviceParcelable.getRssi())
- .addMedium(nearbyDeviceParcelable.getMedium())
- .build();
+ return presenceDevice;
}
return null;
}
@@ -278,29 +276,42 @@
}
/**
- * Read from {@link Settings} whether Fast Pair scan is enabled.
+ * Query offload capability in a device. The query is asynchronous and result is called back
+ * in {@link Consumer}, which is set to true if offload is supported.
*
- * @param context the {@link Context} to query the setting
- * @return whether the Fast Pair is enabled
- * @hide
+ * @param executor the callback will take place on this {@link Executor}
+ * @param callback the callback invoked with {@link OffloadCapability}
*/
- public static boolean getFastPairScanEnabled(@NonNull Context context) {
- final int enabled = Settings.Secure.getInt(
- context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
- return enabled != 0;
+ public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Consumer<OffloadCapability> callback) {
+ try {
+ mService.queryOffloadCapability(new OffloadTransport(executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
}
- /**
- * Write into {@link Settings} whether Fast Pair scan is enabled
- *
- * @param context the {@link Context} to set the setting
- * @param enable whether the Fast Pair scan should be enabled
- * @hide
- */
- @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
- public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
- Settings.Secure.putInt(
- context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+ private static class OffloadTransport extends IOffloadCallback.Stub {
+
+ private final Executor mExecutor;
+ // Null when cancelled
+ volatile @Nullable Consumer<OffloadCapability> mConsumer;
+
+ OffloadTransport(Executor executor, Consumer<OffloadCapability> consumer) {
+ Preconditions.checkArgument(executor != null, "illegal null executor");
+ Preconditions.checkArgument(consumer != null, "illegal null consumer");
+ mExecutor = executor;
+ mConsumer = consumer;
+ }
+
+ @Override
+ public void onQueryComplete(OffloadCapability capability) {
+ mExecutor.execute(() -> {
+ if (mConsumer != null) {
+ mConsumer.accept(capability);
+ }
+ });
+ }
}
private static class ScanListenerTransport extends IScanListener.Stub {
@@ -339,9 +350,9 @@
public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
- mScanCallback.onDiscovered(
- toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
+ mScanCallback.onDiscovered(nearbyDevice);
}
});
}
@@ -350,7 +361,8 @@
public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onUpdated(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
@@ -360,7 +372,8 @@
@Override
public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onLost(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
@@ -368,10 +381,10 @@
}
@Override
- public void onError() {
+ public void onError(int errorCode) {
mExecutor.execute(() -> {
if (mScanCallback != null) {
- Log.e("NearbyManager", "onError: There is an error in scan.");
+ mScanCallback.onError(errorCode);
}
});
}
@@ -410,4 +423,35 @@
});
}
}
+
+ /**
+ * TODO(b/286137024): Remove this when CTS R5 is rolled out.
+ * Read from {@link Settings} whether Fast Pair scan is enabled.
+ *
+ * @param context the {@link Context} to query the setting
+ * @return whether the Fast Pair is enabled
+ * @hide
+ */
+ public static boolean getFastPairScanEnabled(@NonNull Context context) {
+ final int enabled = Settings.Secure.getInt(
+ context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
+ return enabled != 0;
+ }
+
+ /**
+ * TODO(b/286137024): Remove this when CTS R5 is rolled out.
+ * Write into {@link Settings} whether Fast Pair scan is enabled
+ *
+ * @param context the {@link Context} to set the setting
+ * @param enable whether the Fast Pair scan should be enabled
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
+ Settings.Secure.putInt(
+ context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+ Log.v(TAG, String.format(
+ "successfully %s Fast Pair scan", enable ? "enables" : "disables"));
+ }
+
}
diff --git a/nearby/framework/java/android/nearby/OffloadCapability.aidl b/nearby/framework/java/android/nearby/OffloadCapability.aidl
new file mode 100644
index 0000000..fe1c45e
--- /dev/null
+++ b/nearby/framework/java/android/nearby/OffloadCapability.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+/**
+ * A class that can describe what offload functions are available.
+ *
+ * {@hide}
+ */
+parcelable OffloadCapability;
+
diff --git a/nearby/framework/java/android/nearby/OffloadCapability.java b/nearby/framework/java/android/nearby/OffloadCapability.java
new file mode 100644
index 0000000..9071c1c
--- /dev/null
+++ b/nearby/framework/java/android/nearby/OffloadCapability.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class that can describe what offload functions are available.
+ *
+ * @hide
+ */
+@SystemApi
+public final class OffloadCapability implements Parcelable {
+ private final boolean mFastPairSupported;
+ private final boolean mNearbyShareSupported;
+ private final long mVersion;
+
+ public boolean isFastPairSupported() {
+ return mFastPairSupported;
+ }
+
+ public boolean isNearbyShareSupported() {
+ return mNearbyShareSupported;
+ }
+
+ public long getVersion() {
+ return mVersion;
+ }
+
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mFastPairSupported);
+ dest.writeBoolean(mNearbyShareSupported);
+ dest.writeLong(mVersion);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<OffloadCapability> CREATOR = new Creator<OffloadCapability>() {
+ @Override
+ public OffloadCapability createFromParcel(Parcel in) {
+ boolean isFastPairSupported = in.readBoolean();
+ boolean isNearbyShareSupported = in.readBoolean();
+ long version = in.readLong();
+ return new Builder()
+ .setFastPairSupported(isFastPairSupported)
+ .setNearbyShareSupported(isNearbyShareSupported)
+ .setVersion(version)
+ .build();
+ }
+
+ @Override
+ public OffloadCapability[] newArray(int size) {
+ return new OffloadCapability[size];
+ }
+ };
+
+ private OffloadCapability(boolean fastPairSupported, boolean nearbyShareSupported,
+ long version) {
+ mFastPairSupported = fastPairSupported;
+ mNearbyShareSupported = nearbyShareSupported;
+ mVersion = version;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof OffloadCapability)) return false;
+ OffloadCapability that = (OffloadCapability) o;
+ return isFastPairSupported() == that.isFastPairSupported()
+ && isNearbyShareSupported() == that.isNearbyShareSupported()
+ && getVersion() == that.getVersion();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(isFastPairSupported(), isNearbyShareSupported(), getVersion());
+ }
+
+ @Override
+ public String toString() {
+ return "OffloadCapability{"
+ + "fastPairSupported=" + mFastPairSupported
+ + ", nearbyShareSupported=" + mNearbyShareSupported
+ + ", version=" + mVersion
+ + '}';
+ }
+
+ /**
+ * Builder class for {@link OffloadCapability}.
+ */
+ public static final class Builder {
+ private boolean mFastPairSupported;
+ private boolean mNearbyShareSupported;
+ private long mVersion;
+
+ /**
+ * Sets if the Nearby Share feature is supported
+ *
+ * @param fastPairSupported {@code true} if the Fast Pair feature is supported
+ */
+ @NonNull
+ public Builder setFastPairSupported(boolean fastPairSupported) {
+ mFastPairSupported = fastPairSupported;
+ return this;
+ }
+
+ /**
+ * Sets if the Nearby Share feature is supported.
+ *
+ * @param nearbyShareSupported {@code true} if the Nearby Share feature is supported
+ */
+ @NonNull
+ public Builder setNearbyShareSupported(boolean nearbyShareSupported) {
+ mNearbyShareSupported = nearbyShareSupported;
+ return this;
+ }
+
+ /**
+ * Sets the version number of Nearby Offload.
+ *
+ * @param version Nearby Offload version number
+ */
+ @NonNull
+ public Builder setVersion(long version) {
+ mVersion = version;
+ return this;
+ }
+
+ /**
+ * Builds an OffloadCapability object.
+ */
+ @NonNull
+ public OffloadCapability build() {
+ return new OffloadCapability(mFastPairSupported, mNearbyShareSupported, mVersion);
+ }
+ }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceDevice.java b/nearby/framework/java/android/nearby/PresenceDevice.java
index cb406e4..b5d9ad4 100644
--- a/nearby/framework/java/android/nearby/PresenceDevice.java
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -26,6 +26,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -134,6 +135,54 @@
return mExtendedProperties;
}
+ /**
+ * This can only be hidden because this is the System API,
+ * which cannot be changed in T timeline.
+ *
+ * @hide
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof PresenceDevice) {
+ PresenceDevice otherDevice = (PresenceDevice) other;
+ if (super.equals(otherDevice)) {
+ return Arrays.equals(mSalt, otherDevice.mSalt)
+ && Arrays.equals(mSecretId, otherDevice.mSecretId)
+ && Arrays.equals(mEncryptedIdentity, otherDevice.mEncryptedIdentity)
+ && Objects.equals(mDeviceId, otherDevice.mDeviceId)
+ && mDeviceType == otherDevice.mDeviceType
+ && Objects.equals(mDeviceImageUrl, otherDevice.mDeviceImageUrl)
+ && mDiscoveryTimestampMillis == otherDevice.mDiscoveryTimestampMillis
+ && Objects.equals(mExtendedProperties, otherDevice.mExtendedProperties);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This can only be hidden because this is the System API,
+ * which cannot be changed in T timeline.
+ *
+ * @hide
+ *
+ * @return The unique hash value of the {@link PresenceDevice}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getName(),
+ getMediums(),
+ getRssi(),
+ Arrays.hashCode(mSalt),
+ Arrays.hashCode(mSecretId),
+ Arrays.hashCode(mEncryptedIdentity),
+ mDeviceId,
+ mDeviceType,
+ mDeviceImageUrl,
+ mDiscoveryTimestampMillis,
+ mExtendedProperties);
+ }
+
private PresenceDevice(String deviceName, List<Integer> mMediums, int rssi, String deviceId,
byte[] salt, byte[] secretId, byte[] encryptedIdentity, int deviceType,
String deviceImageUrl, long discoveryTimestampMillis,
@@ -326,7 +375,6 @@
return this;
}
-
/**
* Sets the image url of the discovered Presence device.
*
@@ -338,7 +386,6 @@
return this;
}
-
/**
* Sets discovery timestamp, the clock is based on elapsed time.
*
@@ -350,7 +397,6 @@
return this;
}
-
/**
* Adds an extended property of the discovered presence device.
*
diff --git a/nearby/framework/java/android/nearby/PresenceScanFilter.java b/nearby/framework/java/android/nearby/PresenceScanFilter.java
index f0c3c06..50e97b4 100644
--- a/nearby/framework/java/android/nearby/PresenceScanFilter.java
+++ b/nearby/framework/java/android/nearby/PresenceScanFilter.java
@@ -71,7 +71,7 @@
super(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE, rssiThreshold);
mCredentials = new ArrayList<>(credentials);
mPresenceActions = new ArrayList<>(presenceActions);
- mExtendedProperties = extendedProperties;
+ mExtendedProperties = new ArrayList<>(extendedProperties);
}
private PresenceScanFilter(Parcel in) {
@@ -132,7 +132,7 @@
}
dest.writeInt(mExtendedProperties.size());
if (!mExtendedProperties.isEmpty()) {
- dest.writeList(mExtendedProperties);
+ dest.writeParcelableList(mExtendedProperties, 0);
}
}
diff --git a/nearby/framework/java/android/nearby/ScanCallback.java b/nearby/framework/java/android/nearby/ScanCallback.java
index 1b1b4bc..7b66607 100644
--- a/nearby/framework/java/android/nearby/ScanCallback.java
+++ b/nearby/framework/java/android/nearby/ScanCallback.java
@@ -16,9 +16,13 @@
package android.nearby;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Reports newly discovered devices.
* Note: The frequency of the callback is dependent on whether the caller
@@ -31,6 +35,37 @@
*/
@SystemApi
public interface ScanCallback {
+
+ /** General error code for scan. */
+ int ERROR_UNKNOWN = 0;
+
+ /**
+ * Scan failed as the request is not supported.
+ */
+ int ERROR_UNSUPPORTED = 1;
+
+ /**
+ * Invalid argument such as out-of-range, illegal format etc.
+ */
+ int ERROR_INVALID_ARGUMENT = 2;
+
+ /**
+ * Request from clients who do not have permissions.
+ */
+ int ERROR_PERMISSION_DENIED = 3;
+
+ /**
+ * Request cannot be fulfilled due to limited resource.
+ */
+ int ERROR_RESOURCE_EXHAUSTED = 4;
+
+ /** @hide **/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ERROR_UNKNOWN, ERROR_UNSUPPORTED, ERROR_INVALID_ARGUMENT, ERROR_PERMISSION_DENIED,
+ ERROR_RESOURCE_EXHAUSTED})
+ @interface ErrorCode {
+ }
+
/**
* Reports a {@link NearbyDevice} being discovered.
*
@@ -51,4 +86,11 @@
* @param device {@link NearbyDevice} that is lost.
*/
void onLost(@NonNull NearbyDevice device);
+
+ /**
+ * Notifies clients of error from the scan.
+ *
+ * @param errorCode defined by Nearby
+ */
+ default void onError(@ErrorCode int errorCode) {}
}
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
index c717ac7..61cbf39 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.WorkSource;
@@ -33,6 +34,8 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* An encapsulation of various parameters for requesting nearby scans.
@@ -62,6 +65,12 @@
*/
public static final int SCAN_MODE_NO_POWER = -1;
/**
+ * A special scan mode to indicate that client only wants to use CHRE to scan.
+ *
+ * @hide
+ */
+ public static final int SCAN_MODE_CHRE_ONLY = 3;
+ /**
* Used to read a ScanRequest from a Parcel.
*/
@NonNull
@@ -72,6 +81,7 @@
.setScanType(in.readInt())
.setScanMode(in.readInt())
.setBleEnabled(in.readBoolean())
+ .setOffloadOnly(in.readBoolean())
.setWorkSource(in.readTypedObject(WorkSource.CREATOR));
final int size = in.readInt();
for (int i = 0; i < size; i++) {
@@ -89,14 +99,16 @@
private final @ScanType int mScanType;
private final @ScanMode int mScanMode;
private final boolean mBleEnabled;
+ private final boolean mOffloadOnly;
private final @NonNull WorkSource mWorkSource;
private final List<ScanFilter> mScanFilters;
private ScanRequest(@ScanType int scanType, @ScanMode int scanMode, boolean bleEnabled,
- @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
+ boolean offloadOnly, @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
mScanType = scanType;
mScanMode = scanMode;
mBleEnabled = bleEnabled;
+ mOffloadOnly = offloadOnly;
mWorkSource = workSource;
mScanFilters = scanFilters;
}
@@ -162,6 +174,13 @@
}
/**
+ * Returns if CHRE enabled for scanning.
+ */
+ public boolean isOffloadOnly() {
+ return mOffloadOnly;
+ }
+
+ /**
* Returns Scan Filters for this request.
*/
@NonNull
@@ -197,7 +216,13 @@
stringBuilder.append("Request[")
.append("scanType=").append(mScanType);
stringBuilder.append(", scanMode=").append(scanModeToString(mScanMode));
- stringBuilder.append(", enableBle=").append(mBleEnabled);
+ // TODO(b/286137024): Remove this when CTS R5 is rolled out.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ stringBuilder.append(", bleEnabled=").append(mBleEnabled);
+ stringBuilder.append(", offloadOnly=").append(mOffloadOnly);
+ } else {
+ stringBuilder.append(", enableBle=").append(mBleEnabled);
+ }
stringBuilder.append(", workSource=").append(mWorkSource);
stringBuilder.append(", scanFilters=").append(mScanFilters);
stringBuilder.append("]");
@@ -209,6 +234,7 @@
dest.writeInt(mScanType);
dest.writeInt(mScanMode);
dest.writeBoolean(mBleEnabled);
+ dest.writeBoolean(mOffloadOnly);
dest.writeTypedObject(mWorkSource, /* parcelableFlags= */0);
final int size = mScanFilters.size();
dest.writeInt(size);
@@ -224,6 +250,7 @@
return mScanType == otherRequest.mScanType
&& (mScanMode == otherRequest.mScanMode)
&& (mBleEnabled == otherRequest.mBleEnabled)
+ && (mOffloadOnly == otherRequest.mOffloadOnly)
&& (Objects.equals(mWorkSource, otherRequest.mWorkSource));
}
return false;
@@ -231,7 +258,7 @@
@Override
public int hashCode() {
- return Objects.hash(mScanType, mScanMode, mBleEnabled, mWorkSource);
+ return Objects.hash(mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource);
}
/** @hide **/
@@ -254,6 +281,7 @@
private @ScanMode int mScanMode;
private boolean mBleEnabled;
+ private boolean mOffloadOnly;
private WorkSource mWorkSource;
private List<ScanFilter> mScanFilters;
@@ -261,6 +289,7 @@
public Builder() {
mScanType = INVALID_SCAN_TYPE;
mBleEnabled = true;
+ mOffloadOnly = false;
mWorkSource = new WorkSource();
mScanFilters = new ArrayList<>();
}
@@ -301,6 +330,22 @@
}
/**
+ * By default, a scan request can be served by either offload or
+ * non-offload implementation, depending on the resource available in the device.
+ *
+ * A client can explicitly request a scan to be served by offload only.
+ * Before the request, the client should query the offload capability by
+ * using {@link NearbyManager#queryOffloadCapability(Executor, Consumer)}}. Otherwise,
+ * {@link ScanCallback#ERROR_UNSUPPORTED} will be returned on devices without
+ * offload capability.
+ */
+ @NonNull
+ public Builder setOffloadOnly(boolean offloadOnly) {
+ mOffloadOnly = offloadOnly;
+ return this;
+ }
+
+ /**
* Sets the work source to use for power attribution for this scan request. Defaults to
* empty work source, which implies the caller that sends the scan request will be used
* for power attribution.
@@ -355,7 +400,8 @@
Preconditions.checkState(isValidScanMode(mScanMode),
"invalid scan mode : " + mScanMode
+ ", scan mode must be one of ScanMode#SCAN_MODE_");
- return new ScanRequest(mScanType, mScanMode, mBleEnabled, mWorkSource, mScanFilters);
+ return new ScanRequest(
+ mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource, mScanFilters);
}
}
}
diff --git a/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl b/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl
new file mode 100644
index 0000000..8bef817
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+import android.nearby.OffloadCapability;
+
+/**
+ * Listener for offload queries.
+ *
+ * {@hide}
+ */
+oneway interface IOffloadCallback {
+ /** Invokes when ContextHub transaction completes. */
+ void onQueryComplete(in OffloadCapability capability);
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 8fdac87..9b32d69 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -16,9 +16,16 @@
package com.android.server.nearby;
+import android.os.Build;
import android.provider.DeviceConfig;
-import androidx.annotation.VisibleForTesting;
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
+
+import java.util.concurrent.Executors;
/**
* A utility class for encapsulating Nearby feature flag configurations.
@@ -26,33 +33,123 @@
public class NearbyConfiguration {
/**
- * Flag use to enable presence legacy broadcast.
+ * Flag used to enable presence legacy broadcast.
*/
public static final String NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY =
"nearby_enable_presence_broadcast_legacy";
+ /**
+ * Flag used to for minimum nano app version to make Nearby CHRE scan work.
+ */
+ public static final String NEARBY_MAINLINE_NANO_APP_MIN_VERSION =
+ "nearby_mainline_nano_app_min_version";
+ /**
+ * Flag used to allow test mode and customization.
+ */
+ public static final String NEARBY_SUPPORT_TEST_APP = "nearby_support_test_app";
+
+ /**
+ * Flag to control which version of DiscoveryProviderManager should be used.
+ */
+ public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
+ "nearby_refactor_discovery_manager";
+
+ private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
+
+ private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
+ private final Object mDeviceConfigLock = new Object();
+
+ @GuardedBy("mDeviceConfigLock")
private boolean mEnablePresenceBroadcastLegacy;
+ @GuardedBy("mDeviceConfigLock")
+ private int mNanoAppMinVersion;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mSupportTestApp;
+ @GuardedBy("mDeviceConfigLock")
+ private boolean mRefactorDiscoveryManager;
public NearbyConfiguration() {
- mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
- NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+ mDeviceConfigListener.start();
+ }
+ /**
+ * Returns the DeviceConfig namespace for Nearby. The {@link DeviceConfig#NAMESPACE_NEARBY} was
+ * added in UpsideDownCake, in Tiramisu, we use {@link DeviceConfig#NAMESPACE_TETHERING}.
+ */
+ public static String getNamespace() {
+ if (SdkLevel.isAtLeastU()) {
+ return DeviceConfig.NAMESPACE_NEARBY;
+ }
+ return DeviceConfig.NAMESPACE_TETHERING;
+ }
+
+ private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ private static int getDeviceConfigInt(final String name, final int defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Integer.parseInt(value) : defaultValue;
+ }
+
+ private static String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(getNamespace(), name);
}
/**
* Returns whether broadcasting legacy presence spec is enabled.
*/
public boolean isPresenceBroadcastLegacyEnabled() {
- return mEnablePresenceBroadcastLegacy;
+ synchronized (mDeviceConfigLock) {
+ return mEnablePresenceBroadcastLegacy;
+ }
}
- private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
- final String value = getDeviceConfigProperty(name);
- return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ public int getNanoAppMinVersion() {
+ synchronized (mDeviceConfigLock) {
+ return mNanoAppMinVersion;
+ }
}
- @VisibleForTesting
- protected String getDeviceConfigProperty(String name) {
- return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
+ /**
+ * @return {@code true} when in test mode and allows customization.
+ */
+ public boolean isTestAppSupported() {
+ synchronized (mDeviceConfigLock) {
+ return mSupportTestApp;
+ }
+ }
+
+ /**
+ * @return {@code true} if use {@link DiscoveryProviderManager} or use
+ * DiscoveryProviderManagerLegacy if {@code false}.
+ */
+ public boolean refactorDiscoveryManager() {
+ synchronized (mDeviceConfigLock) {
+ return mRefactorDiscoveryManager;
+ }
+ }
+
+ private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+ public void start() {
+ DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
+ Executors.newSingleThreadExecutor(), this);
+ onPropertiesChanged(DeviceConfig.getProperties(getNamespace()));
+ }
+
+ @Override
+ public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+ synchronized (mDeviceConfigLock) {
+ mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
+ NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+ mNanoAppMinVersion = getDeviceConfigInt(
+ NEARBY_MAINLINE_NANO_APP_MIN_VERSION, 0 /* defaultValue */);
+ mSupportTestApp = !IS_USER_BUILD && getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ mRefactorDiscoveryManager = getDeviceConfigBoolean(
+ NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+ }
+ }
}
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 1220104..3c183ec 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -28,6 +28,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.IBroadcastListener;
@@ -35,13 +36,16 @@
import android.nearby.IScanListener;
import android.nearby.NearbyManager;
import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.provider.BroadcastProviderManager;
-import com.android.server.nearby.provider.DiscoveryProviderManager;
+import com.android.server.nearby.managers.BroadcastProviderManager;
+import com.android.server.nearby.managers.DiscoveryManager;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
+import com.android.server.nearby.managers.DiscoveryProviderManagerLegacy;
+import com.android.server.nearby.presence.PresenceManager;
import com.android.server.nearby.util.identity.CallerIdentity;
import com.android.server.nearby.util.permissions.BroadcastPermissions;
import com.android.server.nearby.util.permissions.DiscoveryPermissions;
@@ -49,8 +53,12 @@
/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
public static final String TAG = "NearbyService";
+ // Sets to true to start BLE scan from PresenceManager for manual testing.
+ public static final Boolean MANUAL_TEST = false;
private final Context mContext;
+ private final PresenceManager mPresenceManager;
+ private final NearbyConfiguration mNearbyConfiguration;
private Injector mInjector;
private final BroadcastReceiver mBluetoothReceiver =
new BroadcastReceiver() {
@@ -69,14 +77,19 @@
}
}
};
- private DiscoveryProviderManager mProviderManager;
- private BroadcastProviderManager mBroadcastProviderManager;
+ private final DiscoveryManager mDiscoveryProviderManager;
+ private final BroadcastProviderManager mBroadcastProviderManager;
public NearbyService(Context context) {
mContext = context;
mInjector = new SystemInjector(context);
- mProviderManager = new DiscoveryProviderManager(context, mInjector);
mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
+ mPresenceManager = new PresenceManager(context);
+ mNearbyConfiguration = new NearbyConfiguration();
+ mDiscoveryProviderManager =
+ mNearbyConfiguration.refactorDiscoveryManager()
+ ? new DiscoveryProviderManager(context, mInjector)
+ : new DiscoveryProviderManagerLegacy(context, mInjector);
}
@VisibleForTesting
@@ -93,10 +106,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- if (mProviderManager.registerScanListener(scanRequest, listener, identity)) {
- return NearbyManager.ScanStatus.SUCCESS;
- }
- return NearbyManager.ScanStatus.ERROR;
+ return mDiscoveryProviderManager.registerScanListener(scanRequest, listener, identity);
}
@Override
@@ -107,7 +117,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- mProviderManager.unregisterScanListener(listener);
+ mDiscoveryProviderManager.unregisterScanListener(listener);
}
@Override
@@ -133,6 +143,11 @@
mBroadcastProviderManager.stopBroadcast(listener);
}
+ @Override
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mDiscoveryProviderManager.queryOffloadCapability(callback);
+ }
+
/**
* Called by the service initializer.
*
@@ -146,15 +161,21 @@
}
break;
case PHASE_BOOT_COMPLETED:
+ // mInjector needs to be initialized before mProviderManager.
if (mInjector instanceof SystemInjector) {
// The nearby service must be functioning after this boot phase.
((SystemInjector) mInjector).initializeBluetoothAdapter();
// Initialize ContextManager for CHRE scan.
- ((SystemInjector) mInjector).initializeContextHubManagerAdapter();
+ ((SystemInjector) mInjector).initializeContextHubManager();
}
+ mDiscoveryProviderManager.init();
mContext.registerReceiver(
mBluetoothReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
+ // Only enable for manual Presence test on device.
+ if (MANUAL_TEST) {
+ mPresenceManager.initiate();
+ }
break;
}
}
@@ -165,16 +186,18 @@
* throw a {@link SecurityException}.
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
- private static void enforceBluetoothPrivilegedPermission(Context context) {
- context.enforceCallingOrSelfPermission(
- android.Manifest.permission.BLUETOOTH_PRIVILEGED,
- "Need BLUETOOTH PRIVILEGED permission");
+ private void enforceBluetoothPrivilegedPermission(Context context) {
+ if (!mNearbyConfiguration.isTestAppSupported()) {
+ context.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH PRIVILEGED permission");
+ }
}
private static final class SystemInjector implements Injector {
private final Context mContext;
@Nullable private BluetoothAdapter mBluetoothAdapter;
- @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
+ @Nullable private ContextHubManager mContextHubManager;
@Nullable private AppOpsManager mAppOpsManager;
SystemInjector(Context context) {
@@ -189,8 +212,8 @@
@Override
@Nullable
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
- return mContextHubManagerAdapter;
+ public ContextHubManager getContextHubManager() {
+ return mContextHubManager;
}
@Override
@@ -210,15 +233,13 @@
mBluetoothAdapter = manager.getAdapter();
}
- synchronized void initializeContextHubManagerAdapter() {
- if (mContextHubManagerAdapter != null) {
+ synchronized void initializeContextHubManager() {
+ if (mContextHubManager != null) {
return;
}
- ContextHubManager manager = mContext.getSystemService(ContextHubManager.class);
- if (manager == null) {
- return;
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONTEXT_HUB)) {
+ mContextHubManager = mContext.getSystemService(ContextHubManager.class);
}
- mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
}
synchronized void initializeAppOpsManager() {
diff --git a/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java b/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java
new file mode 100644
index 0000000..00d1570
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.util.Log;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * A cancelable alarm with a name. This is a simple wrapper around the logic for posting a runnable
+ * on a scheduled executor service and (possibly) later canceling it.
+ */
+public class CancelableAlarm {
+
+ private static final String TAG = "NearbyCancelableAlarm";
+
+ private final String mName;
+ private final Runnable mRunnable;
+ private final long mDelayMillis;
+ private final ScheduledExecutorService mExecutor;
+ private final boolean mIsRecurring;
+
+ // The future containing the alarm.
+ private volatile ScheduledFuture<?> mFuture;
+
+ private CancellationFlag mCancellationFlag;
+
+ private CancelableAlarm(
+ String name,
+ Runnable runnable,
+ long delayMillis,
+ ScheduledExecutorService executor,
+ boolean isRecurring) {
+ this.mName = name;
+ this.mRunnable = runnable;
+ this.mDelayMillis = delayMillis;
+ this.mExecutor = executor;
+ this.mIsRecurring = isRecurring;
+ }
+
+ /**
+ * Creates an alarm.
+ *
+ * @param name the task name
+ * @param runnable command the task to execute
+ * @param delayMillis delay the time from now to delay execution
+ * @param executor the executor that schedules commands to run
+ */
+ public static CancelableAlarm createSingleAlarm(
+ String name,
+ Runnable runnable,
+ long delayMillis,
+ ScheduledExecutorService executor) {
+ CancelableAlarm cancelableAlarm =
+ new CancelableAlarm(name, runnable, delayMillis, executor, /* isRecurring= */
+ false);
+ cancelableAlarm.scheduleExecutor();
+ return cancelableAlarm;
+ }
+
+ /**
+ * Creates a recurring alarm.
+ *
+ * @param name the task name
+ * @param runnable command the task to execute
+ * @param delayMillis delay the time from now to delay execution
+ * @param executor the executor that schedules commands to run
+ */
+ public static CancelableAlarm createRecurringAlarm(
+ String name,
+ Runnable runnable,
+ long delayMillis,
+ ScheduledExecutorService executor) {
+ CancelableAlarm cancelableAlarm =
+ new CancelableAlarm(name, runnable, delayMillis, executor, /* isRecurring= */ true);
+ cancelableAlarm.scheduleExecutor();
+ return cancelableAlarm;
+ }
+
+ // A reference to "this" should generally not be passed to another class within the constructor
+ // as it may not have completed being constructed.
+ private void scheduleExecutor() {
+ this.mFuture = mExecutor.schedule(this::processAlarm, mDelayMillis, MILLISECONDS);
+ // For tests to pass (NearbySharingChimeraServiceTest) the Cancellation Flag must come
+ // after the
+ // executor. Doing so prevents the test code from running the callback immediately.
+ this.mCancellationFlag = new CancellationFlag();
+ }
+
+ /**
+ * Cancels the pending alarm.
+ *
+ * @return true if the alarm was canceled, or false if there was a problem canceling the alarm.
+ */
+ public boolean cancel() {
+ mCancellationFlag.cancel();
+ try {
+ return mFuture.cancel(/* mayInterruptIfRunning= */ true);
+ } finally {
+ Log.v(TAG, "Canceled " + mName + " alarm");
+ }
+ }
+
+ private void processAlarm() {
+ if (mCancellationFlag.isCancelled()) {
+ Log.v(TAG, "Ignoring " + mName + " alarm because it has previously been canceled");
+ return;
+ }
+
+ Log.v(TAG, "Running " + mName + " alarm");
+ mRunnable.run();
+ if (mIsRecurring) {
+ this.mFuture = mExecutor.schedule(this::processAlarm, mDelayMillis, MILLISECONDS);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java b/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java
new file mode 100644
index 0000000..f0bb075
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import android.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A cancellation flag to mark an operation has been cancelled and should be cleaned up as soon as
+ * possible.
+ */
+public class CancellationFlag {
+
+ private final Set<OnCancelListener> mListeners = new ArraySet<>();
+ private final AtomicBoolean mIsCancelled = new AtomicBoolean();
+
+ public CancellationFlag() {
+ this(false);
+ }
+
+ public CancellationFlag(boolean isCancelled) {
+ this.mIsCancelled.set(isCancelled);
+ }
+
+ /** Set the flag as cancelled. */
+ public void cancel() {
+ if (mIsCancelled.getAndSet(true)) {
+ // Someone already cancelled. Return immediately.
+ return;
+ }
+
+ // Don't invoke OnCancelListener#onCancel inside the synchronization block, as it makes
+ // deadlocks more likely.
+ Set<OnCancelListener> clonedListeners;
+ synchronized (this) {
+ clonedListeners = new ArraySet<>(mListeners);
+ }
+ for (OnCancelListener listener : clonedListeners) {
+ listener.onCancel();
+ }
+ }
+
+ /** Returns {@code true} if the flag has been set to cancelled. */
+ public synchronized boolean isCancelled() {
+ return mIsCancelled.get();
+ }
+
+ /** Returns the flag as an {@link AtomicBoolean} object. */
+ public synchronized AtomicBoolean asAtomicBoolean() {
+ return mIsCancelled;
+ }
+
+ /** Registers a {@link OnCancelListener} to listen to cancel() event. */
+ public synchronized void registerOnCancelListener(OnCancelListener listener) {
+ mListeners.add(listener);
+ }
+
+ /**
+ * Unregisters a {@link OnCancelListener} that was previously registed through {@link
+ * #registerOnCancelListener(OnCancelListener)}.
+ */
+ public synchronized void unregisterOnCancelListener(OnCancelListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /** Listens to {@link CancellationFlag#cancel()}. */
+ public interface OnCancelListener {
+ /**
+ * When CancellationFlag is canceled.
+ */
+ void onCancel();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/injector/Injector.java b/nearby/service/java/com/android/server/nearby/injector/Injector.java
index 57784a9..3152ee6 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -18,6 +18,7 @@
import android.app.AppOpsManager;
import android.bluetooth.BluetoothAdapter;
+import android.hardware.location.ContextHubManager;
/**
* Nearby dependency injector. To be used for accessing various Nearby class instances and as a
@@ -29,7 +30,7 @@
BluetoothAdapter getBluetoothAdapter();
/** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
- ContextHubManagerAdapter getContextHubManagerAdapter();
+ ContextHubManager getContextHubManager();
/** Get the AppOpsManager to control access. */
AppOpsManager getAppOpsManager();
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
similarity index 70%
rename from nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
rename to nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 3fffda5..024bff8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
+import android.annotation.Nullable;
import android.content.Context;
import android.nearby.BroadcastCallback;
import android.nearby.BroadcastRequest;
@@ -27,7 +28,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.Advertisement;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.presence.FastAdvertisement;
+import com.android.server.nearby.provider.BleBroadcastProvider;
import com.android.server.nearby.util.ForegroundThread;
import java.util.concurrent.Executor;
@@ -66,10 +70,12 @@
public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
synchronized (mLock) {
mExecutor.execute(() -> {
- NearbyConfiguration configuration = new NearbyConfiguration();
- if (!configuration.isPresenceBroadcastLegacyEnabled()) {
- reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
- return;
+ if (!mNearbyConfiguration.isTestAppSupported()) {
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ if (!configuration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
}
if (broadcastRequest.getType() != BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE) {
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
@@ -77,25 +83,38 @@
}
PresenceBroadcastRequest presenceBroadcastRequest =
(PresenceBroadcastRequest) broadcastRequest;
- if (presenceBroadcastRequest.getVersion() != BroadcastRequest.PRESENCE_VERSION_V0) {
+ Advertisement advertisement = getAdvertisement(presenceBroadcastRequest);
+ if (advertisement == null) {
+ Log.e(TAG, "Failed to start broadcast because broadcastRequest is illegal.");
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
- FastAdvertisement fastAdvertisement = FastAdvertisement.createFromRequest(
- presenceBroadcastRequest);
- byte[] advertisementPackets = fastAdvertisement.toBytes();
mBroadcastListener = listener;
- mBleBroadcastProvider.start(advertisementPackets, this);
+ mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
+ advertisement.toBytes(), this);
});
}
}
+ @Nullable
+ private Advertisement getAdvertisement(PresenceBroadcastRequest request) {
+ switch (request.getVersion()) {
+ case BroadcastRequest.PRESENCE_VERSION_V0:
+ return FastAdvertisement.createFromRequest(request);
+ case BroadcastRequest.PRESENCE_VERSION_V1:
+ return ExtendedAdvertisement.createFromRequest(request);
+ default:
+ return null;
+ }
+ }
+
/**
* Stops the nearby broadcast.
*/
public void stopBroadcast(IBroadcastListener listener) {
synchronized (mLock) {
- if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+ if (!mNearbyConfiguration.isTestAppSupported()
+ && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
new file mode 100644
index 0000000..c9b9a43
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import android.nearby.IScanListener;
+import android.nearby.NearbyManager;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+/**
+ * Interface added for flagging DiscoveryProviderManager refactor. After the
+ * nearby_refactor_discovery_manager flag is fully rolled out, this can be deleted.
+ */
+public interface DiscoveryManager {
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity);
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ void unregisterScanListener(IScanListener listener);
+
+ /** Query offload capability in a device. */
+ void queryOffloadCapability(IOffloadCallback callback);
+
+ /** Called after boot completed. */
+ void init();
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
new file mode 100644
index 0000000..0c41426
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.registration.DiscoveryRegistration;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> implements
+ AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
+
+ protected final Object mLock = new Object();
+ @VisibleForTesting
+ @Nullable
+ final ChreDiscoveryProvider mChreDiscoveryProvider;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ private final Injector mInjector;
+ private final Executor mExecutor;
+
+ public DiscoveryProviderManager(Context context, Injector injector) {
+ Log.v(TAG, "DiscoveryProviderManager: ");
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ mExecutor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
+ new ChreCommunication(injector, mContext, mExecutor), mExecutor);
+ mInjector = injector;
+ }
+
+ @VisibleForTesting
+ DiscoveryProviderManager(Context context, Executor executor, Injector injector,
+ BleDiscoveryProvider bleDiscoveryProvider,
+ ChreDiscoveryProvider chreDiscoveryProvider) {
+ mContext = context;
+ mExecutor = executor;
+ mInjector = injector;
+ mBleDiscoveryProvider = bleDiscoveryProvider;
+ mChreDiscoveryProvider = chreDiscoveryProvider;
+ }
+
+ private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
+ for (ScanFilter scanFilter : scanFilters) {
+ List<DataElement> dataElements =
+ ((PresenceScanFilter) scanFilter).getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+ continue;
+ }
+ byte[] scanModeValue = dataElement.getValue();
+ if (scanModeValue == null || scanModeValue.length == 0) {
+ break;
+ }
+ if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+ return true;
+ }
+ }
+
+ }
+ return false;
+ }
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mMultiplexerLock) {
+ Log.d(TAG, "Found device" + nearbyDevice);
+ deliverToListeners(registration -> {
+ try {
+ return registration.onNearbyDeviceDiscovered(nearbyDevice);
+ } catch (Exception e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report callback.", e);
+ return null;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onError(int errorCode) {
+ synchronized (mMultiplexerLock) {
+ Log.e(TAG, "Error found during scanning.");
+ deliverToListeners(registration -> {
+ try {
+ return registration.reportError(errorCode);
+ } catch (Exception e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ return null;
+ }
+ });
+ }
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity) {
+ DiscoveryRegistration registration = new DiscoveryRegistration(this, scanRequest, listener,
+ mExecutor, callerIdentity, mMultiplexerLock, mInjector.getAppOpsManager());
+ synchronized (mMultiplexerLock) {
+ putRegistration(listener.asBinder(), registration);
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+
+ @Override
+ public void onRegister() {
+ Log.v(TAG, "Registering the DiscoveryProviderManager.");
+ startProviders();
+ }
+
+ @Override
+ public void onUnregister() {
+ Log.v(TAG, "Unregistering the DiscoveryProviderManager.");
+ stopProviders();
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ Log.v(TAG, "Unregister scan listener");
+ synchronized (mMultiplexerLock) {
+ removeRegistration(listener.asBinder());
+ }
+ // TODO(b/221082271): updates the scan with reduced filters.
+ }
+
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ }
+
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders() {
+ synchronized (mMultiplexerLock) {
+ if (!mMerged.getMediums().contains(MergedDiscoveryRequest.Medium.BLE)) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ Set<ScanFilter> scanFilters = mMerged.getScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ Log.v(TAG, "startProviders: chreOnly " + chreOnly + " chreAvailable " + chreAvailable);
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG,
+ "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (mMerged.getScanTypes().contains(SCAN_TYPE_NEARBY_PRESENCE)) {
+ startChreProvider(scanFilters);
+ return true;
+ }
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void startBleProvider(Set<ScanFilter> scanFilters) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ mBleDiscoveryProvider.getController().setProviderScanFilters(
+ new ArrayList<>(scanFilters));
+ mBleDiscoveryProvider.getController().start();
+ }
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mMultiplexerLock")
+ void startChreProvider(Collection<ScanFilter> scanFilters) {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning. " + mMerged);
+ mChreDiscoveryProvider.getController().setProviderScanFilters(new ArrayList<>(scanFilters));
+ mChreDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ mChreDiscoveryProvider.getController().start();
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ stopChreProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ protected void stopChreProvider() {
+ mChreDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ void invalidateProviderScanMode() {
+ if (mBleDiscoveryProvider.getController().isStarted()) {
+ synchronized (mMultiplexerLock) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ }
+ } else {
+ Log.d(TAG, "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ }
+ }
+
+ @Override
+ public MergedDiscoveryRequest mergeRegistrations(
+ @NonNull Collection<DiscoveryRegistration> registrations) {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ int scanMode = ScanRequest.SCAN_MODE_NO_POWER;
+ for (DiscoveryRegistration registration : registrations) {
+ builder.addActions(registration.getActions());
+ builder.addScanFilters(registration.getPresenceScanFilters());
+ Log.d(TAG,
+ "mergeRegistrations: type is " + registration.getScanRequest().getScanType());
+ builder.addScanType(registration.getScanRequest().getScanType());
+ if (registration.getScanRequest().isBleEnabled()) {
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ }
+ int requestScanMode = registration.getScanRequest().getScanMode();
+ if (scanMode < requestScanMode) {
+ scanMode = requestScanMode;
+ }
+ }
+ builder.setScanMode(scanMode);
+ return builder.build();
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ invalidateProviderScanMode();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
new file mode 100644
index 0000000..4b76eba
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.metrics.NearbyMetrics;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.PrivacyFilter;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import java.util.ArrayList;
+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.Executors;
+import java.util.stream.Collectors;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManagerLegacy implements AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
+
+ protected final Object mLock = new Object();
+ @VisibleForTesting
+ @Nullable
+ final ChreDiscoveryProvider mChreDiscoveryProvider;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ private final Injector mInjector;
+ @ScanRequest.ScanMode
+ private int mScanMode;
+ @GuardedBy("mLock")
+ private final Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
+
+ public DiscoveryProviderManagerLegacy(Context context, Injector injector) {
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ Executor executor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider =
+ new ChreDiscoveryProvider(
+ mContext, new ChreCommunication(injector, mContext, executor), executor);
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mInjector = injector;
+ Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
+ }
+
+ @VisibleForTesting
+ DiscoveryProviderManagerLegacy(Context context, Injector injector,
+ BleDiscoveryProvider bleDiscoveryProvider,
+ ChreDiscoveryProvider chreDiscoveryProvider,
+ Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+ mContext = context;
+ mInjector = injector;
+ mBleDiscoveryProvider = bleDiscoveryProvider;
+ mChreDiscoveryProvider = chreDiscoveryProvider;
+ mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+ }
+
+ private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+ for (ScanFilter scanFilter : scanFilters) {
+ List<DataElement> dataElements =
+ ((PresenceScanFilter) scanFilter).getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+ continue;
+ }
+ byte[] scanModeValue = dataElement.getValue();
+ if (scanModeValue == null || scanModeValue.length == 0) {
+ break;
+ }
+ if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+ return true;
+ }
+ }
+
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static boolean presenceFilterMatches(
+ NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
+ if (scanFilters.isEmpty()) {
+ return true;
+ }
+ PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+ for (ScanFilter scanFilter : scanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ if (discoveryResult.matches(presenceScanFilter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mLock) {
+ AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ CallerIdentity callerIdentity = record.getCallerIdentity();
+ if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+ appOpsManager, callerIdentity)) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results");
+ try {
+ record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ }
+ return;
+ }
+
+ if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType()
+ == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+ Log.d(TAG, "presence filter does not match for "
+ + "the scanned Presence Device");
+ continue;
+ }
+ }
+ try {
+ record.getScanListener()
+ .onDiscovered(
+ PrivacyFilter.filter(
+ record.getScanRequest().getScanType(), nearbyDevice));
+ NearbyMetrics.logScanDeviceDiscovered(
+ record.hashCode(), record.getScanRequest(), nearbyDevice);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onError(int errorCode) {
+ synchronized (mLock) {
+ AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ CallerIdentity callerIdentity = record.getCallerIdentity();
+ if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+ appOpsManager, callerIdentity)) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results");
+ try {
+ record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+ }
+ return;
+ }
+
+ try {
+ record.getScanListener().onError(errorCode);
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+ }
+ }
+ }
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ CallerIdentity callerIdentity) {
+ synchronized (mLock) {
+ ScanListenerDeathRecipient deathRecipient = (listener != null)
+ ? new ScanListenerDeathRecipient(listener) : null;
+ IBinder listenerBinder = listener.asBinder();
+ if (listenerBinder != null && deathRecipient != null) {
+ try {
+ listenerBinder.linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ throw new IllegalArgumentException("Can't link to scan listener's death");
+ }
+ }
+ if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
+ ScanRequest savedScanRequest =
+ mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
+ if (scanRequest.equals(savedScanRequest)) {
+ Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+ ScanListenerRecord scanListenerRecord =
+ new ScanListenerRecord(scanRequest, listener, callerIdentity, deathRecipient);
+
+ mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
+ Boolean started = startProviders(scanRequest);
+ if (started == null) {
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ return NearbyManager.ScanStatus.UNKNOWN;
+ }
+ if (!started) {
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ return NearbyManager.ScanStatus.ERROR;
+ }
+ NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
+ if (mScanMode < scanRequest.getScanMode()) {
+ mScanMode = scanRequest.getScanMode();
+ invalidateProviderScanMode();
+ }
+ return NearbyManager.ScanStatus.SUCCESS;
+ }
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ IBinder listenerBinder = listener.asBinder();
+ synchronized (mLock) {
+ if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
+ Log.w(
+ TAG,
+ "Cannot unregister the scanRequest because the request is never "
+ + "registered.");
+ return;
+ }
+
+ ScanListenerRecord removedRecord =
+ mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ ScanListenerDeathRecipient deathRecipient = removedRecord.getDeathRecipient();
+ if (listenerBinder != null && deathRecipient != null) {
+ listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
+ }
+ Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+ NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
+ if (mScanTypeScanListenerRecordMap.isEmpty()) {
+ Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+ + "scan listener registered.");
+ stopProviders();
+ return;
+ }
+
+ // TODO(b/221082271): updates the scan with reduced filters.
+
+ // Removes current highest scan mode requested and sets the next highest scan mode.
+ if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
+ Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
+ + "because the highest scan mode listener was unregistered.");
+ @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
+ // find the next highest scan mode;
+ for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
+ @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
+ if (scanMode > highestScanModeRequested) {
+ highestScanModeRequested = scanMode;
+ }
+ }
+ if (mScanMode != highestScanModeRequested) {
+ mScanMode = highestScanModeRequested;
+ invalidateProviderScanMode();
+ }
+ }
+ }
+ }
+
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ }
+
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders(ScanRequest scanRequest) {
+ if (!scanRequest.isBleEnabled()) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ List<ScanFilter> scanFilters = getPresenceScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ startChreProvider(scanFilters);
+ return true;
+ }
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ private void startBleProvider(List<ScanFilter> scanFilters) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mBleDiscoveryProvider.getController().start();
+ }
+ }
+
+ @VisibleForTesting
+ void startChreProvider(List<ScanFilter> scanFilters) {
+ Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mChreDiscoveryProvider.getController().start();
+ }
+
+ private List<ScanFilter> getPresenceScanFilters() {
+ synchronized (mLock) {
+ List<ScanFilter> scanFilters = new ArrayList();
+ for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+ List<ScanFilter> presenceFilters =
+ record.getScanRequest().getScanFilters().stream()
+ .filter(
+ scanFilter ->
+ scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
+ .collect(Collectors.toList());
+ scanFilters.addAll(presenceFilters);
+ }
+ return scanFilters;
+ }
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ stopChreProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ protected void stopChreProvider() {
+ mChreDiscoveryProvider.getController().stop();
+ }
+
+ @VisibleForTesting
+ void invalidateProviderScanMode() {
+ if (mBleDiscoveryProvider.getController().isStarted()) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ } else {
+ Log.d(
+ TAG,
+ "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ }
+ }
+
+ @VisibleForTesting
+ static class ScanListenerRecord {
+
+ private final ScanRequest mScanRequest;
+
+ private final IScanListener mScanListener;
+
+ private final CallerIdentity mCallerIdentity;
+
+ private final ScanListenerDeathRecipient mDeathRecipient;
+
+ ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
+ CallerIdentity callerIdentity, ScanListenerDeathRecipient deathRecipient) {
+ mScanListener = iScanListener;
+ mScanRequest = scanRequest;
+ mCallerIdentity = callerIdentity;
+ mDeathRecipient = deathRecipient;
+ }
+
+ IScanListener getScanListener() {
+ return mScanListener;
+ }
+
+ ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ CallerIdentity getCallerIdentity() {
+ return mCallerIdentity;
+ }
+
+ ScanListenerDeathRecipient getDeathRecipient() {
+ return mDeathRecipient;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ScanListenerRecord) {
+ ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
+ return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
+ && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mScanListener, mScanRequest);
+ }
+ }
+
+ /**
+ * Class to make listener unregister after the binder is dead.
+ */
+ public class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
+ public IScanListener listener;
+
+ ScanListenerDeathRecipient(IScanListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, "Binder is dead - unregistering scan listener");
+ unregisterScanListener(listener);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
new file mode 100644
index 0000000..a6a9388
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration.ListenerOperation;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A simplified class based on {@link com.android.server.location.listeners.ListenerMultiplexer}.
+ * It is a base class to multiplex broadcast and discovery events to multiple listener
+ * registrations. Every listener is represented by a registration object which stores all required
+ * state for a listener.
+ * Registrations will be merged to one request for the service to operate.
+ *
+ * @param <TListener> callback type for clients
+ * @param <TRegistration> child of {@link BinderListenerRegistration}
+ * @param <TMergedRegistration> merged registration type
+ */
+public abstract class ListenerMultiplexer<TListener,
+ TRegistration extends BinderListenerRegistration<TListener>, TMergedRegistration> {
+
+ /**
+ * The lock object used by the multiplexer. Acquiring this lock allows for multiple operations
+ * on the multiplexer to be completed atomically. Otherwise, it is not required to hold this
+ * lock. This lock is held while invoking all lifecycle callbacks on both the multiplexer and
+ * any registrations.
+ */
+ public final Object mMultiplexerLock = new Object();
+
+ @GuardedBy("mMultiplexerLock")
+ final ArrayMap<IBinder, TRegistration> mRegistrations = new ArrayMap<>();
+
+ // this is really @NonNull in many ways, but we explicitly null this out to allow for GC when
+ // not
+ // in use, so we can't annotate with @NonNull
+ @GuardedBy("mMultiplexerLock")
+ public TMergedRegistration mMerged;
+
+ /**
+ * Invoked when the multiplexer goes from having no registrations to having some registrations.
+ * This is a convenient entry point for registering listeners, etc, which only need to be
+ * present
+ * while there are any registrations. Invoked while holding the multiplexer's internal lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public void onRegister() {
+ Log.v(TAG, "ListenerMultiplexer registered.");
+ }
+
+ /**
+ * Invoked when the multiplexer goes from having some registrations to having no registrations.
+ * This is a convenient entry point for unregistering listeners, etc, which only need to be
+ * present while there are any registrations. Invoked while holding the multiplexer's internal
+ * lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public void onUnregister() {
+ Log.v(TAG, "ListenerMultiplexer unregistered.");
+ }
+
+ /**
+ * Puts a new registration with the given key, replacing any previous registration under the
+ * same key. This method cannot be called to put a registration re-entrantly.
+ */
+ public final void putRegistration(@NonNull IBinder key, @NonNull TRegistration registration) {
+ Objects.requireNonNull(key);
+ Objects.requireNonNull(registration);
+ synchronized (mMultiplexerLock) {
+ boolean wasEmpty = mRegistrations.isEmpty();
+
+ int index = mRegistrations.indexOfKey(key);
+ if (index > 0) {
+ BinderListenerRegistration<TListener> oldRegistration = mRegistrations.valueAt(
+ index);
+ oldRegistration.onUnregister();
+ mRegistrations.setValueAt(index, registration);
+ } else {
+ mRegistrations.put(key, registration);
+ }
+
+ registration.onRegister();
+ onRegistrationsUpdated();
+ if (wasEmpty) {
+ onRegister();
+ }
+ }
+ }
+
+ /**
+ * Removes the registration with the given key.
+ */
+ public final void removeRegistration(IBinder key) {
+ synchronized (mMultiplexerLock) {
+ int index = mRegistrations.indexOfKey(key);
+ if (index < 0) {
+ return;
+ }
+
+ removeRegistration(index);
+ }
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void removeRegistration(int index) {
+ TRegistration registration = mRegistrations.valueAt(index);
+
+ registration.onUnregister();
+ mRegistrations.removeAt(index);
+
+ onRegistrationsUpdated();
+
+ if (mRegistrations.isEmpty()) {
+ onUnregister();
+ }
+ }
+
+ /**
+ * Invoked when a registration is added, removed, or replaced. Invoked while holding the
+ * multiplexer's internal lock.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public final void onRegistrationsUpdated() {
+ TMergedRegistration newMerged = mergeRegistrations(mRegistrations.values());
+ if (newMerged.equals(mMerged)) {
+ return;
+ }
+ mMerged = newMerged;
+ onMergedRegistrationsUpdated();
+ }
+
+ /**
+ * Called in order to generate a merged registration from the given set of active registrations.
+ * The list of registrations will never be empty. If the resulting merged registration is equal
+ * to the currently registered merged registration, nothing further will happen. If the merged
+ * registration differs,{@link #onMergedRegistrationsUpdated()} will be invoked with the new
+ * merged registration so that the backing service can be updated.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public abstract TMergedRegistration mergeRegistrations(
+ @NonNull Collection<TRegistration> registrations);
+
+ /**
+ * The operation that the manager wants to handle when there is an update for the merged
+ * registration.
+ */
+ @GuardedBy("mMultiplexerLock")
+ public abstract void onMergedRegistrationsUpdated();
+
+ protected final void deliverToListeners(
+ Function<TRegistration, ListenerOperation<TListener>> function) {
+ synchronized (mMultiplexerLock) {
+ final int size = mRegistrations.size();
+ for (int i = 0; i < size; i++) {
+ TRegistration registration = mRegistrations.valueAt(i);
+ BinderListenerRegistration.ListenerOperation<TListener> operation = function.apply(
+ registration);
+ if (operation != null) {
+ registration.executeOperation(operation);
+ }
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
new file mode 100644
index 0000000..dcfb602
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import android.annotation.IntDef;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Collection;
+import java.util.Set;
+
+/** Internal discovery request to {@link DiscoveryProviderManager} and providers */
+public class MergedDiscoveryRequest {
+
+ private static final MergedDiscoveryRequest EMPTY_REQUEST = new MergedDiscoveryRequest(
+ /* scanMode= */ ScanRequest.SCAN_MODE_NO_POWER,
+ /* scanTypes= */ ImmutableSet.of(),
+ /* actions= */ ImmutableSet.of(),
+ /* scanFilters= */ ImmutableSet.of(),
+ /* mediums= */ ImmutableSet.of());
+ @ScanRequest.ScanMode
+ private final int mScanMode;
+ private final Set<Integer> mScanTypes;
+ private final Set<Integer> mActions;
+ private final Set<ScanFilter> mScanFilters;
+ private final Set<Integer> mMediums;
+
+ private MergedDiscoveryRequest(@ScanRequest.ScanMode int scanMode, Set<Integer> scanTypes,
+ Set<Integer> actions, Set<ScanFilter> scanFilters, Set<Integer> mediums) {
+ mScanMode = scanMode;
+ mScanTypes = scanTypes;
+ mActions = actions;
+ mScanFilters = scanFilters;
+ mMediums = mediums;
+ }
+
+ /**
+ * Returns an empty discovery request.
+ *
+ * <p>The empty request is used as the default request when the discovery engine is enabled,
+ * but
+ * there is no request yet. It's also used to notify the discovery engine all clients have
+ * removed
+ * their requests.
+ */
+ public static MergedDiscoveryRequest empty() {
+ return EMPTY_REQUEST;
+ }
+
+ /** Returns the priority of the request */
+ @ScanRequest.ScanMode
+ public final int getScanMode() {
+ return mScanMode;
+ }
+
+ /** Returns all requested scan types. */
+ public ImmutableSet<Integer> getScanTypes() {
+ return ImmutableSet.copyOf(mScanTypes);
+ }
+
+ /** Returns the actions of the request */
+ public ImmutableSet<Integer> getActions() {
+ return ImmutableSet.copyOf(mActions);
+ }
+
+ /** Returns the scan filters of the request */
+ public ImmutableSet<ScanFilter> getScanFilters() {
+ return ImmutableSet.copyOf(mScanFilters);
+ }
+
+ /** Returns the enabled scan mediums */
+ public ImmutableSet<Integer> getMediums() {
+ return ImmutableSet.copyOf(mMediums);
+ }
+
+ /**
+ * The medium where the broadcast request should be sent.
+ *
+ * @hide
+ */
+ @IntDef({Medium.BLE})
+ public @interface Medium {
+ int BLE = 1;
+ }
+
+ /** Builder for {@link MergedDiscoveryRequest}. */
+ public static class Builder {
+ private final Set<Integer> mScanTypes;
+ private final Set<Integer> mActions;
+ private final Set<ScanFilter> mScanFilters;
+ private final Set<Integer> mMediums;
+ @ScanRequest.ScanMode
+ private int mScanMode;
+
+ public Builder() {
+ mScanMode = ScanRequest.SCAN_MODE_NO_POWER;
+ mScanTypes = new ArraySet<>();
+ mActions = new ArraySet<>();
+ mScanFilters = new ArraySet<>();
+ mMediums = new ArraySet<>();
+ }
+
+ /**
+ * Sets the priority for the engine request.
+ */
+ public Builder setScanMode(@ScanRequest.ScanMode int scanMode) {
+ mScanMode = scanMode;
+ return this;
+ }
+
+ /**
+ * Adds scan type to the request.
+ */
+ public Builder addScanType(@ScanRequest.ScanType int type) {
+ mScanTypes.add(type);
+ return this;
+ }
+
+ /** Add actions to the request. */
+ public Builder addActions(Collection<Integer> actions) {
+ mActions.addAll(actions);
+ return this;
+ }
+
+ /** Add actions to the request. */
+ public Builder addScanFilters(Collection<ScanFilter> scanFilters) {
+ mScanFilters.addAll(scanFilters);
+ return this;
+ }
+
+ /**
+ * Add mediums to the request.
+ */
+ public Builder addMedium(@Medium int medium) {
+ mMediums.add(medium);
+ return this;
+ }
+
+ /** Builds an instance of {@link MergedDiscoveryRequest}. */
+ public MergedDiscoveryRequest build() {
+ return new MergedDiscoveryRequest(mScanMode, mScanTypes, mActions, mScanFilters,
+ mMediums);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
new file mode 100644
index 0000000..4aaa08f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A listener registration object which holds data associated with the listener, such as an optional
+ * request, and an executor responsible for listener invocations. Key is the IBinder.
+ *
+ * @param <TListener> listener for the callback
+ */
+public abstract class BinderListenerRegistration<TListener> implements IBinder.DeathRecipient {
+
+ private final AtomicBoolean mRemoved = new AtomicBoolean(false);
+ private final Executor mExecutor;
+ private final Object mListenerLock = new Object();
+ @Nullable
+ TListener mListener;
+ @Nullable
+ private final IBinder mKey;
+
+ public BinderListenerRegistration(IBinder key, Executor executor, TListener listener) {
+ this.mKey = key;
+ this.mExecutor = executor;
+ this.mListener = listener;
+ }
+
+ /**
+ * Must be implemented to return the
+ * {@link com.android.server.nearby.managers.ListenerMultiplexer} this registration is
+ * registered
+ * with. Often this is easiest to accomplish by defining registration subclasses as non-static
+ * inner classes of the multiplexer they are to be used with.
+ */
+ public abstract ListenerMultiplexer<TListener, ?
+ extends BinderListenerRegistration<TListener>, ?> getOwner();
+
+ public final IBinder getBinder() {
+ return mKey;
+ }
+
+ public final Executor getExecutor() {
+ return mExecutor;
+ }
+
+ /**
+ * Called when the registration is put in the Multiplexer.
+ */
+ public void onRegister() {
+ try {
+ getBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ remove();
+ }
+ }
+
+ /**
+ * Called when the registration is removed in the Multiplexer.
+ */
+ public void onUnregister() {
+ this.mListener = null;
+ try {
+ getBinder().unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ Log.w(TAG, "failed to unregister binder death listener", e);
+ }
+ }
+
+ /**
+ * Removes this registration. All pending listener invocations will fail.
+ *
+ * <p>Does nothing if invoked before {@link #onRegister()} or after {@link #onUnregister()}.
+ */
+ public final void remove() {
+ IBinder key = mKey;
+ if (key != null && !mRemoved.getAndSet(true)) {
+ getOwner().removeRegistration(key);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ remove();
+ }
+
+ /**
+ * May be overridden by subclasses to handle listener operation failures. The default behavior
+ * is
+ * to further propagate any exceptions. Will always be invoked on the executor thread.
+ */
+ protected void onOperationFailure(Exception exception) {
+ throw new AssertionError(exception);
+ }
+
+ /**
+ * Executes the given listener operation on the registration executor, invoking {@link
+ * #onOperationFailure(Exception)} in case the listener operation fails. If the registration is
+ * removed prior to the operation running, the operation is considered canceled. If a null
+ * operation is supplied, nothing happens.
+ */
+ public final void executeOperation(@Nullable ListenerOperation<TListener> operation) {
+ if (operation == null) {
+ return;
+ }
+
+ synchronized (mListenerLock) {
+ if (mListener == null) {
+ return;
+ }
+
+ AtomicBoolean complete = new AtomicBoolean(false);
+ mExecutor.execute(() -> {
+ TListener listener;
+ synchronized (mListenerLock) {
+ listener = mListener;
+ }
+
+ Exception failure = null;
+ if (listener != null) {
+ try {
+ operation.operate(listener);
+ } catch (Exception e) {
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ failure = e;
+ }
+ }
+ }
+
+ operation.onComplete(failure == null);
+ complete.set(true);
+
+ if (failure != null) {
+ onOperationFailure(failure);
+ }
+ });
+ operation.onScheduled(complete.get());
+ }
+ }
+
+ /**
+ * An listener operation to perform.
+ *
+ * @param <ListenerT> listener type
+ */
+ public interface ListenerOperation<ListenerT> {
+
+ /**
+ * Invoked after the operation has been scheduled for execution. The {@code complete}
+ * argument
+ * will be true if {@link #onComplete(boolean)} was invoked prior to this callback (such as
+ * if
+ * using a direct executor), or false if {@link #onComplete(boolean)} will be invoked after
+ * this
+ * callback. This method is always invoked on the calling thread.
+ */
+ default void onScheduled(boolean complete) {
+ }
+
+ /**
+ * Invoked to perform an operation on the given listener. This method is always invoked on
+ * the
+ * executor thread. If this method throws a checked exception, the operation will fail and
+ * result in {@link #onOperationFailure(Exception)} being invoked. If this method throws an
+ * unchecked exception, this propagates normally and should result in a crash.
+ */
+ void operate(ListenerT listener) throws Exception;
+
+ /**
+ * Invoked after the operation is complete. The {@code success} argument will be true if
+ * the
+ * operation completed without throwing any exceptions, and false otherwise (such as if the
+ * operation was canceled prior to executing, or if it threw an exception). This invocation
+ * may
+ * happen either before or after (but never during) the invocation of {@link
+ * #onScheduled(boolean)}. This method is always invoked on the executor thread.
+ */
+ default void onComplete(boolean success) {
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
new file mode 100644
index 0000000..91237d2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.CancelableAlarm;
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
+
+/**
+ * Class responsible for all client based operations. Each {@link DiscoveryRegistration} is for one
+ * valid unique {@link android.nearby.NearbyManager#startScan(ScanRequest, Executor, ScanCallback)}
+ */
+public class DiscoveryRegistration extends BinderListenerRegistration<IScanListener> {
+
+ /**
+ * Timeout before a previous discovered device is reported as lost.
+ */
+ @VisibleForTesting
+ static final int ON_LOST_TIME_OUT_MS = 10000;
+ /** Lock for registration operations. */
+ final Object mMultiplexerLock;
+ private final ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest>
+ mOwner;
+ private final AppOpsManager mAppOpsManager;
+ /** Presence devices that are currently discovered, and not lost yet. */
+ @GuardedBy("mMultiplexerLock")
+ private final Map<Long, NearbyDeviceParcelable> mDiscoveredDevices;
+ /** A map of deviceId and alarms for reporting device lost. */
+ @GuardedBy("mMultiplexerLock")
+ private final Map<Long, DeviceOnLostAlarm> mDiscoveryOnLostAlarmPerDevice = new ArrayMap<>();
+ /**
+ * The single thread executor to run {@link CancelableAlarm} to report
+ * {@link NearbyDeviceParcelable} on lost after timeout.
+ */
+ private final ScheduledExecutorService mAlarmExecutor =
+ Executors.newSingleThreadScheduledExecutor();
+ private final ScanRequest mScanRequest;
+ private final CallerIdentity mCallerIdentity;
+
+ public DiscoveryRegistration(
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> owner,
+ ScanRequest scanRequest, IScanListener scanListener, Executor executor,
+ CallerIdentity callerIdentity, Object multiplexerLock, AppOpsManager appOpsManager) {
+ super(scanListener.asBinder(), executor, scanListener);
+ mOwner = owner;
+ mListener = scanListener;
+ mScanRequest = scanRequest;
+ mCallerIdentity = callerIdentity;
+ mMultiplexerLock = multiplexerLock;
+ mDiscoveredDevices = new ArrayMap<>();
+ mAppOpsManager = appOpsManager;
+ }
+
+ /**
+ * Gets the scan request.
+ */
+ public ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ /**
+ * Gets the actions from the scan filter(s).
+ */
+ public Set<Integer> getActions() {
+ Set<Integer> result = new ArraySet<>();
+ List<ScanFilter> filters = mScanRequest.getScanFilters();
+ for (ScanFilter filter : filters) {
+ if (filter instanceof PresenceScanFilter) {
+ result.addAll(((PresenceScanFilter) filter).getPresenceActions());
+ }
+ }
+ return ImmutableSet.copyOf(result);
+ }
+
+ /**
+ * Gets all the filters that are for Nearby Presence.
+ */
+ public Set<ScanFilter> getPresenceScanFilters() {
+ Set<ScanFilter> result = new ArraySet<>();
+ List<ScanFilter> filters = mScanRequest.getScanFilters();
+ for (ScanFilter filter : filters) {
+ if (filter.getType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ result.add(filter);
+ }
+ }
+ return ImmutableSet.copyOf(result);
+ }
+
+ @VisibleForTesting
+ Map<Long, DeviceOnLostAlarm> getDiscoveryOnLostAlarms() {
+ synchronized (mMultiplexerLock) {
+ return mDiscoveryOnLostAlarmPerDevice;
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof DiscoveryRegistration) {
+ DiscoveryRegistration otherRegistration = (DiscoveryRegistration) other;
+ return Objects.equals(mScanRequest, otherRegistration.mScanRequest) && Objects.equals(
+ mListener, otherRegistration.mListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mListener, mScanRequest);
+ }
+
+ @Override
+ public ListenerMultiplexer<
+ IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> getOwner() {
+ return mOwner;
+ }
+
+ @VisibleForTesting
+ ListenerOperation<IScanListener> reportDeviceLost(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_LOST, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Remove the device from reporting devices after reporting lost.
+ mDiscoveredDevices.remove(deviceId);
+ DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.remove(deviceId);
+ if (alarm != null) {
+ alarm.cancel();
+ }
+ }
+ });
+ }
+
+ /**
+ * Called when there is device discovered from the server.
+ */
+ public ListenerOperation<IScanListener> onNearbyDeviceDiscovered(
+ NearbyDeviceParcelable device) {
+ if (!filterCheck(device)) {
+ Log.d(TAG, "presence filter does not match for the scanned Presence Device");
+ return null;
+ }
+ synchronized (mMultiplexerLock) {
+ long deviceId = device.getDeviceId();
+ boolean deviceReported = mDiscoveredDevices.containsKey(deviceId);
+ scheduleOnLostAlarm(device);
+ if (deviceReported) {
+ NearbyDeviceParcelable oldDevice = mDiscoveredDevices.get(deviceId);
+ if (device.equals(oldDevice)) {
+ return null;
+ }
+ return reportUpdated(device);
+ }
+ return reportDiscovered(device);
+ }
+ }
+
+ @VisibleForTesting
+ static boolean presenceFilterMatches(NearbyDeviceParcelable device,
+ List<ScanFilter> scanFilters) {
+ if (scanFilters.isEmpty()) {
+ return true;
+ }
+ PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+ for (ScanFilter scanFilter : scanFilters) {
+ PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+ if (discoveryResult.matches(presenceScanFilter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportDiscovered(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_DISCOVERED, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Add the device to discovered devices after reporting device is
+ // discovered.
+ mDiscoveredDevices.put(deviceId, device);
+ scheduleOnLostAlarm(device);
+ }
+ });
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportUpdated(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ return reportResult(DiscoveryResult.DEVICE_UPDATED, device, () -> {
+ synchronized (mMultiplexerLock) {
+ // Update the new device to discovered devices after reporting device is
+ // discovered.
+ mDiscoveredDevices.put(deviceId, device);
+ scheduleOnLostAlarm(device);
+ }
+ });
+
+ }
+
+ /** Reports an error to the client. */
+ public ListenerOperation<IScanListener> reportError(@ScanCallback.ErrorCode int errorCode) {
+ return listener -> listener.onError(errorCode);
+ }
+
+ @Nullable
+ ListenerOperation<IScanListener> reportResult(@DiscoveryResult int result,
+ NearbyDeviceParcelable device, @Nullable Runnable successReportCallback) {
+ // Report the operation to AppOps.
+ // NOTE: AppOps report has to be the last operation before delivering the result. Otherwise
+ // we may over-report when the discovery result doesn't end up being delivered.
+ if (!checkIdentity()) {
+ return reportError(ScanCallback.ERROR_PERMISSION_DENIED);
+ }
+
+ return new ListenerOperation<>() {
+
+ @Override
+ public void operate(IScanListener listener) throws Exception {
+ switch (result) {
+ case DiscoveryResult.DEVICE_DISCOVERED:
+ listener.onDiscovered(device);
+ break;
+ case DiscoveryResult.DEVICE_UPDATED:
+ listener.onUpdated(device);
+ break;
+ case DiscoveryResult.DEVICE_LOST:
+ listener.onLost(device);
+ break;
+ }
+ }
+
+ @Override
+ public void onComplete(boolean success) {
+ if (success) {
+ if (successReportCallback != null) {
+ successReportCallback.run();
+ Log.d(TAG, "Successfully delivered result to caller.");
+ }
+ }
+ }
+ };
+ }
+
+ private boolean filterCheck(NearbyDeviceParcelable device) {
+ if (device.getScanType() != SCAN_TYPE_NEARBY_PRESENCE) {
+ return true;
+ }
+ List<ScanFilter> presenceFilters = mScanRequest.getScanFilters().stream().filter(
+ scanFilter -> scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE).collect(
+ Collectors.toList());
+ return presenceFilterMatches(device, presenceFilters);
+ }
+
+ private boolean checkIdentity() {
+ boolean result = DiscoveryPermissions.noteDiscoveryResultDelivery(mAppOpsManager,
+ mCallerIdentity);
+ if (!result) {
+ Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+ + "- not forwarding results for the registration.");
+ }
+ return result;
+ }
+
+ @GuardedBy("mMultiplexerLock")
+ private void scheduleOnLostAlarm(NearbyDeviceParcelable device) {
+ long deviceId = device.getDeviceId();
+ DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.get(deviceId);
+ if (alarm == null) {
+ alarm = new DeviceOnLostAlarm(device, mAlarmExecutor);
+ mDiscoveryOnLostAlarmPerDevice.put(deviceId, alarm);
+ }
+ alarm.start();
+ Log.d(TAG, "DiscoveryProviderManager updated state for " + device.getDeviceId());
+ }
+
+ /** Status of the discovery result. */
+ @IntDef({DiscoveryResult.DEVICE_DISCOVERED, DiscoveryResult.DEVICE_UPDATED,
+ DiscoveryResult.DEVICE_LOST})
+ public @interface DiscoveryResult {
+ int DEVICE_DISCOVERED = 0;
+ int DEVICE_UPDATED = 1;
+ int DEVICE_LOST = 2;
+ }
+
+ private class DeviceOnLostAlarm {
+
+ private static final String NAME = "DeviceOnLostAlarm";
+ private final NearbyDeviceParcelable mDevice;
+ private final ScheduledExecutorService mAlarmExecutor;
+ @Nullable
+ private CancelableAlarm mTimeoutAlarm;
+
+ DeviceOnLostAlarm(NearbyDeviceParcelable device, ScheduledExecutorService alarmExecutor) {
+ mDevice = device;
+ mAlarmExecutor = alarmExecutor;
+ }
+
+ synchronized void start() {
+ cancel();
+ this.mTimeoutAlarm = CancelableAlarm.createSingleAlarm(NAME, () -> {
+ Log.d(TAG, String.format("%s timed out after %d ms. Reporting %s on lost.", NAME,
+ ON_LOST_TIME_OUT_MS, mDevice.getName()));
+ synchronized (mMultiplexerLock) {
+ executeOperation(reportDeviceLost(mDevice));
+ }
+ }, ON_LOST_TIME_OUT_MS, mAlarmExecutor);
+ }
+
+ synchronized void cancel() {
+ if (mTimeoutAlarm != null) {
+ mTimeoutAlarm.cancel();
+ mTimeoutAlarm = null;
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/Advertisement.java b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
new file mode 100644
index 0000000..d42f6c7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.PresenceCredential;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A Nearby Presence advertisement to be advertised. */
+public abstract class Advertisement {
+
+ @BroadcastRequest.BroadcastVersion
+ int mVersion = BroadcastRequest.PRESENCE_VERSION_UNKNOWN;
+ int mLength;
+ @PresenceCredential.IdentityType int mIdentityType;
+ byte[] mIdentity;
+ byte[] mSalt;
+ List<Integer> mActions;
+
+ /** Serialize an {@link Advertisement} object into bytes. */
+ @Nullable
+ public byte[] toBytes() {
+ return new byte[0];
+ }
+
+ /** Returns the length of the advertisement. */
+ public int getLength() {
+ return mLength;
+ }
+
+ /** Returns the version in the advertisement. */
+ @BroadcastRequest.BroadcastVersion
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /** Returns the identity type in the advertisement. */
+ @PresenceCredential.IdentityType
+ public int getIdentityType() {
+ return mIdentityType;
+ }
+
+ /** Returns the identity bytes in the advertisement. */
+ public byte[] getIdentity() {
+ return mIdentity.clone();
+ }
+
+ /** Returns the salt of the advertisement. */
+ public byte[] getSalt() {
+ return mSalt.clone();
+ }
+
+ /** Returns the actions in the advertisement. */
+ public List<Integer> getActions() {
+ return new ArrayList<>(mActions);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
new file mode 100644
index 0000000..ae4a728
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Represents a data element header in Nearby Presence.
+ * Each header has 3 parts: tag, length and style.
+ * Tag: 1 bit (MSB at each byte). 1 for extending, which means there will be more bytes after
+ * the current one for the header.
+ * Length: The total length of a Data Element field. Length is up to 127 and is limited within
+ * the entire first byte in the header. (7 bits, MSB is the tag).
+ * Type: Represents {@link DataElement.DataType}. There is no limit for the type number.
+ *
+ * @hide
+ */
+public class DataElementHeader {
+ // Each Data reserved MSB for tag.
+ static final int TAG_BITMASK = 0b10000000;
+ static final int TAG_OFFSET = 7;
+
+ // If the header is only 1 byte, it has the format: 0b0LLLTTTT. (L for length, T for type.)
+ static final int SINGLE_AVAILABLE_LENGTH_BIT = 3;
+ static final int SINGLE_AVAILABLE_TYPE_BIT = 4;
+ static final int SINGLE_LENGTH_BITMASK = 0b01110000;
+ static final int SINGLE_LENGTH_OFFSET = SINGLE_AVAILABLE_TYPE_BIT;
+ static final int SINGLE_TYPE_BITMASK = 0b00001111;
+
+ // If there are multiple data element headers.
+ // First byte is always the length.
+ static final int MULTIPLE_LENGTH_BYTE = 1;
+ // Each byte reserves MSB for tag.
+ static final int MULTIPLE_BITMASK = 0b01111111;
+
+ @BroadcastRequest.BroadcastVersion
+ private final int mVersion;
+ @DataElement.DataType
+ private final int mDataType;
+ private final int mDataLength;
+
+ DataElementHeader(@BroadcastRequest.BroadcastVersion int version,
+ @DataElement.DataType int dataType, int dataLength) {
+ Preconditions.checkArgument(version == BroadcastRequest.PRESENCE_VERSION_V1,
+ "DataElementHeader is only supported in V1.");
+ Preconditions.checkArgument(dataLength >= 0, "Length should not be negative.");
+ Preconditions.checkArgument(dataLength < (1 << TAG_OFFSET),
+ "Data element should be equal or shorter than 128.");
+
+ this.mVersion = version;
+ this.mDataType = dataType;
+ this.mDataLength = dataLength;
+ }
+
+ /**
+ * The total type of the data element.
+ */
+ @DataElement.DataType
+ public int getDataType() {
+ return mDataType;
+ }
+
+ /**
+ * The total length of a Data Element field.
+ */
+ public int getDataLength() {
+ return mDataLength;
+ }
+
+ /** Serialize a {@link DataElementHeader} object into bytes. */
+ public byte[] toBytes() {
+ Preconditions.checkState(mVersion == BroadcastRequest.PRESENCE_VERSION_V1,
+ "DataElementHeader is only supported in V1.");
+ // Only 1 byte needed for the header
+ if (mDataType < (1 << SINGLE_AVAILABLE_TYPE_BIT)
+ && mDataLength < (1 << SINGLE_AVAILABLE_LENGTH_BIT)) {
+ return new byte[]{createSingleByteHeader(mDataType, mDataLength)};
+ }
+
+ return createMultipleBytesHeader(mDataType, mDataLength);
+ }
+
+ /** Creates a {@link DataElementHeader} object from bytes. */
+ @Nullable
+ public static DataElementHeader fromBytes(@BroadcastRequest.BroadcastVersion int version,
+ @Nonnull byte[] bytes) {
+ Objects.requireNonNull(bytes, "Data parsed in for DataElement should not be null.");
+
+ if (bytes.length == 0) {
+ return null;
+ }
+
+ if (bytes.length == 1) {
+ if (isExtending(bytes[0])) {
+ throw new IllegalArgumentException("The header is not complete.");
+ }
+ return new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+ getTypeSingleByte(bytes[0]), getLengthSingleByte(bytes[0]));
+ }
+
+ // The first byte should be length and there should be at least 1 more byte following to
+ // represent type.
+ // The last header byte's MSB should be 0.
+ if (!isExtending(bytes[0]) || isExtending(bytes[bytes.length - 1])) {
+ throw new IllegalArgumentException("The header format is wrong.");
+ }
+
+ return new DataElementHeader(version,
+ getTypeMultipleBytes(Arrays.copyOfRange(bytes, 1, bytes.length)),
+ getHeaderValue(bytes[0]));
+ }
+
+ /** Creates a header based on type and length.
+ * This is used when the type is <= 16 and length is <= 7. */
+ static byte createSingleByteHeader(int type, int length) {
+ return (byte) (convertTag(/* extend= */ false)
+ | convertLengthSingleByte(length)
+ | convertTypeSingleByte(type));
+ }
+
+ /** Creates a header based on type and length.
+ * This is used when the type is > 16 or length is > 7. */
+ static byte[] createMultipleBytesHeader(int type, int length) {
+ List<Byte> typeIntList = convertTypeMultipleBytes(type);
+ byte[] res = new byte[typeIntList.size() + MULTIPLE_LENGTH_BYTE];
+ int index = 0;
+ res[index++] = convertLengthMultipleBytes(length);
+
+ for (int typeInt : typeIntList) {
+ res[index++] = (byte) typeInt;
+ }
+ return res;
+ }
+
+ /** Constructs a Data Element header with length indicated in byte format.
+ * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+ */
+ @VisibleForTesting
+ static int convertLengthSingleByte(int length) {
+ Preconditions.checkArgument(length >= 0, "Length should not be negative.");
+ Preconditions.checkArgument(length < (1 << SINGLE_AVAILABLE_LENGTH_BIT),
+ "In single Data Element header, length should be shorter than 8.");
+ return (length << SINGLE_LENGTH_OFFSET) & SINGLE_LENGTH_BITMASK;
+ }
+
+ /** Constructs a Data Element header with type indicated in byte format.
+ * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+ */
+ @VisibleForTesting
+ static int convertTypeSingleByte(int type) {
+ Preconditions.checkArgument(type >= 0, "Type should not be negative.");
+ Preconditions.checkArgument(type < (1 << SINGLE_AVAILABLE_TYPE_BIT),
+ "In single Data Element header, type should be smaller than 16.");
+
+ return type & SINGLE_TYPE_BITMASK;
+ }
+
+ /**
+ * Gets the length of Data Element from the header. (When there is only 1 byte of header)
+ */
+ static int getLengthSingleByte(byte header) {
+ Preconditions.checkArgument(!isExtending(header),
+ "Cannot apply this method for the extending header.");
+ return (header & SINGLE_LENGTH_BITMASK) >> SINGLE_LENGTH_OFFSET;
+ }
+
+ /**
+ * Gets the type of Data Element from the header. (When there is only 1 byte of header)
+ */
+ static int getTypeSingleByte(byte header) {
+ Preconditions.checkArgument(!isExtending(header),
+ "Cannot apply this method for the extending header.");
+ return header & SINGLE_TYPE_BITMASK;
+ }
+
+ /** Creates a DE(data element) header based on length.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ */
+ static byte convertLengthMultipleBytes(int length) {
+ Preconditions.checkArgument(length < (1 << TAG_OFFSET),
+ "Data element should be equal or shorter than 128.");
+ return (byte) (convertTag(/* extend= */ true) | (length & MULTIPLE_BITMASK));
+ }
+
+ /** Creates a DE(data element) header based on type.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ */
+ @VisibleForTesting
+ static List<Byte> convertTypeMultipleBytes(int type) {
+ List<Byte> typeBytes = new ArrayList<>();
+ while (type > 0) {
+ byte current = (byte) (type & MULTIPLE_BITMASK);
+ type = type >> TAG_OFFSET;
+ typeBytes.add(current);
+ }
+
+ Collections.reverse(typeBytes);
+ int size = typeBytes.size();
+ // The last byte's MSB should be 0.
+ for (int i = 0; i < size - 1; i++) {
+ typeBytes.set(i, (byte) (convertTag(/* extend= */ true) | typeBytes.get(i)));
+ }
+ return typeBytes;
+ }
+
+ /** Creates a DE(data element) header based on type.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ * Uses Integer when doing bit operation to avoid error.
+ */
+ @VisibleForTesting
+ static int getTypeMultipleBytes(byte[] typeByteArray) {
+ int type = 0;
+ int size = typeByteArray.length;
+ for (int i = 0; i < size; i++) {
+ type = (type << TAG_OFFSET) | getHeaderValue(typeByteArray[i]);
+ }
+ return type;
+ }
+
+ /** Gets the integer value of the 7 bits in the header. (The MSB is tag) */
+ @VisibleForTesting
+ static int getHeaderValue(byte header) {
+ return (header & MULTIPLE_BITMASK);
+ }
+
+ /** Sets the MSB of the header byte. If this is the last byte of headers, MSB is 0.
+ * If there are at least header following, the MSB is 1.
+ */
+ @VisibleForTesting
+ static byte convertTag(boolean extend) {
+ return (byte) (extend ? 0b10000000 : 0b00000000);
+ }
+
+ /** Returns {@code true} if there are at least 1 byte of header after the current one. */
+ @VisibleForTesting
+ static boolean isExtending(byte header) {
+ return (header & TAG_BITMASK) != 0;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
new file mode 100644
index 0000000..34a7514
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PublicCredential;
+import android.util.Log;
+
+import com.android.server.nearby.util.encryption.Cryptor;
+import com.android.server.nearby.util.encryption.CryptorImpFake;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A Nearby Presence advertisement to be advertised on BT5.0 devices.
+ *
+ * <p>Serializable between Java object and bytes formats. Java object is used at the upper scanning
+ * and advertising interface as an abstraction of the actual bytes. Bytes format is used at the
+ * underlying BLE and mDNS stacks, which do necessary slicing and merging based on advertising
+ * capacities.
+ *
+ * The extended advertisement is defined in the format below:
+ * Header (1 byte) | salt (1+2 bytes) | Identity + filter (2+16 bytes)
+ * | repeated DE fields (various bytes)
+ * The header contains:
+ * version (3 bits) | 5 bit reserved for future use (RFU)
+ */
+public class ExtendedAdvertisement extends Advertisement{
+
+ public static final int SALT_DATA_LENGTH = 2;
+
+ static final int HEADER_LENGTH = 1;
+
+ static final int IDENTITY_DATA_LENGTH = 16;
+
+ private final List<DataElement> mDataElements;
+
+ private final byte[] mAuthenticityKey;
+
+ // All Data Elements including salt and identity.
+ // Each list item (byte array) is a Data Element (with its header).
+ private final List<byte[]> mCompleteDataElementsBytes;
+ // Signature generated from data elements.
+ private final byte[] mHmacTag;
+
+ /**
+ * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+ * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
+ */
+ @Nullable
+ public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
+ if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+ Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
+ return null;
+ }
+
+ byte[] salt = request.getSalt();
+ if (salt.length != SALT_DATA_LENGTH) {
+ Log.v(TAG, "Salt does not match correct length");
+ return null;
+ }
+
+ byte[] identity = request.getCredential().getMetadataEncryptionKey();
+ byte[] authenticityKey = request.getCredential().getAuthenticityKey();
+ if (identity.length != IDENTITY_DATA_LENGTH) {
+ Log.v(TAG, "Identity does not match correct length");
+ return null;
+ }
+
+ List<Integer> actions = request.getActions();
+ if (actions.isEmpty()) {
+ Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
+ return null;
+ }
+
+ List<DataElement> dataElements = request.getExtendedProperties();
+ return new ExtendedAdvertisement(
+ request.getCredential().getIdentityType(),
+ identity,
+ salt,
+ authenticityKey,
+ actions,
+ dataElements);
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(getLength());
+
+ // Header
+ buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
+
+ // Salt
+ buffer.put(mCompleteDataElementsBytes.get(0));
+
+ // Identity
+ buffer.put(mCompleteDataElementsBytes.get(1));
+
+ List<Byte> rawDataBytes = new ArrayList<>();
+ // Data Elements (Already includes salt and identity)
+ for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
+ byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
+ for (Byte b : dataElementBytes) {
+ rawDataBytes.add(b);
+ }
+ }
+
+ byte[] dataElements = new byte[rawDataBytes.size()];
+ for (int i = 0; i < rawDataBytes.size(); i++) {
+ dataElements[i] = rawDataBytes.get(i);
+ }
+
+ buffer.put(
+ getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
+
+ buffer.put(mHmacTag);
+
+ return buffer.array();
+ }
+
+ /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+ * {@code null} when there is something when parsing.
+ */
+ @Nullable
+ public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
+ @BroadcastRequest.BroadcastVersion
+ int version = ExtendedAdvertisementUtils.getVersion(bytes);
+ if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+ Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
+ return null;
+ }
+
+ byte[] authenticityKey = publicCredential.getAuthenticityKey();
+
+ int index = HEADER_LENGTH;
+ // Salt
+ byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
+ if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
+ Log.v(TAG, "First data element has to be salt.");
+ return null;
+ }
+ index += saltHeaderArray.length;
+ byte[] salt = new byte[saltHeader.getDataLength()];
+ for (int i = 0; i < saltHeader.getDataLength(); i++) {
+ salt[i] = bytes[index++];
+ }
+
+ // Identity
+ byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader identityHeader =
+ DataElementHeader.fromBytes(version, identityHeaderArray);
+ if (identityHeader == null) {
+ Log.v(TAG, "The second element has to be identity.");
+ return null;
+ }
+ index += identityHeaderArray.length;
+ @PresenceCredential.IdentityType int identityType =
+ toPresenceCredentialIdentityType(identityHeader.getDataType());
+ if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
+ Log.v(TAG, "The identity type is unknown.");
+ return null;
+ }
+ byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
+ for (int i = 0; i < identityHeader.getDataLength(); i++) {
+ encryptedIdentity[i] = bytes[index++];
+ }
+ byte[] identity =
+ CryptorImpIdentityV1
+ .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
+
+ Cryptor cryptor = getCryptor(/* encrypt= */ true);
+ byte[] encryptedDataElements =
+ new byte[bytes.length - index - cryptor.getSignatureLength()];
+ // Decrypt other data elements
+ System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
+ byte[] decryptedDataElements =
+ cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
+ if (decryptedDataElements == null) {
+ return null;
+ }
+
+ // Verify the computed HMAC tag is equal to HMAC tag in advertisement
+ if (cryptor.getSignatureLength() > 0) {
+ byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+ System.arraycopy(
+ bytes, bytes.length - cryptor.getSignatureLength(),
+ expectedHmacTag, 0, cryptor.getSignatureLength());
+ if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
+ Log.e(TAG, "HMAC tags not match.");
+ return null;
+ }
+ }
+
+ int dataElementArrayIndex = 0;
+ // Other Data Elements
+ List<Integer> actions = new ArrayList<>();
+ List<DataElement> dataElements = new ArrayList<>();
+ while (dataElementArrayIndex < decryptedDataElements.length) {
+ byte[] deHeaderArray = ExtendedAdvertisementUtils
+ .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
+ DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+ dataElementArrayIndex += deHeaderArray.length;
+
+ @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
+ if (type == DataElement.DataType.ACTION) {
+ if (deHeader.getDataLength() != 1) {
+ Log.v(TAG, "Action id should only 1 byte.");
+ return null;
+ }
+ actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
+ } else {
+ if (isSaltOrIdentity(type)) {
+ Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+ + " and one identity in the advertisement.");
+ return null;
+ }
+ byte[] deData = new byte[deHeader.getDataLength()];
+ for (int i = 0; i < deHeader.getDataLength(); i++) {
+ deData[i] = decryptedDataElements[dataElementArrayIndex++];
+ }
+ dataElements.add(new DataElement(type, deData));
+ }
+ }
+
+ return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
+ dataElements);
+ }
+
+ /** Returns the {@link DataElement}s in the advertisement. */
+ public List<DataElement> getDataElements() {
+ return new ArrayList<>(mDataElements);
+ }
+
+ /** Returns the {@link DataElement}s in the advertisement according to the key. */
+ public List<DataElement> getDataElements(@DataElement.DataType int key) {
+ List<DataElement> res = new ArrayList<>();
+ for (DataElement dataElement : mDataElements) {
+ if (key == dataElement.getKey()) {
+ res.add(dataElement);
+ }
+ }
+ return res;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "ExtendedAdvertisement:"
+ + "<VERSION: %s, length: %s, dataElementCount: %s, identityType: %s,"
+ + " identity: %s, salt: %s, actions: %s>",
+ getVersion(),
+ getLength(),
+ getDataElements().size(),
+ getIdentityType(),
+ Arrays.toString(getIdentity()),
+ Arrays.toString(getSalt()),
+ getActions());
+ }
+
+ ExtendedAdvertisement(
+ @PresenceCredential.IdentityType int identityType,
+ byte[] identity,
+ byte[] salt,
+ byte[] authenticityKey,
+ List<Integer> actions,
+ List<DataElement> dataElements) {
+ this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
+ this.mIdentityType = identityType;
+ this.mIdentity = identity;
+ this.mSalt = salt;
+ this.mAuthenticityKey = authenticityKey;
+ this.mActions = actions;
+ this.mDataElements = dataElements;
+ this.mCompleteDataElementsBytes = new ArrayList<>();
+
+ int length = HEADER_LENGTH; // header
+
+ // Salt
+ DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
+ byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
+ mCompleteDataElementsBytes.add(saltByteArray);
+ length += saltByteArray.length;
+
+ // Identity
+ byte[] encryptedIdentity =
+ CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
+ DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
+ byte[] identityByteArray =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
+ mCompleteDataElementsBytes.add(identityByteArray);
+ length += identityByteArray.length;
+
+ List<Byte> dataElementBytes = new ArrayList<>();
+ // Intents
+ for (int action : mActions) {
+ DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
+ new byte[] {(byte) action});
+ byte[] intentByteArray =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
+ mCompleteDataElementsBytes.add(intentByteArray);
+ for (Byte b : intentByteArray) {
+ dataElementBytes.add(b);
+ }
+ }
+
+ // Data Elements (Extended properties)
+ for (DataElement dataElement : mDataElements) {
+ byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
+ mCompleteDataElementsBytes.add(deByteArray);
+ for (Byte b : deByteArray) {
+ dataElementBytes.add(b);
+ }
+ }
+
+ byte[] data = new byte[dataElementBytes.size()];
+ for (int i = 0; i < dataElementBytes.size(); i++) {
+ data[i] = dataElementBytes.get(i);
+ }
+ Cryptor cryptor = getCryptor(/* encrypt= */ true);
+ byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
+
+ length += encryptedDeBytes.length;
+
+ // Signature
+ byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
+ mHmacTag = hmacTag;
+ length += hmacTag.length;
+
+ this.mLength = length;
+ }
+
+ @PresenceCredential.IdentityType
+ private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
+ switch (type) {
+ case DataElement.DataType.PRIVATE_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ case DataElement.DataType.PROVISIONED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+ case DataElement.DataType.TRUSTED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+ case DataElement.DataType.PUBLIC_IDENTITY:
+ default:
+ return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+ }
+ }
+
+ @DataElement.DataType
+ private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+ switch (identityType) {
+ case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+ return DataElement.DataType.PRIVATE_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+ return DataElement.DataType.PROVISIONED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+ return DataElement.DataType.TRUSTED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+ default:
+ return DataElement.DataType.PUBLIC_IDENTITY;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
+ * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+ */
+ private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
+ return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
+ || type == DataElement.DataType.TRUSTED_IDENTITY
+ || type == DataElement.DataType.PROVISIONED_IDENTITY
+ || type == DataElement.DataType.PUBLIC_IDENTITY;
+ }
+
+ private static Cryptor getCryptor(boolean encrypt) {
+ if (encrypt) {
+ Log.d(TAG, "get V1 Cryptor");
+ return CryptorImpV1.getInstance();
+ }
+ Log.d(TAG, "get fake Cryptor");
+ return CryptorImpFake.getInstance();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
new file mode 100644
index 0000000..06d0f2b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.presence.ExtendedAdvertisement.HEADER_LENGTH;
+
+import android.annotation.SuppressLint;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides serialization and deserialization util methods for {@link ExtendedAdvertisement}.
+ */
+public final class ExtendedAdvertisementUtils {
+
+ // Advertisement header related static fields.
+ private static final int VERSION_MASK = 0b11100000;
+ private static final int VERSION_MASK_AFTER_SHIT = 0b00000111;
+ private static final int HEADER_INDEX = 0;
+ private static final int HEADER_VERSION_OFFSET = 5;
+
+ /**
+ * Constructs the header of a {@link ExtendedAdvertisement}.
+ * 3 bit version, and 5 bit reserved for future use (RFU).
+ */
+ public static byte constructHeader(@BroadcastRequest.BroadcastVersion int version) {
+ return (byte) ((version << 5) & VERSION_MASK);
+ }
+
+ /** Returns the {@link BroadcastRequest.BroadcastVersion} from the advertisement
+ * in bytes format. */
+ public static int getVersion(byte[] advertisement) {
+ if (advertisement.length < HEADER_LENGTH) {
+ throw new IllegalArgumentException("Advertisement must contain header");
+ }
+ return ((advertisement[HEADER_INDEX] & VERSION_MASK) >> HEADER_VERSION_OFFSET)
+ & VERSION_MASK_AFTER_SHIT;
+ }
+
+ /** Returns the {@link DataElementHeader} from the advertisement in bytes format. */
+ public static byte[] getDataElementHeader(byte[] advertisement, int startIndex) {
+ Preconditions.checkArgument(startIndex < advertisement.length,
+ "Advertisement has no longer data left.");
+ List<Byte> headerBytes = new ArrayList<>();
+ while (startIndex < advertisement.length) {
+ byte current = advertisement[startIndex];
+ headerBytes.add(current);
+ if (!DataElementHeader.isExtending(current)) {
+ int size = headerBytes.size();
+ byte[] res = new byte[size];
+ for (int i = 0; i < size; i++) {
+ res[i] = headerBytes.get(i);
+ }
+ return res;
+ }
+ startIndex++;
+ }
+ throw new IllegalArgumentException("There is no end of the DataElement header.");
+ }
+
+ /**
+ * Constructs {@link DataElement}, including header(s) and actual data element data.
+ *
+ * Suppresses warning because {@link DataElement} checks isValidType in constructor.
+ */
+ @SuppressLint("WrongConstant")
+ public static byte[] convertDataElementToBytes(DataElement dataElement) {
+ @DataElement.DataType int type = dataElement.getKey();
+ byte[] data = dataElement.getValue();
+ DataElementHeader header = new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+ type, data.length);
+ byte[] headerByteArray = header.toBytes();
+
+ byte[] res = new byte[headerByteArray.length + data.length];
+ System.arraycopy(headerByteArray, 0, res, 0, headerByteArray.length);
+ System.arraycopy(data, 0, res, headerByteArray.length, data.length);
+ return res;
+ }
+
+ private ExtendedAdvertisementUtils() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
index e4df673..ae53ada 100644
--- a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
@@ -24,7 +24,6 @@
import com.android.internal.util.Preconditions;
import java.nio.ByteBuffer;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -42,7 +41,7 @@
// The header contains:
// version (3 bits) | provision_mode_flag (1 bit) | identity_type (3 bits) |
// extended_advertisement_mode (1 bit)
-public class FastAdvertisement {
+public class FastAdvertisement extends Advertisement {
private static final int FAST_ADVERTISEMENT_MAX_LENGTH = 24;
@@ -85,7 +84,8 @@
(byte) request.getTxPower());
}
- /** Serialize an {@link FastAdvertisement} object into bytes. */
+ /** Serialize a {@link FastAdvertisement} object into bytes. */
+ @Override
public byte[] toBytes() {
ByteBuffer buffer = ByteBuffer.allocate(getLength());
@@ -100,18 +100,8 @@
return buffer.array();
}
- private final int mLength;
-
private final int mLtvFieldCount;
- @PresenceCredential.IdentityType private final int mIdentityType;
-
- private final byte[] mIdentity;
-
- private final byte[] mSalt;
-
- private final List<Integer> mActions;
-
@Nullable
private final Byte mTxPower;
@@ -121,6 +111,7 @@
byte[] salt,
List<Integer> actions,
@Nullable Byte txPower) {
+ this.mVersion = BroadcastRequest.PRESENCE_VERSION_V0;
this.mIdentityType = identityType;
this.mIdentity = identity;
this.mSalt = salt;
@@ -143,44 +134,12 @@
"FastAdvertisement exceeds maximum length");
}
- /** Returns the version in the advertisement. */
- @BroadcastRequest.BroadcastVersion
- public int getVersion() {
- return BroadcastRequest.PRESENCE_VERSION_V0;
- }
-
- /** Returns the identity type in the advertisement. */
- @PresenceCredential.IdentityType
- public int getIdentityType() {
- return mIdentityType;
- }
-
- /** Returns the identity bytes in the advertisement. */
- public byte[] getIdentity() {
- return mIdentity.clone();
- }
-
- /** Returns the salt of the advertisement. */
- public byte[] getSalt() {
- return mSalt.clone();
- }
-
- /** Returns the actions in the advertisement. */
- public List<Integer> getActions() {
- return new ArrayList<>(mActions);
- }
-
/** Returns the adjusted TX Power in the advertisement. Null if not available. */
@Nullable
public Byte getTxPower() {
return mTxPower;
}
- /** Returns the length of the advertisement. */
- public int getLength() {
- return mLength;
- }
-
/** Returns the count of LTV fields in the advertisement. */
public int getLtvFieldCount() {
return mLtvFieldCount;
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
index d1c72ae..5a76d96 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -16,31 +16,55 @@
package com.android.server.nearby.presence;
-import android.nearby.NearbyDevice;
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.nearby.DataElement;
import android.nearby.NearbyDeviceParcelable;
import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
import android.nearby.PublicCredential;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
/** Represents a Presence discovery result. */
public class PresenceDiscoveryResult {
/** Creates a {@link PresenceDiscoveryResult} from the scan data. */
public static PresenceDiscoveryResult fromDevice(NearbyDeviceParcelable device) {
+ PresenceDevice presenceDevice = device.getPresenceDevice();
+ if (presenceDevice != null) {
+ return new PresenceDiscoveryResult.Builder()
+ .setTxPower(device.getTxPower())
+ .setRssi(device.getRssi())
+ .setSalt(presenceDevice.getSalt())
+ .setPublicCredential(device.getPublicCredential())
+ .addExtendedProperties(presenceDevice.getExtendedProperties())
+ .setEncryptedIdentityTag(device.getEncryptionKeyTag())
+ .build();
+ }
byte[] salt = device.getSalt();
if (salt == null) {
salt = new byte[0];
}
- return new PresenceDiscoveryResult.Builder()
- .setTxPower(device.getTxPower())
+
+ PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder();
+ builder.setTxPower(device.getTxPower())
.setRssi(device.getRssi())
.setSalt(salt)
.addPresenceAction(device.getAction())
- .setPublicCredential(device.getPublicCredential())
- .build();
+ .setPublicCredential(device.getPublicCredential());
+ if (device.getPresenceDevice() != null) {
+ builder.addExtendedProperties(device.getPresenceDevice().getExtendedProperties());
+ }
+ return builder.build();
}
private final int mTxPower;
@@ -48,25 +72,35 @@
private final byte[] mSalt;
private final List<Integer> mPresenceActions;
private final PublicCredential mPublicCredential;
+ private final List<DataElement> mExtendedProperties;
+ private final byte[] mEncryptedIdentityTag;
private PresenceDiscoveryResult(
int txPower,
int rssi,
byte[] salt,
List<Integer> presenceActions,
- PublicCredential publicCredential) {
+ PublicCredential publicCredential,
+ List<DataElement> extendedProperties,
+ byte[] encryptedIdentityTag) {
mTxPower = txPower;
mRssi = rssi;
mSalt = salt;
mPresenceActions = presenceActions;
mPublicCredential = publicCredential;
+ mExtendedProperties = extendedProperties;
+ mEncryptedIdentityTag = encryptedIdentityTag;
}
/** Returns whether the discovery result matches the scan filter. */
public boolean matches(PresenceScanFilter scanFilter) {
+ if (accountKeyMatches(scanFilter.getExtendedProperties())) {
+ return true;
+ }
+
return pathLossMatches(scanFilter.getMaxPathLoss())
&& actionMatches(scanFilter.getPresenceActions())
- && credentialMatches(scanFilter.getCredentials());
+ && identityMatches(scanFilter.getCredentials());
}
private boolean pathLossMatches(int maxPathLoss) {
@@ -80,21 +114,47 @@
return filterActions.stream().anyMatch(mPresenceActions::contains);
}
- private boolean credentialMatches(List<PublicCredential> credentials) {
- return credentials.contains(mPublicCredential);
+ @VisibleForTesting
+ boolean accountKeyMatches(List<DataElement> extendedProperties) {
+ Set<byte[]> accountKeys = new ArraySet<>();
+ for (DataElement requestedDe : mExtendedProperties) {
+ if (requestedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+ continue;
+ }
+ accountKeys.add(requestedDe.getValue());
+ }
+ for (DataElement scannedDe : extendedProperties) {
+ if (scannedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+ continue;
+ }
+ // If one account key matches, then returns true.
+ for (byte[] key : accountKeys) {
+ if (Arrays.equals(key, scannedDe.getValue())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
- /** Converts a presence device from the discovery result. */
- public PresenceDevice toPresenceDevice() {
- return new PresenceDevice.Builder(
- // Use the public credential hash as the device Id.
- String.valueOf(mPublicCredential.hashCode()),
- mSalt,
- mPublicCredential.getSecretId(),
- mPublicCredential.getEncryptedMetadata())
- .setRssi(mRssi)
- .addMedium(NearbyDevice.Medium.BLE)
- .build();
+ @VisibleForTesting
+ /** Gets presence {@link DataElement}s of the discovery result. */
+ public List<DataElement> getExtendedProperties() {
+ return mExtendedProperties;
+ }
+
+ private boolean identityMatches(List<PublicCredential> publicCredentials) {
+ if (mEncryptedIdentityTag.length == 0) {
+ return true;
+ }
+ for (PublicCredential publicCredential : publicCredentials) {
+ if (Arrays.equals(
+ mEncryptedIdentityTag, publicCredential.getEncryptedMetadataKeyTag())) {
+ return true;
+ }
+ }
+ return false;
}
/** Builder for {@link PresenceDiscoveryResult}. */
@@ -105,9 +165,12 @@
private PublicCredential mPublicCredential;
private final List<Integer> mPresenceActions;
+ private final List<DataElement> mExtendedProperties;
+ private byte[] mEncryptedIdentityTag = new byte[0];
public Builder() {
mPresenceActions = new ArrayList<>();
+ mExtendedProperties = new ArrayList<>();
}
/** Sets the calibrated tx power for the discovery result. */
@@ -130,7 +193,18 @@
/** Sets the public credential for the discovery result. */
public Builder setPublicCredential(PublicCredential publicCredential) {
- mPublicCredential = publicCredential;
+ if (publicCredential != null) {
+ mPublicCredential = publicCredential;
+ }
+ return this;
+ }
+
+ /** Sets the encrypted identity tag for the discovery result. Usually it is passed from
+ * {@link NearbyDeviceParcelable} and the tag is calculated with authenticity key when
+ * receiving an advertisement.
+ */
+ public Builder setEncryptedIdentityTag(byte[] encryptedIdentityTag) {
+ mEncryptedIdentityTag = encryptedIdentityTag;
return this;
}
@@ -140,10 +214,34 @@
return this;
}
+ /** Adds presence {@link DataElement}s of the discovery result. */
+ public Builder addExtendedProperties(DataElement dataElement) {
+ if (dataElement.getKey() == DataElement.DataType.ACTION) {
+ byte[] value = dataElement.getValue();
+ if (value.length == 1) {
+ addPresenceAction(Byte.toUnsignedInt(value[0]));
+ } else {
+ Log.e(TAG, "invalid action data element");
+ }
+ } else {
+ mExtendedProperties.add(dataElement);
+ }
+ return this;
+ }
+
+ /** Adds presence {@link DataElement}s of the discovery result. */
+ public Builder addExtendedProperties(@NonNull List<DataElement> dataElements) {
+ for (DataElement dataElement : dataElements) {
+ addExtendedProperties(dataElement);
+ }
+ return this;
+ }
+
/** Builds a {@link PresenceDiscoveryResult}. */
public PresenceDiscoveryResult build() {
return new PresenceDiscoveryResult(
- mTxPower, mRssi, mSalt, mPresenceActions, mPublicCredential);
+ mTxPower, mRssi, mSalt, mPresenceActions,
+ mPublicCredential, mExtendedProperties, mEncryptedIdentityTag);
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
new file mode 100644
index 0000000..0a51068
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+
+/** PresenceManager is the class initiated in nearby service to handle presence related work. */
+public class PresenceManager {
+
+ final Context mContext;
+ private final IntentFilter mIntentFilter;
+
+ @VisibleForTesting
+ final ScanCallback mScanCallback =
+ new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {
+ Log.i(TAG, "[PresenceManager] discovered Device.");
+ PresenceDevice presenceDevice = (PresenceDevice) device;
+ List<DataElement> dataElements = presenceDevice.getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ Log.i(TAG, "[PresenceManager] Data Element key "
+ + dataElement.getKey());
+ Log.i(TAG, "[PresenceManager] Data Element value "
+ + Arrays.toString(dataElement.getValue()));
+ }
+ }
+
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onError(int errorCode) {
+ Log.w(TAG, "[PresenceManager] Scan error is " + errorCode);
+ }
+ };
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NearbyManager manager = getNearbyManager();
+ if (manager == null) {
+ Log.e(TAG, "Nearby Manager is null");
+ return;
+ }
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+ Log.d(TAG, "PresenceManager Start scan.");
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(new byte[]{1}, new byte[]{1},
+ new byte[]{1}, new byte[]{1}, new byte[]{1}).build();
+ PresenceScanFilter presenceScanFilter =
+ new PresenceScanFilter.Builder()
+ .setMaxPathLoss(3)
+ .addCredential(publicCredential)
+ .addPresenceAction(1)
+ .addExtendedProperty(new DataElement(
+ DataElement.DataType.ACCOUNT_KEY_DATA,
+ new byte[16]))
+ .build();
+ ScanRequest scanRequest =
+ new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(presenceScanFilter)
+ .build();
+ Log.d(
+ TAG,
+ String.format(
+ Locale.getDefault(),
+ "[PresenceManager] Start Presence scan with request: %s",
+ scanRequest.toString()));
+ manager.startScan(
+ scanRequest, Executors.newSingleThreadExecutor(), mScanCallback);
+ } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ Log.d(TAG, "PresenceManager Stop scan.");
+ manager.stopScan(mScanCallback);
+ }
+ }
+ };
+
+ public PresenceManager(Context context) {
+ mContext = context;
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Null when the Nearby Service is not available. */
+ @Nullable
+ private NearbyManager getNearbyManager() {
+ return (NearbyManager)
+ mContext.getApplicationContext()
+ .getSystemService(Context.NEARBY_SERVICE);
+ }
+
+ /** Function called when nearby service start. */
+ public void initiate() {
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
index f136695..3de6ff0 100644
--- a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanCallback;
import android.nearby.ScanFilter;
import android.nearby.ScanRequest;
import android.util.Log;
@@ -40,15 +41,6 @@
protected final DiscoveryProviderController mController;
protected final Executor mExecutor;
protected Listener mListener;
- protected List<ScanFilter> mScanFilters;
-
- /** Interface for listening to discovery providers. */
- public interface Listener {
- /**
- * Called when a provider has a new nearby device available. May be invoked from any thread.
- */
- void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
- }
protected AbstractDiscoveryProvider(Context context, Executor executor) {
mContext = context;
@@ -77,14 +69,33 @@
protected void invalidateScanMode() {}
/**
+ * Callback invoked to inform the provider of new provider scan filters which replaces any prior
+ * provider filters. Always invoked on the provider executor.
+ */
+ protected void onSetScanFilters(List<ScanFilter> filters) {}
+
+ /**
* Retrieves the controller for this discovery provider. Should never be invoked by subclasses,
* as a discovery provider should not be controlling itself. Using this method from subclasses
* could also result in deadlock.
*/
- protected DiscoveryProviderController getController() {
+ public DiscoveryProviderController getController() {
return mController;
}
+ /** Interface for listening to discovery providers. */
+ public interface Listener {
+ /**
+ * Called when a provider has a new nearby device available. May be invoked from any thread.
+ */
+ void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
+
+ /**
+ * Called when a provider found error from the scan.
+ */
+ void onError(@ScanCallback.ErrorCode int errorCode);
+ }
+
private class Controller implements DiscoveryProviderController {
private boolean mStarted = false;
@@ -120,6 +131,12 @@
mExecutor.execute(AbstractDiscoveryProvider.this::onStop);
}
+ @ScanRequest.ScanMode
+ @Override
+ public int getProviderScanMode() {
+ return mScanMode;
+ }
+
@Override
public void setProviderScanMode(@ScanRequest.ScanMode int scanMode) {
if (mScanMode == scanMode) {
@@ -130,15 +147,9 @@
mExecutor.execute(AbstractDiscoveryProvider.this::invalidateScanMode);
}
- @ScanRequest.ScanMode
- @Override
- public int getProviderScanMode() {
- return mScanMode;
- }
-
@Override
public void setProviderScanFilters(List<ScanFilter> filters) {
- mScanFilters = filters;
+ mExecutor.execute(() -> onSetScanFilters(filters));
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 67392ad..6829fba 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -16,17 +16,24 @@
package com.android.server.nearby.provider;
+import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSet;
+import android.bluetooth.le.AdvertisingSetCallback;
+import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.nearby.BroadcastCallback;
-import android.os.ParcelUuid;
+import android.nearby.BroadcastRequest;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
-import java.util.UUID;
import java.util.concurrent.Executor;
/**
@@ -37,7 +44,7 @@
/**
* Listener for Broadcast status changes.
*/
- interface BroadcastListener {
+ public interface BroadcastListener {
void onStatusChanged(int status);
}
@@ -46,13 +53,19 @@
private BroadcastListener mBroadcastListener;
private boolean mIsAdvertising;
-
- BleBroadcastProvider(Injector injector, Executor executor) {
+ @VisibleForTesting
+ AdvertisingSetCallback mAdvertisingSetCallback;
+ public BleBroadcastProvider(Injector injector, Executor executor) {
mInjector = injector;
mExecutor = executor;
+ mAdvertisingSetCallback = getAdvertisingSetCallback();
}
- void start(byte[] advertisementPackets, BroadcastListener listener) {
+ /**
+ * Starts to broadcast with given bytes.
+ */
+ public void start(@BroadcastRequest.BroadcastVersion int version, byte[] advertisementPackets,
+ BroadcastListener listener) {
if (mIsAdvertising) {
stop();
}
@@ -63,23 +76,35 @@
mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser != null) {
advertiseStarted = true;
- AdvertiseSettings settings =
- new AdvertiseSettings.Builder()
- .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
- .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
- .setConnectable(true)
- .build();
-
- // TODO(b/230538655) Use empty data until Presence V1 protocol is implemented.
- ParcelUuid emptyParcelUuid = new ParcelUuid(new UUID(0L, 0L));
- byte[] emptyAdvertisementPackets = new byte[0];
AdvertiseData advertiseData =
new AdvertiseData.Builder()
- .addServiceData(emptyParcelUuid, emptyAdvertisementPackets).build();
+ .addServiceData(PRESENCE_UUID, advertisementPackets).build();
try {
mBroadcastListener = listener;
- bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
+ switch (version) {
+ case BroadcastRequest.PRESENCE_VERSION_V0:
+ bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
+ advertiseData, this);
+ break;
+ case BroadcastRequest.PRESENCE_VERSION_V1:
+ if (adapter.isLeExtendedAdvertisingSupported()) {
+ bluetoothLeAdvertiser.startAdvertisingSet(
+ getAdvertisingSetParameters(),
+ advertiseData,
+ null, null, null, mAdvertisingSetCallback);
+ } else {
+ Log.w(TAG, "Failed to start advertising set because the chipset"
+ + " does not supports LE Extended Advertising feature.");
+ advertiseStarted = false;
+ }
+ break;
+ default:
+ Log.w(TAG, "Failed to start advertising set because the advertisement"
+ + " is wrong.");
+ advertiseStarted = false;
+ }
} catch (NullPointerException | IllegalStateException | SecurityException e) {
+ Log.w(TAG, "Failed to start advertising.", e);
advertiseStarted = false;
}
}
@@ -89,7 +114,10 @@
}
}
- void stop() {
+ /**
+ * Stops current advertisement.
+ */
+ public void stop() {
if (mIsAdvertising) {
BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
if (adapter != null) {
@@ -97,6 +125,7 @@
mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser != null) {
bluetoothLeAdvertiser.stopAdvertising(this);
+ bluetoothLeAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
}
}
mBroadcastListener = null;
@@ -120,4 +149,41 @@
mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
}
}
+
+ private static AdvertiseSettings getAdvertiseSettings() {
+ return new AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .setConnectable(true)
+ .build();
+ }
+
+ private static AdvertisingSetParameters getAdvertisingSetParameters() {
+ return new AdvertisingSetParameters.Builder()
+ .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
+ .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM)
+ .setIncludeTxPower(true)
+ .setConnectable(true)
+ .build();
+ }
+
+ private AdvertisingSetCallback getAdvertisingSetCallback() {
+ return new AdvertisingSetCallback() {
+ @Override
+ public void onAdvertisingSetStarted(AdvertisingSet advertisingSet,
+ int txPower, int status) {
+ if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_OK);
+ }
+ mIsAdvertising = true;
+ } else {
+ Log.e(TAG, "Starts advertising failed in status " + status);
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+ }
+ }
+ }
+ };
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index e2fbe77..355f7cf 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -16,6 +16,8 @@
package com.android.server.nearby.provider;
+import static android.nearby.ScanCallback.ERROR_UNKNOWN;
+
import static com.android.server.nearby.NearbyService.TAG;
import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
@@ -28,18 +30,27 @@
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
+import android.nearby.DataElement;
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
import android.nearby.ScanRequest;
import android.os.ParcelUuid;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
+import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.ForegroundThread;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
@@ -52,15 +63,32 @@
// Don't block the thread as it may be used by other services.
private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
private final Injector mInjector;
+ private final Object mLock = new Object();
+ // Null when the filters are never set
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @Nullable
+ private List<android.nearby.ScanFilter> mScanFilters;
+ private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
+ new android.bluetooth.le.ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult scanResult) {
+ }
+ @Override
+ public void onScanFailed(int errorCode) {
+ }
+ };
private android.bluetooth.le.ScanCallback mScanCallback =
new android.bluetooth.le.ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult scanResult) {
NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
- builder.setMedium(NearbyDevice.Medium.BLE)
+ String bleAddress = scanResult.getDevice().getAddress();
+ builder.setDeviceId(bleAddress.hashCode())
+ .setMedium(NearbyDevice.Medium.BLE)
.setRssi(scanResult.getRssi())
.setTxPower(scanResult.getTxPower())
- .setBluetoothAddress(scanResult.getDevice().getAddress());
+ .setBluetoothAddress(bleAddress);
ScanRecord record = scanResult.getScanRecord();
if (record != null) {
@@ -72,7 +100,8 @@
if (serviceDataMap != null) {
byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
if (presenceData != null) {
- builder.setData(serviceDataMap.get(PRESENCE_UUID));
+ setPresenceDevice(presenceData, builder, deviceName,
+ scanResult.getRssi());
}
}
}
@@ -81,7 +110,8 @@
@Override
public void onScanFailed(int errorCode) {
- Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+ Log.w(TAG, "BLE 5.0 Scan failed with error code " + errorCode);
+ mExecutor.execute(() -> mListener.onError(ERROR_UNKNOWN));
}
};
@@ -90,6 +120,29 @@
mInjector = injector;
}
+ private static PresenceDevice getPresenceDevice(ExtendedAdvertisement advertisement,
+ String deviceName, int rssi) {
+ // TODO(238458326): After implementing encryption, use real data.
+ byte[] secretIdBytes = new byte[0];
+ PresenceDevice.Builder builder =
+ new PresenceDevice.Builder(
+ String.valueOf(advertisement.hashCode()),
+ advertisement.getSalt(),
+ secretIdBytes,
+ advertisement.getIdentity())
+ .addMedium(NearbyDevice.Medium.BLE)
+ .setName(deviceName)
+ .setRssi(rssi);
+ for (int i : advertisement.getActions()) {
+ builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
+ new byte[]{(byte) i}));
+ }
+ for (DataElement dataElement : advertisement.getDataElements()) {
+ builder.addExtendedProperty(dataElement);
+ }
+ return builder.build();
+ }
+
private static List<ScanFilter> getScanFilters() {
List<ScanFilter> scanFilterList = new ArrayList<>();
scanFilterList.add(
@@ -120,8 +173,9 @@
@Override
protected void onStart() {
if (isBleAvailable()) {
- Log.d(TAG, "BleDiscoveryProvider started.");
- startScan(getScanFilters(), getScanSettings(), mScanCallback);
+ Log.d(TAG, "BleDiscoveryProvider started");
+ startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
+ startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
return;
}
Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -138,6 +192,12 @@
}
Log.v(TAG, "Ble scan stopped.");
bluetoothLeScanner.stopScan(mScanCallback);
+ bluetoothLeScanner.stopScan(mScanCallbackLegacy);
+ synchronized (mLock) {
+ if (mScanFilters != null) {
+ mScanFilters = null;
+ }
+ }
}
@Override
@@ -146,6 +206,20 @@
onStart();
}
+ @Override
+ protected void onSetScanFilters(List<android.nearby.ScanFilter> filters) {
+ synchronized (mLock) {
+ mScanFilters = filters == null ? null : List.copyOf(filters);
+ }
+ }
+
+ @VisibleForTesting
+ protected List<android.nearby.ScanFilter> getFiltersLocked() {
+ synchronized (mLock) {
+ return mScanFilters == null ? null : List.copyOf(mScanFilters);
+ }
+ }
+
private void startScan(
List<ScanFilter> scanFilters, ScanSettings scanSettings,
android.bluetooth.le.ScanCallback scanCallback) {
@@ -169,7 +243,7 @@
}
}
- private ScanSettings getScanSettings() {
+ private ScanSettings getScanSettings(boolean legacy) {
int bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
switch (mController.getProviderScanMode()) {
case ScanRequest.SCAN_MODE_LOW_LATENCY:
@@ -185,11 +259,45 @@
bleScanMode = ScanSettings.SCAN_MODE_OPPORTUNISTIC;
break;
}
- return new ScanSettings.Builder().setScanMode(bleScanMode).build();
+ return new ScanSettings.Builder().setScanMode(bleScanMode).setLegacy(legacy).build();
}
@VisibleForTesting
ScanCallback getScanCallback() {
return mScanCallback;
}
+
+ private void setPresenceDevice(byte[] data, NearbyDeviceParcelable.Builder builder,
+ String deviceName, int rssi) {
+ synchronized (mLock) {
+ if (mScanFilters == null) {
+ return;
+ }
+ for (android.nearby.ScanFilter scanFilter : mScanFilters) {
+ if (scanFilter instanceof PresenceScanFilter) {
+ // Iterate all possible authenticity key and identity combinations to decrypt
+ // advertisement
+ PresenceScanFilter presenceFilter = (PresenceScanFilter) scanFilter;
+ for (PublicCredential credential : presenceFilter.getCredentials()) {
+ ExtendedAdvertisement advertisement =
+ ExtendedAdvertisement.fromBytes(data, credential);
+ if (advertisement == null) {
+ continue;
+ }
+ if (CryptorImpIdentityV1.getInstance().verify(
+ advertisement.getIdentity(),
+ credential.getEncryptedMetadataKeyTag())) {
+ builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+ rssi));
+ builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+ if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+ builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
index 5077ffe..020c7b2 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -19,15 +19,18 @@
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.Nullable;
+import android.content.Context;
import android.hardware.location.ContextHubClient;
import android.hardware.location.ContextHubClientCallback;
import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.util.Log;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.google.common.base.Preconditions;
@@ -36,7 +39,9 @@
import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
/**
* Responsible for setting up communication with the appropriate contexthub on the device and
@@ -44,6 +49,8 @@
*/
public class ChreCommunication extends ContextHubClientCallback {
+ public static final int INVALID_NANO_APP_VERSION = -1;
+
/** Callback that receives messages forwarded from the context hub. */
public interface ContextHubCommsCallback {
/** Indicates whether {@link ChreCommunication} was started successfully. */
@@ -63,19 +70,30 @@
}
private final Injector mInjector;
+ private final Context mContext;
private final Executor mExecutor;
private boolean mStarted = false;
+ // null when CHRE availability result has not been returned
+ @Nullable private Boolean mChreSupport = null;
+ private long mNanoAppVersion = INVALID_NANO_APP_VERSION;
@Nullable private ContextHubCommsCallback mCallback;
@Nullable private ContextHubClient mContextHubClient;
+ private CountDownLatch mCountDownLatch;
- public ChreCommunication(Injector injector, Executor executor) {
+ public ChreCommunication(Injector injector, Context context, Executor executor) {
mInjector = injector;
+ mContext = context;
mExecutor = executor;
}
- public boolean available() {
- return mContextHubClient != null;
+ /**
+ * @return {@code true} if NanoApp is available and {@code null} when CHRE availability result
+ * has not been returned
+ */
+ @Nullable
+ public Boolean available() {
+ return mChreSupport;
}
/**
@@ -86,12 +104,12 @@
* contexthub.
*/
public synchronized void start(ContextHubCommsCallback callback, Set<Long> nanoAppIds) {
- ContextHubManagerAdapter manager = mInjector.getContextHubManagerAdapter();
+ ContextHubManager manager = mInjector.getContextHubManager();
if (manager == null) {
Log.e(TAG, "ContexHub not available in this device");
return;
} else {
- Log.i(TAG, "Start ChreCommunication");
+ Log.i(TAG, "[ChreCommunication] Start ChreCommunication");
}
Preconditions.checkNotNull(callback);
Preconditions.checkArgument(!nanoAppIds.isEmpty());
@@ -134,6 +152,7 @@
if (mContextHubClient != null) {
mContextHubClient.close();
mContextHubClient = null;
+ mChreSupport = null;
}
}
@@ -156,6 +175,25 @@
return true;
}
+ /**
+ * Checks the Nano App version
+ */
+ public long queryNanoAppVersion() {
+ if (mCountDownLatch == null || mCountDownLatch.getCount() == 0) {
+ // already gets result from CHRE
+ return mNanoAppVersion;
+ }
+ try {
+ boolean success = mCountDownLatch.await(1, TimeUnit.SECONDS);
+ if (!success) {
+ Log.w(TAG, "Failed to get ContextHubTransaction result before the timeout.");
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return mNanoAppVersion;
+ }
+
@Override
public synchronized void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
mCallback.onMessageFromNanoApp(message);
@@ -172,7 +210,8 @@
mCallback.onNanoAppRestart(nanoAppId);
}
- private static String contextHubTransactionResultToString(int result) {
+ @VisibleForTesting
+ static String contextHubTransactionResultToString(int result) {
switch (result) {
case ContextHubTransaction.RESULT_SUCCESS:
return "RESULT_SUCCESS";
@@ -207,13 +246,13 @@
private final ContextHubInfo mQueriedContextHub;
private final List<ContextHubInfo> mContextHubs;
private final Set<Long> mNanoAppIds;
- private final ContextHubManagerAdapter mManager;
+ private final ContextHubManager mManager;
OnQueryCompleteListener(
ContextHubInfo queriedContextHub,
List<ContextHubInfo> contextHubs,
Set<Long> nanoAppIds,
- ContextHubManagerAdapter manager) {
+ ContextHubManager manager) {
this.mQueriedContextHub = queriedContextHub;
this.mContextHubs = contextHubs;
this.mNanoAppIds = nanoAppIds;
@@ -231,21 +270,32 @@
return;
}
+ mCountDownLatch = new CountDownLatch(1);
if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
for (NanoAppState state : response.getContents()) {
+ long version = state.getNanoAppVersion();
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ long minVersion = configuration.getNanoAppMinVersion();
+ if (version < minVersion) {
+ Log.w(TAG, String.format("Current nano app version is %s, which does not "
+ + "meet minimum version required %s", version, minVersion));
+ continue;
+ }
if (mNanoAppIds.contains(state.getNanoAppId())) {
Log.i(
TAG,
String.format(
"Found valid contexthub: %s", mQueriedContextHub.getId()));
- mContextHubClient =
- mManager.createClient(
- mQueriedContextHub, ChreCommunication.this, mExecutor);
+ mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,
+ mExecutor, ChreCommunication.this);
+ mChreSupport = true;
mCallback.started(true);
+ mNanoAppVersion = version;
+ mCountDownLatch.countDown();
return;
}
}
- Log.e(
+ Log.i(
TAG,
String.format(
"Didn't find the nanoapp on contexthub: %s",
@@ -259,10 +309,12 @@
}
mContextHubs.remove(mQueriedContextHub);
+ mCountDownLatch.countDown();
// If this is the last context hub response left to receive, indicate that
// there isn't a valid context available on this device.
if (mContextHubs.isEmpty()) {
mCallback.started(false);
+ mChreSupport = false;
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index f20c6d8..7ab0523 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,20 +20,33 @@
import static com.android.server.nearby.NearbyService.TAG;
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.location.NanoAppMessage;
+import android.nearby.DataElement;
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.OffloadCapability;
+import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
import android.nearby.PublicCredential;
import android.nearby.ScanFilter;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
-import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ByteString;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executor;
import service.proto.Blefilter;
@@ -42,52 +55,122 @@
public class ChreDiscoveryProvider extends AbstractDiscoveryProvider {
// Nanoapp ID reserved for Nearby Presence.
/** @hide */
- @VisibleForTesting public static final long NANOAPP_ID = 0x476f6f676c001031L;
+ @VisibleForTesting
+ public static final long NANOAPP_ID = 0x476f6f676c001031L;
/** @hide */
- @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
/** @hide */
- @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+ /** @hide */
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_CONFIG = 5;
- private static final int PRESENCE_UUID = 0xFCF1;
+ private final ChreCommunication mChreCommunication;
+ private final ChreCallback mChreCallback;
+ private final Object mLock = new Object();
- private ChreCommunication mChreCommunication;
- private ChreCallback mChreCallback;
private boolean mChreStarted = false;
- private Blefilter.BleFilters mFilters = null;
- private int mFilterId;
+ private Context mContext;
+ private NearbyConfiguration mNearbyConfiguration;
+ private final IntentFilter mIntentFilter;
+ // Null when CHRE not started and the filters are never set. Empty the list every time the scan
+ // stops.
+ @GuardedBy("mLock")
+ @Nullable
+ private List<ScanFilter> mScanFilters;
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Boolean screenOn = intent.getAction().equals(Intent.ACTION_SCREEN_ON)
+ || intent.getAction().equals(Intent.ACTION_USER_PRESENT);
+ Log.d(TAG, String.format(
+ "[ChreDiscoveryProvider] update nanoapp screen status: %B", screenOn));
+ sendScreenUpdate(screenOn);
+ }
+ };
public ChreDiscoveryProvider(
Context context, ChreCommunication chreCommunication, Executor executor) {
super(context, executor);
+ mContext = context;
mChreCommunication = chreCommunication;
mChreCallback = new ChreCallback();
- mFilterId = 0;
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Initialize the CHRE discovery provider. */
+ public void init() {
+ mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
+ mNearbyConfiguration = new NearbyConfiguration();
}
@Override
protected void onStart() {
Log.d(TAG, "Start CHRE scan");
- mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
- updateFilters();
+ synchronized (mLock) {
+ updateFiltersLocked();
+ }
}
@Override
protected void onStop() {
- mChreStarted = false;
- mChreCommunication.stop();
+ Log.d(TAG, "Stop CHRE scan");
+ synchronized (mLock) {
+ if (mScanFilters != null) {
+ // Cleaning the filters by assigning an empty list
+ mScanFilters = List.of();
+ }
+ updateFiltersLocked();
+ }
}
@Override
- protected void invalidateScanMode() {
- onStop();
- onStart();
+ protected void onSetScanFilters(List<ScanFilter> filters) {
+ synchronized (mLock) {
+ mScanFilters = filters == null ? null : List.copyOf(filters);
+ updateFiltersLocked();
+ }
}
- public boolean available() {
+ /**
+ * @return {@code true} if CHRE is available and {@code null} when CHRE availability result
+ * has not been returned
+ */
+ @Nullable
+ public Boolean available() {
return mChreCommunication.available();
}
- private synchronized void updateFilters() {
+ /**
+ * Query offload capability in a device.
+ */
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ OffloadCapability.Builder builder = new OffloadCapability.Builder();
+ mExecutor.execute(() -> {
+ long version = mChreCommunication.queryNanoAppVersion();
+ builder.setVersion(version);
+ builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
+ try {
+ callback.onQueryComplete(builder.build());
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public List<ScanFilter> getFiltersLocked() {
+ synchronized (mLock) {
+ return mScanFilters == null ? null : List.copyOf(mScanFilters);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void updateFiltersLocked() {
if (mScanFilters == null) {
Log.e(TAG, "ScanFilters not set.");
return;
@@ -95,29 +178,69 @@
Blefilter.BleFilters.Builder filtersBuilder = Blefilter.BleFilters.newBuilder();
for (ScanFilter scanFilter : mScanFilters) {
PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
- Blefilter.BleFilter filter =
- Blefilter.BleFilter.newBuilder()
- .setId(mFilterId)
- .setUuid(PRESENCE_UUID)
- .setIntent(presenceScanFilter.getPresenceActions().get(0))
- .build();
- filtersBuilder.addFilter(filter);
- mFilterId++;
+ Blefilter.BleFilter.Builder filterBuilder = Blefilter.BleFilter.newBuilder();
+ for (PublicCredential credential : presenceScanFilter.getCredentials()) {
+ filterBuilder.addCertificate(toProtoPublicCredential(credential));
+ }
+ for (DataElement dataElement : presenceScanFilter.getExtendedProperties()) {
+ if (dataElement.getKey() == DataElement.DataType.ACCOUNT_KEY_DATA) {
+ filterBuilder.addDataElement(toProtoDataElement(dataElement));
+ } else if (mNearbyConfiguration.isTestAppSupported()
+ && DataElement.isTestDeType(dataElement.getKey())) {
+ filterBuilder.addDataElement(toProtoDataElement(dataElement));
+ }
+ }
+ if (!presenceScanFilter.getPresenceActions().isEmpty()) {
+ filterBuilder.setIntent(presenceScanFilter.getPresenceActions().get(0));
+ }
+ filtersBuilder.addFilter(filterBuilder.build());
}
- mFilters = filtersBuilder.build();
if (mChreStarted) {
- sendFilters(mFilters);
- mFilters = null;
+ sendFilters(filtersBuilder.build());
}
}
+ private Blefilter.PublicateCertificate toProtoPublicCredential(PublicCredential credential) {
+ Log.d(TAG, String.format("Returns a PublicCertificate with authenticity key size %d and"
+ + " encrypted metadata key tag size %d",
+ credential.getAuthenticityKey().length,
+ credential.getEncryptedMetadataKeyTag().length));
+ return Blefilter.PublicateCertificate.newBuilder()
+ .setAuthenticityKey(ByteString.copyFrom(credential.getAuthenticityKey()))
+ .setMetadataEncryptionKeyTag(
+ ByteString.copyFrom(credential.getEncryptedMetadataKeyTag()))
+ .build();
+ }
+
+ private Blefilter.DataElement toProtoDataElement(DataElement dataElement) {
+ return Blefilter.DataElement.newBuilder()
+ .setKey(dataElement.getKey())
+ .setValue(ByteString.copyFrom(dataElement.getValue()))
+ .setValueLength(dataElement.getValue().length)
+ .build();
+ }
+
private void sendFilters(Blefilter.BleFilters filters) {
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER, filters.toByteArray());
- if (!mChreCommunication.sendMessageToNanoApp(message)) {
- Log.e(TAG, "Failed to send filters to CHRE.");
+ if (mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.v(TAG, "Successfully sent filters to CHRE.");
+ return;
}
+ Log.e(TAG, "Failed to send filters to CHRE.");
+ }
+
+ private void sendScreenUpdate(Boolean screenOn) {
+ Blefilter.BleConfig config = Blefilter.BleConfig.newBuilder().setScreenOn(screenOn).build();
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ NANOAPP_ID, NANOAPP_MESSAGE_TYPE_CONFIG, config.toByteArray());
+ if (mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.v(TAG, "Successfully sent config to CHRE.");
+ return;
+ }
+ Log.e(TAG, "Failed to send config to CHRE.");
}
private class ChreCallback implements ChreCommunication.ContextHubCommsCallback {
@@ -127,11 +250,11 @@
if (success) {
synchronized (ChreDiscoveryProvider.this) {
Log.i(TAG, "CHRE communication started");
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_USER_PRESENT);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
mChreStarted = true;
- if (mFilters != null) {
- sendFilters(mFilters);
- mFilters = null;
- }
}
}
}
@@ -163,32 +286,123 @@
Blefilter.BleFilterResults results =
Blefilter.BleFilterResults.parseFrom(message.getMessageBody());
for (Blefilter.BleFilterResult filterResult : results.getResultList()) {
- Blefilter.PublicCredential credential = filterResult.getPublicCredential();
+ // TODO(b/234653356): There are some duplicate fields set both in
+ // PresenceDevice and NearbyDeviceParcelable, cleanup is needed.
+ byte[] salt = {1};
+ byte[] secretId = {1};
+ byte[] authenticityKey = {1};
+ byte[] publicKey = {1};
+ byte[] encryptedMetaData = {1};
+ byte[] encryptedMetaDataTag = {1};
+ if (filterResult.hasPublicCredential()) {
+ Blefilter.PublicCredential credential =
+ filterResult.getPublicCredential();
+ secretId = credential.getSecretId().toByteArray();
+ authenticityKey = credential.getAuthenticityKey().toByteArray();
+ publicKey = credential.getPublicKey().toByteArray();
+ encryptedMetaData = credential.getEncryptedMetadata().toByteArray();
+ encryptedMetaDataTag =
+ credential.getEncryptedMetadataTag().toByteArray();
+ }
+ PresenceDevice.Builder presenceDeviceBuilder =
+ new PresenceDevice.Builder(
+ String.valueOf(filterResult.hashCode()),
+ salt,
+ secretId,
+ encryptedMetaData)
+ .setRssi(filterResult.getRssi())
+ .addMedium(NearbyDevice.Medium.BLE);
+ // Data Elements reported from nanoapp added to Data Elements.
+ // i.e. Fast Pair account keys, connection status and battery
+ for (Blefilter.DataElement element : filterResult.getDataElementList()) {
+ addDataElementsToPresenceDevice(element, presenceDeviceBuilder);
+ }
+ // BlE address appended to Data Element.
+ if (filterResult.hasBluetoothAddress()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.BLE_ADDRESS,
+ filterResult.getBluetoothAddress().toByteArray()));
+ }
+ // BlE TX Power appended to Data Element.
+ if (filterResult.hasTxPower()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.TX_POWER,
+ new byte[]{(byte) filterResult.getTxPower()}));
+ }
+ // BLE Service data appended to Data Elements.
+ if (filterResult.hasBleServiceData()) {
+ // Retrieves the length of the service data from the first byte,
+ // and then skips the first byte and returns data[1 .. dataLength)
+ // as the DataElement value.
+ int dataLength = Byte.toUnsignedInt(
+ filterResult.getBleServiceData().byteAt(0));
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.BLE_SERVICE_DATA,
+ filterResult.getBleServiceData()
+ .substring(1, 1 + dataLength).toByteArray()));
+ }
+ // Add action
+ if (filterResult.hasIntent()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.ACTION,
+ new byte[]{(byte) filterResult.getIntent()}));
+ }
+
PublicCredential publicCredential =
new PublicCredential.Builder(
- credential.getSecretId().toByteArray(),
- credential.getAuthenticityKey().toByteArray(),
- credential.getPublicKey().toByteArray(),
- credential.getEncryptedMetadata().toByteArray(),
- credential.getEncryptedMetadataTag().toByteArray())
+ secretId,
+ authenticityKey,
+ publicKey,
+ encryptedMetaData,
+ encryptedMetaDataTag)
.build();
+
NearbyDeviceParcelable device =
new NearbyDeviceParcelable.Builder()
+ .setDeviceId(Arrays.hashCode(secretId))
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setMedium(NearbyDevice.Medium.BLE)
.setTxPower(filterResult.getTxPower())
.setRssi(filterResult.getRssi())
.setAction(filterResult.getIntent())
.setPublicCredential(publicCredential)
+ .setPresenceDevice(presenceDeviceBuilder.build())
+ .setEncryptionKeyTag(encryptedMetaDataTag)
.build();
mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(device));
}
- } catch (InvalidProtocolBufferException e) {
- Log.e(
- TAG,
- String.format("Failed to decode the filter result %s", e.toString()));
+ } catch (Exception e) {
+ Log.e(TAG, String.format("Failed to decode the filter result %s", e));
}
}
}
+
+ private void addDataElementsToPresenceDevice(Blefilter.DataElement element,
+ PresenceDevice.Builder presenceDeviceBuilder) {
+ int endIndex = element.hasValueLength() ? element.getValueLength() :
+ element.getValue().size();
+ int key = element.getKey();
+ switch (key) {
+ case DataElement.DataType.ACCOUNT_KEY_DATA:
+ case DataElement.DataType.CONNECTION_STATUS:
+ case DataElement.DataType.BATTERY:
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(key,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ break;
+ default:
+ if (mNearbyConfiguration.isTestAppSupported()
+ && DataElement.isTestDeType(key)) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(key,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ }
+ break;
+ }
+ }
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
index fa1a874..71ffda5 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
@@ -23,7 +23,7 @@
import java.util.List;
/** Interface for controlling discovery providers. */
-interface DiscoveryProviderController {
+public interface DiscoveryProviderController {
/**
* Sets the listener which can expect to receive all state updates from after this point. May be
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
deleted file mode 100644
index bdeab51..0000000
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.provider;
-
-import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.annotation.Nullable;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.nearby.IScanListener;
-import android.nearby.NearbyDeviceParcelable;
-import android.nearby.PresenceScanFilter;
-import android.nearby.ScanFilter;
-import android.nearby.ScanRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.metrics.NearbyMetrics;
-import com.android.server.nearby.presence.PresenceDiscoveryResult;
-import com.android.server.nearby.util.identity.CallerIdentity;
-import com.android.server.nearby.util.permissions.DiscoveryPermissions;
-
-import java.util.ArrayList;
-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.Executors;
-import java.util.stream.Collectors;
-
-/** Manages all aspects of discovery providers. */
-public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
-
- protected final Object mLock = new Object();
- private final Context mContext;
- private final BleDiscoveryProvider mBleDiscoveryProvider;
- @Nullable private final ChreDiscoveryProvider mChreDiscoveryProvider;
- private @ScanRequest.ScanMode int mScanMode;
- private final Injector mInjector;
-
- @GuardedBy("mLock")
- private Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
-
- @Override
- public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
- synchronized (mLock) {
- AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- if (record == null) {
- Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
- continue;
- }
- CallerIdentity callerIdentity = record.getCallerIdentity();
- if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
- appOpsManager, callerIdentity)) {
- Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
- + "- not forwarding results");
- try {
- record.getScanListener().onError();
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
- }
- return;
- }
-
- if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- List<ScanFilter> presenceFilters =
- record.getScanRequest().getScanFilters().stream()
- .filter(
- scanFilter ->
- scanFilter.getType()
- == SCAN_TYPE_NEARBY_PRESENCE)
- .collect(Collectors.toList());
- Log.i(
- TAG,
- String.format("match with filters size: %d", presenceFilters.size()));
- if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
- continue;
- }
- }
- try {
- record.getScanListener()
- .onDiscovered(
- PrivacyFilter.filter(
- record.getScanRequest().getScanType(), nearbyDevice));
- NearbyMetrics.logScanDeviceDiscovered(
- record.hashCode(), record.getScanRequest(), nearbyDevice);
- } catch (RemoteException e) {
- Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
- }
- }
- }
- }
-
- public DiscoveryProviderManager(Context context, Injector injector) {
- mContext = context;
- mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
- Executor executor = Executors.newSingleThreadExecutor();
- mChreDiscoveryProvider =
- new ChreDiscoveryProvider(
- mContext, new ChreCommunication(injector, executor), executor);
- mScanTypeScanListenerRecordMap = new HashMap<>();
- mInjector = injector;
- }
-
- /**
- * Registers the listener in the manager and starts scan according to the requested scan mode.
- */
- public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener,
- CallerIdentity callerIdentity) {
- synchronized (mLock) {
- IBinder listenerBinder = listener.asBinder();
- if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
- ScanRequest savedScanRequest =
- mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
- if (scanRequest.equals(savedScanRequest)) {
- Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
- return true;
- }
- }
- ScanListenerRecord scanListenerRecord =
- new ScanListenerRecord(scanRequest, listener, callerIdentity);
- mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
-
- if (!startProviders(scanRequest)) {
- return false;
- }
-
- NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
- if (mScanMode < scanRequest.getScanMode()) {
- mScanMode = scanRequest.getScanMode();
- invalidateProviderScanMode();
- }
- return true;
- }
- }
-
- /**
- * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
- */
- public void unregisterScanListener(IScanListener listener) {
- IBinder listenerBinder = listener.asBinder();
- synchronized (mLock) {
- if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
- Log.w(
- TAG,
- "Cannot unregister the scanRequest because the request is never "
- + "registered.");
- return;
- }
-
- ScanListenerRecord removedRecord =
- mScanTypeScanListenerRecordMap.remove(listenerBinder);
- Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
- NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
- if (mScanTypeScanListenerRecordMap.isEmpty()) {
- Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
- + "scan listener registered.");
- stopProviders();
- return;
- }
-
- // TODO(b/221082271): updates the scan with reduced filters.
-
- // Removes current highest scan mode requested and sets the next highest scan mode.
- if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
- Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
- + "because the highest scan mode listener was unregistered.");
- @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
- // find the next highest scan mode;
- for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
- @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
- if (scanMode > highestScanModeRequested) {
- highestScanModeRequested = scanMode;
- }
- }
- if (mScanMode != highestScanModeRequested) {
- mScanMode = highestScanModeRequested;
- invalidateProviderScanMode();
- }
- }
- }
- }
-
- // Returns false when fail to start all the providers. Returns true if any one of the provider
- // starts successfully.
- private boolean startProviders(ScanRequest scanRequest) {
- if (scanRequest.isBleEnabled()) {
- if (mChreDiscoveryProvider.available()
- && scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- startChreProvider();
- } else {
- startBleProvider(scanRequest);
- }
- return true;
- }
- return false;
- }
-
- private void startBleProvider(ScanRequest scanRequest) {
- if (!mBleDiscoveryProvider.getController().isStarted()) {
- Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
- mBleDiscoveryProvider.getController().start();
- mBleDiscoveryProvider.getController().setListener(this);
- mBleDiscoveryProvider.getController().setProviderScanMode(scanRequest.getScanMode());
- }
- }
-
- private void startChreProvider() {
- Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
- synchronized (mLock) {
- mChreDiscoveryProvider.getController().setListener(this);
- List<ScanFilter> scanFilters = new ArrayList();
- for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
- ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
- List<ScanFilter> presenceFilters =
- record.getScanRequest().getScanFilters().stream()
- .filter(
- scanFilter ->
- scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
- .collect(Collectors.toList());
- scanFilters.addAll(presenceFilters);
- }
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
- mChreDiscoveryProvider.getController().start();
- }
- }
-
- private void stopProviders() {
- stopBleProvider();
- stopChreProvider();
- }
-
- private void stopBleProvider() {
- mBleDiscoveryProvider.getController().stop();
- }
-
- private void stopChreProvider() {
- mChreDiscoveryProvider.getController().stop();
- }
-
- private void invalidateProviderScanMode() {
- if (mBleDiscoveryProvider.getController().isStarted()) {
- mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
- } else {
- Log.d(
- TAG,
- "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
- + "started.");
- }
- }
-
- private static boolean presenceFilterMatches(
- NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
- if (scanFilters.isEmpty()) {
- return true;
- }
- PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
- for (ScanFilter scanFilter : scanFilters) {
- PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
- if (discoveryResult.matches(presenceScanFilter)) {
- return true;
- }
- }
- return false;
- }
-
- private static class ScanListenerRecord {
-
- private final ScanRequest mScanRequest;
-
- private final IScanListener mScanListener;
-
- private final CallerIdentity mCallerIdentity;
-
- ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
- CallerIdentity callerIdentity) {
- mScanListener = iScanListener;
- mScanRequest = scanRequest;
- mCallerIdentity = callerIdentity;
- }
-
- IScanListener getScanListener() {
- return mScanListener;
- }
-
- ScanRequest getScanRequest() {
- return mScanRequest;
- }
-
- CallerIdentity getCallerIdentity() {
- return mCallerIdentity;
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof ScanListenerRecord) {
- ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
- return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
- && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mScanListener, mScanRequest);
- }
- }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 599843c..35251d8 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -45,4 +45,11 @@
}
return result;
}
+
+ /**
+ * @return true when the array is null or length is 0
+ */
+ public static boolean isEmpty(byte[] bytes) {
+ return bytes == null || bytes.length == 0;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
new file mode 100644
index 0000000..3c5132d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Class for encryption/decryption functionality. */
+public abstract class Cryptor {
+
+ /** AES only supports key sizes of 16, 24 or 32 bytes. */
+ static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
+
+ private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+ /**
+ * Encrypt the provided data blob.
+ *
+ * @param data data blob to be encrypted.
+ * @param salt used for IV
+ * @param secretKeyBytes secrete key accessed from credentials
+ * @return encrypted data, {@code null} if failed to encrypt.
+ */
+ @Nullable
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+ return data;
+ }
+
+ /**
+ * Decrypt the original data blob from the provided byte array.
+ *
+ * @param encryptedData data blob to be decrypted.
+ * @param salt used for IV
+ * @param secretKeyBytes secrete key accessed from credentials
+ * @return decrypted data, {@code null} if failed to decrypt.
+ */
+ @Nullable
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+ return encryptedData;
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ *
+ * @return signature {@code null} if failed to sign
+ */
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ return new byte[0];
+ }
+
+ /**
+ * Verifies the signature generated by data and key, with the original signed data
+ */
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return true;
+ }
+
+ /**
+ * @return length of the signature generated
+ */
+ public int getSignatureLength() {
+ return 0;
+ }
+
+ /**
+ * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
+ * given size.
+ */
+ // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+ @Nullable
+ static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
+ Mac mac;
+ try {
+ mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(TAG, "HMAC_SHA256_ALGORITHM is not supported.", e);
+ return null;
+ }
+
+ if (size > 255 * mac.getMacLength()) {
+ Log.w(TAG, "Size too large.");
+ return null;
+ }
+
+ if (salt.length == 0) {
+ Log.w(TAG, "Salt cannot be empty.");
+ return null;
+ }
+
+ try {
+ mac.init(new SecretKeySpec(salt, HMAC_SHA256_ALGORITHM));
+ } catch (InvalidKeyException e) {
+ Log.w(TAG, "Invalid key.", e);
+ return null;
+ }
+
+ byte[] prk = mac.doFinal(ikm);
+ byte[] result = new byte[size];
+ try {
+ mac.init(new SecretKeySpec(prk, HMAC_SHA256_ALGORITHM));
+ } catch (InvalidKeyException e) {
+ Log.w(TAG, "Invalid key.", e);
+ return null;
+ }
+
+ byte[] digest = new byte[0];
+ int ctr = 1;
+ int pos = 0;
+ while (true) {
+ mac.update(digest);
+ mac.update((byte) ctr);
+ digest = mac.doFinal();
+ if (pos + digest.length < size) {
+ System.arraycopy(digest, 0, result, pos, digest.length);
+ pos += digest.length;
+ ctr++;
+ } else {
+ System.arraycopy(digest, 0, result, pos, size - pos);
+ break;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
new file mode 100644
index 0000000..1c0ec9e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A Cryptor that returns the original data without actual encryption
+ */
+public class CryptorImpFake extends Cryptor {
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable
+ private static CryptorImpFake sCryptor;
+
+ /** Returns an instance of CryptorImpFake. */
+ public static CryptorImpFake getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpFake();
+ }
+ return sCryptor;
+ }
+
+ private CryptorImpFake() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
new file mode 100644
index 0000000..b0e19b4
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
+ * encryption and decryption.
+ */
+public class CryptorImpIdentityV1 extends Cryptor {
+
+ // 3 16 byte arrays known by both the encryptor and decryptor.
+ private static final byte[] EK_IV =
+ new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
+ private static final byte[] ESALT_IV =
+ new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
+ private static final byte[] KTAG_IV =
+ {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
+
+ /** Length of encryption key required by AES/GCM encryption. */
+ private static final int ENCRYPTION_KEY_SIZE = 32;
+
+ /** Length of salt required by AES/GCM encryption. */
+ private static final int AES_CTR_IV_SIZE = 16;
+
+ /** Length HMAC tag */
+ public static final int HMAC_TAG_SIZE = 8;
+
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ @VisibleForTesting
+ static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable private static CryptorImpIdentityV1 sCryptor;
+
+ /** Returns an instance of CryptorImpIdentityV1. */
+ public static CryptorImpIdentityV1 getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpIdentityV1();
+ }
+ return sCryptor;
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Encrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+ if (esalt == null) {
+ Log.e(TAG, "Failed to generate salt.");
+ return null;
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+ try {
+ return cipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Decrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+ if (esalt == null) {
+ return null;
+ }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ *
+ * @return signature {@code null} if failed to sign
+ */
+ @Nullable
+ @Override
+ public byte[] sign(byte[] data, byte[] salt) {
+ if (data == null) {
+ Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+ return null;
+ }
+
+ // Generates a 8 bytes HMAC tag
+ return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ * Uses KTAG_IV as salt value.
+ */
+ @Nullable
+ public byte[] sign(byte[] data) {
+ // Generates a 8 bytes HMAC tag
+ return sign(data, KTAG_IV);
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /**
+ * Verifies the signature generated by data and key, with the original signed data. Uses
+ * KTAG_IV as salt value.
+ */
+ public boolean verify(byte[] data, byte[] signature) {
+ return verify(data, KTAG_IV, signature);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
new file mode 100644
index 0000000..15073fb
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
+ */
+public class CryptorImpV1 extends Cryptor {
+
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ @VisibleForTesting
+ static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+ /** Length of encryption key required by AES/GCM encryption. */
+ private static final int ENCRYPTION_KEY_SIZE = 32;
+
+ /** Length of salt required by AES/GCM encryption. */
+ private static final int AES_CTR_IV_SIZE = 16;
+
+ /** Length HMAC tag */
+ public static final int HMAC_TAG_SIZE = 16;
+
+ // 3 16 byte arrays known by both the encryptor and decryptor.
+ private static final byte[] AK_IV =
+ new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+ private static final byte[] ASALT_IV =
+ new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
+ private static final byte[] HK_IV =
+ new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable private static CryptorImpV1 sCryptor;
+
+ /** Returns an instance of CryptorImpV1. */
+ public static CryptorImpV1 getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpV1();
+ }
+ return sCryptor;
+ }
+
+ private CryptorImpV1() {
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Encrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+ if (asalt == null) {
+ Log.e(TAG, "Failed to generate salt.");
+ return null;
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+ try {
+ return cipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Decrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+ if (asalt == null) {
+ return null;
+ }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ return generateHmacTag(data, key);
+ }
+
+ @Override
+ public int getSignatureLength() {
+ return HMAC_TAG_SIZE;
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+ * is equal to HMAC tag in advertisement to see data integrity. */
+ @Nullable
+ @VisibleForTesting
+ byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
+ if (data == null || authenticityKey == null) {
+ Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+ return null;
+ }
+
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.e(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes HMAC key from authenticity_key
+ byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
+ if (hmacKey == null) {
+ Log.e(TAG, "Failed to generate HMAC key.");
+ return null;
+ }
+
+ // Generates a 16 bytes HMAC tag from authenticity_key
+ return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
+ }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
new file mode 100644
index 0000000..a4761ab
--- /dev/null
+++ b/nearby/service/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.hardware.location.ContextHubManager#createClient`"
+ errorLine1=" mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
+ line="263"
+ column="54"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index 9f75d34..e1bf455 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -47,6 +47,7 @@
optional bytes metadata_encryption_key_tag = 2;
}
+// Public credential returned in BleFilterResult.
message PublicCredential {
optional bytes secret_id = 1;
optional bytes authenticity_key = 2;
@@ -55,6 +56,23 @@
optional bytes encrypted_metadata_tag = 5;
}
+message DataElement {
+ enum ElementType {
+ DE_NONE = 0;
+ DE_FAST_PAIR_ACCOUNT_KEY = 9;
+ DE_CONNECTION_STATUS = 10;
+ DE_BATTERY_STATUS = 11;
+ // Reserves 128 Test DEs.
+ DE_TEST_BEGIN = 2147483520; // INT_MAX - 127
+ DE_TEST_END = 2147483647; // INT_MAX
+ }
+
+ optional int32 key = 1;
+ optional bytes value = 2;
+ optional uint32 value_length = 3;
+}
+
+// A single filter used to filter BLE events.
message BleFilter {
optional uint32 id = 1; // Required, unique id of this filter.
// Maximum delay to notify the client after an event occurs.
@@ -71,7 +89,9 @@
// the period of latency defined above.
optional float distance_m = 7;
// Used to verify the list of trusted devices.
- repeated PublicateCertificate certficate = 8;
+ repeated PublicateCertificate certificate = 8;
+ // Data Elements for extended properties.
+ repeated DataElement data_element = 9;
}
message BleFilters {
@@ -80,14 +100,33 @@
// FilterResult is returned to host when a BLE event matches a Filter.
message BleFilterResult {
+ enum ResultType {
+ RESULT_NONE = 0;
+ RESULT_PRESENCE = 1;
+ RESULT_FAST_PAIR = 2;
+ }
+
optional uint32 id = 1; // id of the matched Filter.
- optional uint32 tx_power = 2;
- optional uint32 rssi = 3;
+ optional int32 tx_power = 2;
+ optional int32 rssi = 3;
optional uint32 intent = 4;
optional bytes bluetooth_address = 5;
optional PublicCredential public_credential = 6;
+ repeated DataElement data_element = 7;
+ optional bytes ble_service_data = 8;
+ optional ResultType result_type = 9;
}
message BleFilterResults {
repeated BleFilterResult result = 1;
}
+
+message BleConfig {
+ // True to start BLE scan. Otherwise, stop BLE scan.
+ optional bool start_scan = 1;
+ // True when screen is turned on. Otherwise, set to false when screen is
+ // turned off.
+ optional bool screen_on = 2;
+ // Fast Pair cache expires after this time period.
+ optional uint64 fast_pair_cache_expire_time_sec = 3;
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 0410cd5..66a1ffe 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -26,7 +26,7 @@
"bluetooth-test-util-lib",
"compatibility-device-util-axt",
"ctstestrunner-axt",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.base",
@@ -41,7 +41,6 @@
"mts-tethering",
],
certificate: "platform",
- platform_apis: true,
sdk_version: "module_current",
min_sdk_version: "30",
target_sdk_version: "32",
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index aacb6d8..a2da967 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -42,7 +42,6 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testBuilder() {
CredentialElement element = new CredentialElement(KEY, VALUE);
-
assertThat(element.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
}
@@ -58,9 +57,31 @@
CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
parcel);
parcel.recycle();
-
assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ CredentialElement element = new CredentialElement(KEY, VALUE);
+ assertThat(element.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEqual() {
+ CredentialElement element1 = new CredentialElement(KEY, VALUE);
+ CredentialElement element2 = new CredentialElement(KEY, VALUE);
+ assertThat(element1.equals(element2)).isTrue();
+ assertThat(element1.hashCode()).isEqualTo(element2.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ CredentialElement [] elements =
+ CredentialElement.CREATOR.newArray(2);
+ assertThat(elements.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
index 3654d0d..84814ae 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
@@ -16,6 +16,13 @@
package android.nearby.cts;
+import static android.nearby.DataElement.DataType.PRIVATE_IDENTITY;
+import static android.nearby.DataElement.DataType.PROVISIONED_IDENTITY;
+import static android.nearby.DataElement.DataType.PUBLIC_IDENTITY;
+import static android.nearby.DataElement.DataType.SALT;
+import static android.nearby.DataElement.DataType.TRUSTED_IDENTITY;
+import static android.nearby.DataElement.DataType.TX_POWER;
+
import static com.google.common.truth.Truth.assertThat;
import android.nearby.DataElement;
@@ -31,7 +38,6 @@
import java.util.Arrays;
-
@RunWith(AndroidJUnit4.class)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class DataElementTest {
@@ -63,4 +69,59 @@
assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ assertThat(dataElement.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ DataElement[] elements =
+ DataElement.CREATOR.newArray(2);
+ assertThat(elements.length).isEqualTo(2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquals() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ DataElement dataElement2 = new DataElement(KEY, VALUE);
+
+ assertThat(dataElement.equals(dataElement2)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsIdentity() {
+ DataElement privateIdentity = new DataElement(PRIVATE_IDENTITY, new byte[]{1, 2, 3});
+ DataElement trustedIdentity = new DataElement(TRUSTED_IDENTITY, new byte[]{1, 2, 3});
+ DataElement publicIdentity = new DataElement(PUBLIC_IDENTITY, new byte[]{1, 2, 3});
+ DataElement provisionedIdentity =
+ new DataElement(PROVISIONED_IDENTITY, new byte[]{1, 2, 3});
+
+ DataElement salt = new DataElement(SALT, new byte[]{1, 2, 3});
+ DataElement txPower = new DataElement(TX_POWER, new byte[]{1, 2, 3});
+
+ assertThat(privateIdentity.isIdentityDataType()).isTrue();
+ assertThat(trustedIdentity.isIdentityDataType()).isTrue();
+ assertThat(publicIdentity.isIdentityDataType()).isTrue();
+ assertThat(provisionedIdentity.isIdentityDataType()).isTrue();
+ assertThat(salt.isIdentityDataType()).isFalse();
+ assertThat(txPower.isIdentityDataType()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_notEquals() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ DataElement dataElement2 = new DataElement(KEY, new byte[]{1, 2, 1, 1});
+ DataElement dataElement3 = new DataElement(6, VALUE);
+
+ assertThat(dataElement.equals(dataElement2)).isFalse();
+ assertThat(dataElement.equals(dataElement3)).isFalse();
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
index f37800a..8ca5a94 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -16,6 +16,8 @@
package android.nearby.cts;
+import static android.nearby.NearbyDevice.Medium.BLE;
+
import android.annotation.TargetApi;
import android.nearby.FastPairDevice;
import android.nearby.NearbyDevice;
@@ -34,13 +36,18 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyDeviceTest {
+ private static final String NAME = "NearbyDevice";
+ private static final String MODEL_ID = "112233";
+ private static final int TX_POWER = -10;
+ private static final int RSSI = -60;
+ private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_isValidMedium() {
assertThat(NearbyDevice.isValidMedium(1)).isTrue();
assertThat(NearbyDevice.isValidMedium(2)).isTrue();
-
assertThat(NearbyDevice.isValidMedium(0)).isFalse();
assertThat(NearbyDevice.isValidMedium(3)).isFalse();
}
@@ -49,11 +56,55 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_getMedium_fromChild() {
FastPairDevice fastPairDevice = new FastPairDevice.Builder()
- .addMedium(NearbyDevice.Medium.BLE)
- .setRssi(-60)
+ .addMedium(BLE)
+ .setRssi(RSSI)
.build();
assertThat(fastPairDevice.getMediums()).contains(1);
- assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
+ assertThat(fastPairDevice.getRssi()).isEqualTo(RSSI);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEqual() {
+ FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .setRssi(RSSI)
+ .addMedium(BLE)
+ .setName(NAME)
+ .build();
+ FastPairDevice fastPairDevice2 = new FastPairDevice.Builder()
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .setRssi(RSSI)
+ .addMedium(BLE)
+ .setName(NAME)
+ .build();
+
+ assertThat(fastPairDevice1.equals(fastPairDevice1)).isTrue();
+ assertThat(fastPairDevice1.equals(fastPairDevice2)).isTrue();
+ assertThat(fastPairDevice1.equals(null)).isFalse();
+ assertThat(fastPairDevice1.hashCode()).isEqualTo(fastPairDevice2.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString() {
+ FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+ .addMedium(BLE)
+ .setRssi(RSSI)
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .build();
+
+ assertThat(fastPairDevice1.toString())
+ .isEqualTo("FastPairDevice [medium={BLE} rssi=-60 "
+ + "txPower=-10 modelId=112233 bluetoothAddress=00:11:22:33:FF:EE]");
}
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 7696a61..bc9691d 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -20,7 +20,7 @@
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.nearby.ScanCallback.ERROR_UNSUPPORTED;
import static com.google.common.truth.Truth.assertThat;
@@ -35,7 +35,9 @@
import android.nearby.BroadcastRequest;
import android.nearby.NearbyDevice;
import android.nearby.NearbyManager;
+import android.nearby.OffloadCapability;
import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceDevice;
import android.nearby.PrivateCredential;
import android.nearby.ScanCallback;
import android.nearby.ScanRequest;
@@ -48,6 +50,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
+import com.android.modules.utils.build.SdkLevel;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,6 +61,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* TODO(b/215435939) This class doesn't include any logic yet. Because SELinux denies access to
@@ -66,7 +71,7 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyManagerTest {
private static final byte[] SALT = new byte[]{1, 2};
- private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
private static final String DEVICE_NAME = "test_device";
@@ -82,6 +87,9 @@
.setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
.setBleEnabled(true)
.build();
+ private PresenceDevice.Builder mBuilder =
+ new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
+
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onDiscovered(@NonNull NearbyDevice device) {
@@ -94,14 +102,21 @@
@Override
public void onLost(@NonNull NearbyDevice device) {
}
+
+ @Override
+ public void onError(int errorCode) {
+ }
};
+
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
@Before
public void setUp() {
mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
BLUETOOTH_PRIVILEGED);
- DeviceConfig.setProperty(NAMESPACE_TETHERING,
+ String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
+ : DeviceConfig.NAMESPACE_TETHERING;
+ DeviceConfig.setProperty(nameSpace,
"nearby_enable_presence_broadcast_legacy",
"true", false);
@@ -137,7 +152,7 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testStartStopBroadcast() throws InterruptedException {
- PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY,
+ PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
.setIdentityType(IDENTITY_TYPE_PRIVATE)
.build();
@@ -158,6 +173,22 @@
mNearbyManager.stopBroadcast(callback);
}
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void queryOffloadScanSupport() {
+ OffloadCallback callback = new OffloadCallback();
+ mNearbyManager.queryOffloadCapability(EXECUTOR, callback);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void testAllCallbackMethodsExits() {
+ mScanCallback.onDiscovered(mBuilder.setRssi(-10).build());
+ mScanCallback.onUpdated(mBuilder.setRssi(-5).build());
+ mScanCallback.onLost(mBuilder.setRssi(-8).build());
+ mScanCallback.onError(ERROR_UNSUPPORTED);
+ }
+
private void enableBluetooth() {
BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -165,4 +196,11 @@
assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
}
}
+
+ private static class OffloadCallback implements Consumer<OffloadCapability> {
+ @Override
+ public void accept(OffloadCapability aBoolean) {
+ // no-op for now
+ }
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java
new file mode 100644
index 0000000..a745c7d
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.OffloadCapability;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class OffloadCapabilityTest {
+ private static final long VERSION = 123456;
+
+ @Test
+ public void testDefault() {
+ OffloadCapability offloadCapability = new OffloadCapability.Builder().build();
+
+ assertThat(offloadCapability.isFastPairSupported()).isFalse();
+ assertThat(offloadCapability.isNearbyShareSupported()).isFalse();
+ assertThat(offloadCapability.getVersion()).isEqualTo(0);
+ }
+
+ @Test
+ public void testBuilder() {
+ OffloadCapability offloadCapability = new OffloadCapability.Builder()
+ .setFastPairSupported(true)
+ .setNearbyShareSupported(true)
+ .setVersion(VERSION)
+ .build();
+
+ assertThat(offloadCapability.isFastPairSupported()).isTrue();
+ assertThat(offloadCapability.isNearbyShareSupported()).isTrue();
+ assertThat(offloadCapability.getVersion()).isEqualTo(VERSION);
+ }
+
+ @Test
+ public void testWriteParcel() {
+ OffloadCapability offloadCapability = new OffloadCapability.Builder()
+ .setFastPairSupported(true)
+ .setNearbyShareSupported(false)
+ .setVersion(VERSION)
+ .build();
+
+ Parcel parcel = Parcel.obtain();
+ offloadCapability.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ OffloadCapability capability = OffloadCapability.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+
+ assertThat(capability.isFastPairSupported()).isTrue();
+ assertThat(capability.isNearbyShareSupported()).isFalse();
+ assertThat(capability.getVersion()).isEqualTo(VERSION);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index eaa5ca1..71be889 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -114,4 +114,18 @@
assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+ assertThat(broadcastRequest.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PresenceBroadcastRequest[] presenceBroadcastRequests =
+ PresenceBroadcastRequest.CREATOR.newArray(2);
+ assertThat(presenceBroadcastRequests.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index 94f8fe7..ea1de6b 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -104,4 +104,24 @@
assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceDevice device =
+ new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+ .addExtendedProperty(new DataElement(KEY, VALUE))
+ .setRssi(RSSI)
+ .addMedium(MEDIUM)
+ .setName(DEVICE_NAME)
+ .build();
+ assertThat(device.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PresenceDevice[] devices =
+ PresenceDevice.CREATOR.newArray(2);
+ assertThat(devices.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index cecdfd2..821f2d0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -51,7 +51,6 @@
private static final int KEY = 3;
private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
-
private PublicCredential mPublicCredential =
new PublicCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
ENCRYPTED_METADATA, METADATA_ENCRYPTION_KEY_TAG)
@@ -90,5 +89,21 @@
assertThat(parcelFilter.getType()).isEqualTo(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
+ assertThat(parcelFilter.getExtendedProperties().get(0).getKey()).isEqualTo(KEY);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceScanFilter filter = mBuilder.build();
+ assertThat(filter.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ PresenceScanFilter[] filters =
+ PresenceScanFilter.CREATOR.newArray(2);
+ assertThat(filters.length).isEqualTo(2);
}
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index f05f65f..fa8c954 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -99,4 +99,19 @@
assertThat(credentialElement.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void describeContents() {
+ PrivateCredential credential = mBuilder.build();
+ assertThat(credential.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testCreatorNewArray() {
+ PrivateCredential[] credentials =
+ PrivateCredential.CREATOR.newArray(2);
+ assertThat(credentials.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index 11bbacc..774e897 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -135,6 +135,7 @@
.setIdentityType(IDENTITY_TYPE_PRIVATE)
.build();
assertThat(credentialOne.equals((Object) credentialTwo)).isTrue();
+ assertThat(credentialOne.equals(null)).isFalse();
}
@Test
@@ -161,4 +162,19 @@
.build();
assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PublicCredential credential = mBuilder.build();
+ assertThat(credential.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ PublicCredential[] credentials =
+ PublicCredential.CREATOR.newArray(2);
+ assertThat(credentials.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index 21f3d28..5ad52c2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -30,7 +30,6 @@
import android.nearby.PublicCredential;
import android.nearby.ScanRequest;
import android.os.Build;
-import android.os.WorkSource;
import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,12 +42,10 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class ScanRequestTest {
- private static final int UID = 1001;
- private static final String APP_NAME = "android.nearby.tests";
private static final int RSSI = -40;
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanType() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
@@ -59,13 +56,13 @@
// Valid scan type must be set to one of ScanRequest#SCAN_TYPE_
@Test(expected = IllegalStateException.class)
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanType_notSet_throwsException() {
new ScanRequest.Builder().setScanMode(SCAN_MODE_BALANCED).build();
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanMode_defaultLowPower() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
@@ -76,7 +73,7 @@
/** Verify setting work source with null value in the scan request is allowed */
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testSetWorkSource_nullValue() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
@@ -87,39 +84,9 @@
assertThat(request.getWorkSource().isEmpty()).isTrue();
}
- /** Verify toString returns expected string. */
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToString() {
- WorkSource workSource = getWorkSource();
- ScanRequest request = new ScanRequest.Builder()
- .setScanType(SCAN_TYPE_FAST_PAIR)
- .setScanMode(SCAN_MODE_BALANCED)
- .setBleEnabled(true)
- .setWorkSource(workSource)
- .build();
-
- assertThat(request.toString()).isEqualTo(
- "Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
- + "enableBle=true, workSource=WorkSource{" + UID + " " + APP_NAME
- + "}, scanFilters=[]]");
- }
-
- /** Verify toString works correctly with null WorkSource. */
- @Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToString_nullWorkSource() {
- ScanRequest request = new ScanRequest.Builder().setScanType(
- SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
-
- assertThat(request.toString()).isEqualTo("Request[scanType=1, "
- + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
- + "scanFilters=[]]");
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testisEnableBle_defaultTrue() {
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testIsEnableBle_defaultTrue() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
.build();
@@ -128,7 +95,28 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void testIsOffloadOnly_defaultFalse() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+
+ assertThat(request.isOffloadOnly()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void testSetOffloadOnly_isOffloadOnlyTrue() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .setOffloadOnly(true)
+ .build();
+
+ assertThat(request.isOffloadOnly()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void test_isValidScanType() {
assertThat(ScanRequest.isValidScanType(SCAN_TYPE_FAST_PAIR)).isTrue();
assertThat(ScanRequest.isValidScanType(SCAN_TYPE_NEARBY_PRESENCE)).isTrue();
@@ -138,7 +126,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void test_isValidScanMode() {
assertThat(ScanRequest.isValidScanMode(SCAN_MODE_LOW_LATENCY)).isTrue();
assertThat(ScanRequest.isValidScanMode(SCAN_MODE_BALANCED)).isTrue();
@@ -150,7 +138,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void test_scanModeToString() {
assertThat(ScanRequest.scanModeToString(2)).isEqualTo("SCAN_MODE_LOW_LATENCY");
assertThat(ScanRequest.scanModeToString(1)).isEqualTo("SCAN_MODE_BALANCED");
@@ -162,7 +150,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testScanFilter() {
ScanRequest request = new ScanRequest.Builder().setScanType(
SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(getPresenceScanFilter()).build();
@@ -171,6 +159,23 @@
assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(RSSI);
}
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void describeContents() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+ assertThat(request.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testCreatorNewArray() {
+ ScanRequest[] requests =
+ ScanRequest.CREATOR.newArray(2);
+ assertThat(requests.length).isEqualTo(2);
+ }
+
private static PresenceScanFilter getPresenceScanFilter() {
final byte[] secretId = new byte[]{1, 2, 3, 4};
final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
@@ -190,8 +195,4 @@
.addPresenceAction(action)
.build();
}
-
- private static WorkSource getWorkSource() {
- return new WorkSource(UID, APP_NAME);
- }
}
diff --git a/nearby/tests/integration/privileged/Android.bp b/nearby/tests/integration/privileged/Android.bp
index e3250f6..9b6e488 100644
--- a/nearby/tests/integration/privileged/Android.bp
+++ b/nearby/tests/integration/privileged/Android.bp
@@ -27,7 +27,7 @@
"androidx.test.ext.junit",
"androidx.test.rules",
"junit",
- "truth-prebuilt",
+ "truth",
],
test_suites: ["device-tests"],
}
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
index 66bab23..506b4e2 100644
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -63,6 +63,8 @@
override fun onUpdated(device: NearbyDevice) {}
override fun onLost(device: NearbyDevice) {}
+
+ override fun onError(errorCode: Int) {}
}
nearbyManager.startScan(scanRequest, /* executor */ { it.run() }, scanCallback)
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 57499e4..75f765b 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -31,7 +31,7 @@
"androidx.test.uiautomator_uiautomator",
"junit",
"kotlin-test",
- "truth-prebuilt",
+ "truth",
],
test_suites: ["device-tests"],
}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 9b35452..112c751 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -42,7 +42,7 @@
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"service-nearby-pre-jarjar",
- "truth-prebuilt",
+ "truth",
// "Robolectric_all-target",
],
// these are needed for Extended Mockito
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index 9f58baf..7dcb263 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
new file mode 100644
index 0000000..edda3c2
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FastPairDeviceTest {
+ private static final String NAME = "name";
+ private static final byte[] DATA = new byte[] {0x01, 0x02};
+ private static final String MODEL_ID = "112233";
+ private static final int RSSI = -80;
+ private static final int TX_POWER = -10;
+ private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+ private static List<Integer> sMediums = new ArrayList<Integer>(List.of(1));
+ private static FastPairDevice sDevice;
+
+
+ @Before
+ public void setup() {
+ sDevice = new FastPairDevice(NAME, sMediums, RSSI, TX_POWER, MODEL_ID, MAC_ADDRESS, DATA);
+ }
+
+ @Test
+ public void testParcelable() {
+ Parcel dest = Parcel.obtain();
+ sDevice.writeToParcel(dest, 0);
+ dest.setDataPosition(0);
+ FastPairDevice compareDevice = FastPairDevice.CREATOR.createFromParcel(dest);
+ assertThat(compareDevice.getName()).isEqualTo(NAME);
+ assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+ assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+ assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(compareDevice.getData()).isEqualTo(DATA);
+ assertThat(compareDevice.equals(sDevice)).isTrue();
+ assertThat(compareDevice.hashCode()).isEqualTo(sDevice.hashCode());
+ }
+
+ @Test
+ public void describeContents() {
+ assertThat(sDevice.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testToString() {
+ assertThat(sDevice.toString()).isEqualTo(
+ "FastPairDevice [name=name, medium={BLE} "
+ + "rssi=-80 txPower=-10 "
+ + "modelId=112233 bluetoothAddress=00:11:22:33:44:55]");
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ FastPairDevice[] fastPairDevices = FastPairDevice.CREATOR.newArray(2);
+ assertThat(fastPairDevices.length).isEqualTo(2);
+ }
+
+ @Test
+ public void testBuilder() {
+ FastPairDevice.Builder builder = new FastPairDevice.Builder();
+ FastPairDevice compareDevice = builder.setName(NAME)
+ .addMedium(1)
+ .setBluetoothAddress(MAC_ADDRESS)
+ .setRssi(RSSI)
+ .setTxPower(TX_POWER)
+ .setData(DATA)
+ .setModelId(MODEL_ID)
+ .build();
+ assertThat(compareDevice.getName()).isEqualTo(NAME);
+ assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+ assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+ assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(compareDevice.getData()).isEqualTo(DATA);
+ }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
similarity index 60%
rename from nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
rename to nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
index 654b852..a4909b2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,20 +14,17 @@
* limitations under the License.
*/
-package android.nearby.cts;
+package android.nearby;
import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
import static com.google.common.truth.Truth.assertThat;
-import android.nearby.NearbyDevice;
-import android.nearby.NearbyDeviceParcelable;
import android.os.Build;
import android.os.Parcel;
import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
import org.junit.Before;
import org.junit.Test;
@@ -39,10 +36,15 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyDeviceParcelableTest {
+ private static final long DEVICE_ID = 1234;
private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+ private static final byte[] SALT = new byte[] {1, 2, 3, 4};
private static final String FAST_PAIR_MODEL_ID = "1234";
private static final int RSSI = -60;
+ private static final int TX_POWER = -10;
+ private static final int ACTION = 1;
+ private static final int MEDIUM_BLE = 1;
private NearbyDeviceParcelable.Builder mBuilder;
@@ -50,9 +52,10 @@
public void setUp() {
mBuilder =
new NearbyDeviceParcelable.Builder()
+ .setDeviceId(DEVICE_ID)
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setName("testDevice")
- .setMedium(NearbyDevice.Medium.BLE)
+ .setMedium(MEDIUM_BLE)
.setRssi(RSSI)
.setFastPairModelId(FAST_PAIR_MODEL_ID)
.setBluetoothAddress(BLUETOOTH_ADDRESS)
@@ -60,25 +63,40 @@
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
- public void test_defaultNullFields() {
+ public void testNullFields() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
NearbyDeviceParcelable nearbyDeviceParcelable =
new NearbyDeviceParcelable.Builder()
- .setMedium(NearbyDevice.Medium.BLE)
+ .setMedium(MEDIUM_BLE)
+ .setPublicCredential(publicCredential)
+ .setAction(ACTION)
.setRssi(RSSI)
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .setTxPower(TX_POWER)
+ .setSalt(SALT)
.build();
+ assertThat(nearbyDeviceParcelable.getDeviceId()).isEqualTo(-1);
assertThat(nearbyDeviceParcelable.getName()).isNull();
assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
assertThat(nearbyDeviceParcelable.getData()).isNull();
-
- assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
+ assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(MEDIUM_BLE);
assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+ assertThat(nearbyDeviceParcelable.getAction()).isEqualTo(ACTION);
+ assertThat(nearbyDeviceParcelable.getPublicCredential()).isEqualTo(publicCredential);
+ assertThat(nearbyDeviceParcelable.getSalt()).isEqualTo(SALT);
+ assertThat(nearbyDeviceParcelable.getTxPower()).isEqualTo(TX_POWER);
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.build();
@@ -89,6 +107,7 @@
NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
parcel.recycle();
+ assertThat(actualNearbyDevice.getDeviceId()).isEqualTo(DEVICE_ID);
assertThat(actualNearbyDevice.getRssi()).isEqualTo(RSSI);
assertThat(actualNearbyDevice.getFastPairModelId()).isEqualTo(FAST_PAIR_MODEL_ID);
assertThat(actualNearbyDevice.getBluetoothAddress()).isEqualTo(BLUETOOTH_ADDRESS);
@@ -96,7 +115,6 @@
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel_nullModelId() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setFastPairModelId(null).build();
@@ -111,10 +129,8 @@
}
@Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel_nullBluetoothAddress() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
-
Parcel parcel = Parcel.obtain();
nearbyDeviceParcelable.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
@@ -124,4 +140,34 @@
assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
}
+
+ @Test
+ public void describeContents() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+ assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testEqual() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
+ NearbyDeviceParcelable nearbyDeviceParcelable1 =
+ mBuilder.setPublicCredential(publicCredential).build();
+ NearbyDeviceParcelable nearbyDeviceParcelable2 =
+ mBuilder.setPublicCredential(publicCredential).build();
+ assertThat(nearbyDeviceParcelable1.equals(nearbyDeviceParcelable2)).isTrue();
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ NearbyDeviceParcelable[] nearbyDeviceParcelables =
+ NearbyDeviceParcelable.CREATOR.newArray(2);
+ assertThat(nearbyDeviceParcelables.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
index 12de30e..6020c7e 100644
--- a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
+++ b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
@@ -24,11 +24,14 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.Build;
import android.os.Parcel;
import android.os.WorkSource;
import android.platform.test.annotations.Presubmit;
+import androidx.annotation.RequiresApi;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import org.junit.Test;
@@ -38,14 +41,15 @@
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class ScanRequestTest {
private static final int RSSI = -40;
+ private static final int UID = 1001;
+ private static final String APP_NAME = "android.nearby.tests";
private static WorkSource getWorkSource() {
- final int uid = 1001;
- final String appName = "android.nearby.tests";
- return new WorkSource(uid, appName);
+ return new WorkSource(UID, APP_NAME);
}
/** Test creating a scan request. */
@@ -104,6 +108,7 @@
/** Verify toString returns expected string. */
@Test
+ @SdkSuppress(minSdkVersion = 34)
public void testToString() {
WorkSource workSource = getWorkSource();
ScanRequest request = new ScanRequest.Builder()
@@ -115,28 +120,28 @@
assertThat(request.toString()).isEqualTo(
"Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
- + "enableBle=true, workSource=WorkSource{1001 android.nearby.tests}, "
- + "scanFilters=[]]");
+ + "bleEnabled=true, offloadOnly=false, "
+ + "workSource=WorkSource{" + UID + " " + APP_NAME + "}, scanFilters=[]]");
}
/** Verify toString works correctly with null WorkSource. */
@Test
- public void testToString_nullWorkSource() {
+ @SdkSuppress(minSdkVersion = 34)
+ public void testToString_nullWorkSource_offloadOnly() {
ScanRequest request = new ScanRequest.Builder().setScanType(
- SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
+ SCAN_TYPE_FAST_PAIR).setWorkSource(null).setOffloadOnly(true).build();
assertThat(request.toString()).isEqualTo("Request[scanType=1, "
- + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
- + "scanFilters=[]]");
+ + "scanMode=SCAN_MODE_LOW_POWER, bleEnabled=true, offloadOnly=true, "
+ + "workSource=WorkSource{}, scanFilters=[]]");
}
/** Verify writing and reading from parcel for scan request. */
@Test
public void testParceling() {
- final int scanType = SCAN_TYPE_NEARBY_PRESENCE;
WorkSource workSource = getWorkSource();
ScanRequest originalRequest = new ScanRequest.Builder()
- .setScanType(scanType)
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setScanMode(SCAN_MODE_BALANCED)
.setBleEnabled(true)
.setWorkSource(workSource)
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
new file mode 100644
index 0000000..5ddfed3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.DeviceConfig;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class NearbyConfigurationTest {
+
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private NearbyConfiguration mNearbyConfiguration;
+
+ @Before
+ public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ }
+
+ @Test
+ public void testDeviceConfigChanged() throws InterruptedException {
+ mNearbyConfiguration = new NearbyConfiguration();
+
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "false", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "1", false);
+ Thread.sleep(500);
+
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+ assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isFalse();
+ assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(1);
+
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "true", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "true", false);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "3", false);
+ Thread.sleep(500);
+
+ assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+ assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
+ assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
+ }
+
+ @After
+ public void tearDown() {
+ // Sets DeviceConfig values back to default
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", true);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+ "false", true);
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "0", true);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index 8a18cca..5b640cc 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -18,6 +18,9 @@
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -32,6 +35,8 @@
import android.content.Context;
import android.nearby.IScanListener;
import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.provider.DeviceConfig;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -45,6 +50,7 @@
public final class NearbyServiceTest {
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
private static final String PACKAGE_NAME = "android.nearby.test";
private Context mContext;
private NearbyService mService;
@@ -56,11 +62,16 @@
private IScanListener mScanListener;
@Mock
private AppOpsManager mMockAppOpsManager;
+ @Mock
+ private IBinder mIBinder;
@Before
public void setUp() {
initMocks(this);
- mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
+ when(mScanListener.asBinder()).thenReturn(mIBinder);
+
+ mUiAutomation.adoptShellPermissionIdentity(
+ READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mService = new NearbyService(mContext);
mScanRequest = createScanRequest();
@@ -80,6 +91,8 @@
@Test
public void test_register_noPrivilegedPermission_throwsException() {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", false);
mUiAutomation.dropShellPermissionIdentity();
assertThrows(java.lang.SecurityException.class,
() -> mService.registerScanListener(mScanRequest, mScanListener, PACKAGE_NAME,
@@ -88,6 +101,8 @@
@Test
public void test_unregister_noPrivilegedPermission_throwsException() {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+ "false", false);
mUiAutomation.dropShellPermissionIdentity();
assertThrows(java.lang.SecurityException.class,
() -> mService.unregisterScanListener(mScanListener, PACKAGE_NAME,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
new file mode 100644
index 0000000..719e816
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.SystemClock;
+
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class CancelableAlarmTest {
+
+ private static final long DELAY_MILLIS = 1000;
+
+ private final ScheduledExecutorService mExecutor =
+ Executors.newScheduledThreadPool(1);
+
+ @Test
+ public void alarmRuns_singleExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(1);
+ CancelableAlarm.createSingleAlarm(
+ "alarmRuns", new CountDownRunnable(latch), DELAY_MILLIS, mExecutor);
+ latch.awaitAndExpectDelay(DELAY_MILLIS);
+ }
+
+ @Test
+ public void alarmRuns_periodicExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(2);
+ CancelableAlarm.createRecurringAlarm(
+ "alarmRunsPeriodically", new CountDownRunnable(latch), DELAY_MILLIS, mExecutor);
+ latch.awaitAndExpectDelay(DELAY_MILLIS * 2);
+ }
+
+ @Test
+ public void canceledAlarmDoesNotRun_singleExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(1);
+ CancelableAlarm alarm =
+ CancelableAlarm.createSingleAlarm(
+ "canceledAlarmDoesNotRun",
+ new CountDownRunnable(latch),
+ DELAY_MILLIS,
+ mExecutor);
+ assertThat(alarm.cancel()).isTrue();
+ latch.awaitAndExpectTimeout(DELAY_MILLIS);
+ }
+
+ @Test
+ public void canceledAlarmDoesNotRun_periodicExecution() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(2);
+ CancelableAlarm alarm =
+ CancelableAlarm.createRecurringAlarm(
+ "canceledAlarmDoesNotRunPeriodically",
+ new CountDownRunnable(latch),
+ DELAY_MILLIS,
+ mExecutor);
+ latch.awaitAndExpectTimeout(DELAY_MILLIS);
+ assertThat(alarm.cancel()).isTrue();
+ latch.awaitAndExpectTimeout(DELAY_MILLIS);
+ }
+
+ @Test
+ public void cancelOfRunAlarmReturnsFalse() throws InterruptedException {
+ TestCountDownLatch latch = new TestCountDownLatch(1);
+ long delayMillis = 500;
+ CancelableAlarm alarm =
+ CancelableAlarm.createSingleAlarm(
+ "cancelOfRunAlarmReturnsFalse",
+ new CountDownRunnable(latch),
+ delayMillis,
+ mExecutor);
+ latch.awaitAndExpectDelay(delayMillis - 1);
+
+ assertThat(alarm.cancel()).isFalse();
+ }
+
+ private static class CountDownRunnable implements Runnable {
+ private final CountDownLatch mLatch;
+
+ CountDownRunnable(CountDownLatch latch) {
+ this.mLatch = latch;
+ }
+
+ @Override
+ public void run() {
+ mLatch.countDown();
+ }
+ }
+
+ /** A CountDownLatch for test with extra test features like throw exception on await(). */
+ private static class TestCountDownLatch extends CountDownLatch {
+
+ TestCountDownLatch(int count) {
+ super(count);
+ }
+
+ /**
+ * Asserts that the latch does not go off until delayMillis has passed and that it does in
+ * fact go off after delayMillis has passed.
+ */
+ public void awaitAndExpectDelay(long delayMillis) throws InterruptedException {
+ SystemClock.sleep(delayMillis - 1);
+ assertThat(await(0, TimeUnit.MILLISECONDS)).isFalse();
+ SystemClock.sleep(10);
+ assertThat(await(0, TimeUnit.MILLISECONDS)).isTrue();
+ }
+
+ /** Asserts that the latch does not go off within delayMillis. */
+ public void awaitAndExpectTimeout(long delayMillis) throws InterruptedException {
+ SystemClock.sleep(delayMillis + 1);
+ assertThat(await(0, TimeUnit.MILLISECONDS)).isFalse();
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java
new file mode 100644
index 0000000..eb6316e
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CancellationFlagTest {
+
+ @Test
+ public void initialValueIsFalse() {
+ assertThat(new CancellationFlag().isCancelled()).isFalse();
+ }
+
+ @Test
+ public void cancel() {
+ CancellationFlag flag = new CancellationFlag();
+ flag.cancel();
+ assertThat(flag.isCancelled()).isTrue();
+ }
+
+ @Test
+ public void cancelShouldOnlyCancelOnce() {
+ CancellationFlag flag = new CancellationFlag();
+ AtomicInteger record = new AtomicInteger();
+
+ flag.registerOnCancelListener(() -> record.incrementAndGet());
+ for (int i = 0; i < 3; i++) {
+ flag.cancel();
+ }
+
+ assertThat(flag.isCancelled()).isTrue();
+ assertThat(record.get()).isEqualTo(1);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
new file mode 100644
index 0000000..b577064
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.injector;
+
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ContextHubManagerAdapterTest {
+ private ContextHubManagerAdapter mContextHubManagerAdapter;
+
+ @Mock
+ ContextHubManager mContextHubManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContextHubManagerAdapter = new ContextHubManagerAdapter(mContextHubManager);
+ }
+
+ @Test
+ public void getContextHubs() {
+ mContextHubManagerAdapter.getContextHubs();
+ }
+
+ @Test
+ public void queryNanoApps() {
+ mContextHubManagerAdapter.queryNanoApps(new ContextHubInfo());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
similarity index 79%
rename from nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
rename to nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index d45d570..bc38210 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.eq;
@@ -39,6 +39,9 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.provider.BleBroadcastProvider;
+
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
@@ -51,9 +54,10 @@
import java.util.Collections;
/**
- * Unit test for {@link BroadcastProviderManager}.
+ * Unit test for {@link com.android.server.nearby.managers.BroadcastProviderManager}.
*/
public class BroadcastProviderManagerTest {
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
private static final int MEDIUM_TYPE_BLE = 0;
private static final byte[] SALT = {2, 3};
@@ -79,11 +83,12 @@
@Before
public void setUp() {
mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
- DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
- "true", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
mContext = ApplicationProvider.getApplicationContext();
- mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+ mBroadcastProviderManager = new BroadcastProviderManager(
+ MoreExecutors.directExecutor(),
mBleBroadcastProvider);
PrivateCredential privateCredential =
@@ -101,14 +106,22 @@
@Test
public void testStartAdvertising() {
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
- verify(mBleBroadcastProvider).start(any(byte[].class), any(
- BleBroadcastProvider.BroadcastListener.class));
+ verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
+ any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+ }
+
+ @Test
+ public void testStopAdvertising() {
+ mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
}
@Test
public void testStartAdvertising_featureDisabled() throws Exception {
- DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
- "false", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "false", false);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+
mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
mBleBroadcastProvider);
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
new file mode 100644
index 0000000..aa0dad3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryProviderManagerLegacy} class.
+ */
+public class DiscoveryProviderManagerLegacyTest {
+ private static final int SCAN_MODE_CHRE_ONLY = 3;
+ private static final int DATA_TYPE_SCAN_MODE = 102;
+ private static final int UID = 1234;
+ private static final int PID = 5678;
+ private static final String PACKAGE_NAME = "android.nearby.test";
+ private static final int RSSI = -60;
+ @Mock
+ Injector mInjector;
+ @Mock
+ Context mContext;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock
+ ChreDiscoveryProvider mChreDiscoveryProvider;
+ @Mock
+ DiscoveryProviderController mBluetoothController;
+ @Mock
+ DiscoveryProviderController mChreController;
+ @Mock
+ IScanListener mScanListener;
+ @Mock
+ CallerIdentity mCallerIdentity;
+ @Mock
+ DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+ @Mock
+ IBinder mIBinder;
+ private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
+ private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
+ mScanTypeScanListenerRecordMap;
+
+ private static PresenceScanFilter getPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .build();
+ }
+
+ private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+ new byte[]{SCAN_MODE_CHRE_ONLY});
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .addExtendedProperty(scanModeElement)
+ .build();
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+ when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mDiscoveryProviderManager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ mChreDiscoveryProvider,
+ mScanTypeScanListenerRecordMap);
+ mCallerIdentity = CallerIdentity
+ .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+ }
+
+ @Test
+ public void testOnNearbyDeviceDiscovered() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+ mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+ }
+
+ @Test
+ public void testInvalidateProviderScanMode() {
+ mDiscoveryProviderManager.invalidateProviderScanMode();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter())
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isFalse();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isNull();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void test_stopChreProvider_clearFilters() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManagerLegacy manager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+ scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ manager.stopChreProvider();
+ Thread.sleep(200);
+ // The filters should be cleared right after.
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isFalse();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+ }
+
+ @Test
+ public void test_restartChreProvider() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManagerLegacy manager =
+ new DiscoveryProviderManagerLegacy(mContext, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+ new DiscoveryProviderManagerLegacy.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // We want to make sure quickly restart the provider the filters should
+ // be reset correctly.
+ // See b/255922206, there can be a race condition that filters get cleared because onStop()
+ // get executed after onStart() if they are called from different threads.
+ manager.stopChreProvider();
+ manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+ List.of(getPresenceScanFilter()));
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ Thread.sleep(200);
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // Wait for enough time
+ Thread.sleep(1000);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
new file mode 100644
index 0000000..7ecf631
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class DiscoveryProviderManagerTest {
+ private static final int SCAN_MODE_CHRE_ONLY = 3;
+ private static final int DATA_TYPE_SCAN_MODE = 102;
+ private static final int UID = 1234;
+ private static final int PID = 5678;
+ private static final String PACKAGE_NAME = "android.nearby.test";
+ private static final int RSSI = -60;
+ @Mock
+ Injector mInjector;
+ @Mock
+ Context mContext;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock
+ ChreDiscoveryProvider mChreDiscoveryProvider;
+ @Mock
+ DiscoveryProviderController mBluetoothController;
+ @Mock
+ DiscoveryProviderController mChreController;
+ @Mock
+ IScanListener mScanListener;
+ @Mock
+ CallerIdentity mCallerIdentity;
+ @Mock
+ IBinder mIBinder;
+ private Executor mExecutor;
+ private DiscoveryProviderManager mDiscoveryProviderManager;
+
+ private static PresenceScanFilter getPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .build();
+ }
+
+ private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+ new byte[]{SCAN_MODE_CHRE_ONLY});
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .addExtendedProperty(scanModeElement)
+ .build();
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mExecutor = Executors.newSingleThreadExecutor();
+ when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+ when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+ when(mScanListener.asBinder()).thenReturn(mIBinder);
+
+ mDiscoveryProviderManager =
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+ mBleDiscoveryProvider,
+ mChreDiscoveryProvider);
+ mCallerIdentity = CallerIdentity
+ .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+ }
+
+ @Test
+ public void testOnNearbyDeviceDiscovered() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+ mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+ }
+
+ @Test
+ public void testInvalidateProviderScanMode() {
+ mDiscoveryProviderManager.invalidateProviderScanMode();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ reset(mBluetoothController);
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ reset(mBluetoothController);
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ reset(mBluetoothController);
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isFalse();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isNull();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+ reset(mBluetoothController);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ Boolean start = mDiscoveryProviderManager.startProviders();
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void test_stopChreProvider_clearFilters() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ DiscoveryProviderManager manager =
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ manager.stopChreProvider();
+ Thread.sleep(200);
+ // The filters should be cleared right after.
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isFalse();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+ }
+
+ @Test
+ public void test_restartChreProvider() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ DiscoveryProviderManager manager =
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+ mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // We want to make sure quickly restart the provider the filters should
+ // be reset correctly.
+ // See b/255922206, there can be a race condition that filters get cleared because onStop()
+ // get executed after onStart() if they are called from different threads.
+ manager.stopChreProvider();
+ manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+ List.of(getPresenceScanFilter()));
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ Thread.sleep(200);
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // Wait for enough time
+ Thread.sleep(1000);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
new file mode 100644
index 0000000..104d762
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.MockitoAnnotations.initMocks;
+
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+public class ListenerMultiplexerTest {
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ }
+
+ @Test
+ public void testAdd() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+ multiplexer.addListener(binder2, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ }
+
+ @Test
+ public void testReplace() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ reset(listener);
+
+ // Same key, different value
+ Runnable listener2 = mock(Runnable.class);
+ int value2 = 1;
+ multiplexer.addListener(binder, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ // Should not be called again
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(0);
+ assertThat(multiplexer.mMerged).isEqualTo(value2);
+ }
+ // Run on the new listener
+ multiplexer.notifyListeners();
+ verify(listener, never()).run();
+ verify(listener2, times(1)).run();
+
+ multiplexer.removeRegistration(binder);
+
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ }
+
+ @Test
+ public void testRemove() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ reset(listener);
+
+ multiplexer.removeRegistration(binder);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, never()).run();
+ }
+
+ @Test
+ public void testMergeMultiple() {
+ TestMultiplexer multiplexer = new TestMultiplexer();
+
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+
+ Runnable listener3 = mock(Runnable.class);
+ IBinder binder3 = mock(IBinder.class);
+ int value3 = 5;
+
+ multiplexer.addListener(binder, listener, value);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ verify(listener2, never()).run();
+ verify(listener3, never()).run();
+
+ multiplexer.addListener(binder2, listener2, value2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+ assertThat(multiplexer.mMerged).isEqualTo(value);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(2)).run();
+ verify(listener2, times(1)).run();
+ verify(listener3, never()).run();
+
+ multiplexer.addListener(binder3, listener3, value3);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(3);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+ assertThat(multiplexer.mMerged).isEqualTo(value3);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(2)).run();
+ verify(listener3, times(1)).run();
+
+ multiplexer.removeRegistration(binder);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(4);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+ assertThat(multiplexer.mMerged).isEqualTo(value3);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(3)).run();
+ verify(listener3, times(2)).run();
+
+ multiplexer.removeRegistration(binder3);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isTrue();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(5);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(3);
+ assertThat(multiplexer.mMerged).isEqualTo(value2);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(4)).run();
+ verify(listener3, times(2)).run();
+
+ multiplexer.removeRegistration(binder2);
+ synchronized (multiplexer.mMultiplexerLock) {
+ assertThat(multiplexer.mRegistered).isFalse();
+ assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+ assertThat(multiplexer.mMergeOperationCount).isEqualTo(6);
+ assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(4);
+ assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ multiplexer.notifyListeners();
+ verify(listener, times(3)).run();
+ verify(listener2, times(4)).run();
+ verify(listener3, times(2)).run();
+ }
+
+ private class TestMultiplexer extends
+ ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+ int mOnRegisterCalledCount;
+ int mOnUnregisterCalledCount;
+ boolean mRegistered;
+ private int mMergeOperationCount;
+ private int mMergeUpdatedCount;
+
+ @Override
+ public void onRegister() {
+ mOnRegisterCalledCount++;
+ mRegistered = true;
+ }
+
+ @Override
+ public void onUnregister() {
+ mOnUnregisterCalledCount++;
+ mRegistered = false;
+ }
+
+ @Override
+ public Integer mergeRegistrations(
+ @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+ mMergeOperationCount++;
+ int max = Integer.MIN_VALUE;
+ for (TestListenerRegistration registration : testListenerRegistrations) {
+ max = Math.max(max, registration.getValue());
+ }
+ return max;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ mMergeUpdatedCount++;
+ }
+
+ public void addListener(IBinder binder, Runnable runnable, int value) {
+ TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+ value);
+ putRegistration(binder, registration);
+ }
+
+ public void notifyListeners() {
+ deliverToListeners(registration -> Runnable::run);
+ }
+
+ private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+ private final int mValue;
+
+ protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+ super(binder, MoreExecutors.directExecutor(), runnable);
+ mValue = value;
+ }
+
+ @Override
+ public TestMultiplexer getOwner() {
+ return TestMultiplexer.this;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
new file mode 100644
index 0000000..9281e42
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Unit test for {@link MergedDiscoveryRequest} class.
+ */
+public class MergedDiscoveryRequestTest {
+
+ @Test
+ public void test_addScanType() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addScanType(ScanRequest.SCAN_TYPE_FAST_PAIR);
+ builder.addScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
+ MergedDiscoveryRequest request = builder.build();
+
+ assertThat(request.getScanTypes()).isEqualTo(new ArraySet<>(
+ Arrays.asList(ScanRequest.SCAN_TYPE_FAST_PAIR,
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)));
+ }
+
+ @Test
+ public void test_addActions() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addActions(new ArrayList<>(Arrays.asList(1, 2, 3)));
+ builder.addActions(new ArraySet<>(Arrays.asList(2, 3, 4)));
+ builder.addActions(new ArraySet<>(Collections.singletonList(5)));
+
+ MergedDiscoveryRequest request = builder.build();
+ assertThat(request.getActions()).isEqualTo(new ArraySet<>(new Integer[]{1, 2, 3, 4, 5}));
+ }
+
+ @Test
+ public void test_addFilters() {
+ final int rssi = -40;
+ final int action = 123;
+ final byte[] secreteId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+ final int key = 3;
+ final byte[] value = new byte[]{1, 1, 1, 1};
+
+ PublicCredential mPublicCredential = new PublicCredential.Builder(secreteId,
+ authenticityKey, publicKey, encryptedMetadata,
+ metadataEncryptionKeyTag).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+ PresenceScanFilter scanFilterBuilder = new PresenceScanFilter.Builder().setMaxPathLoss(
+ rssi).addCredential(mPublicCredential).addPresenceAction(
+ action).addExtendedProperty(new DataElement(key, value)).build();
+
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addScanFilters(Collections.singleton(scanFilterBuilder));
+ MergedDiscoveryRequest request = builder.build();
+
+ Set<ScanFilter> expectedResult = new ArraySet<>();
+ expectedResult.add(scanFilterBuilder);
+ assertThat(request.getScanFilters()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void test_addMedium() {
+ MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+ MergedDiscoveryRequest request = builder.build();
+
+ Set<Integer> expectedResult = new ArraySet<>();
+ expectedResult.add(MergedDiscoveryRequest.Medium.BLE);
+ assertThat(request.getMediums()).isEqualTo(expectedResult);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
new file mode 100644
index 0000000..8814190
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+/**
+ * Unit test for {@link BinderListenerRegistration} class.
+ */
+public class BinderListenerRegistrationTest {
+ private TestMultiplexer mMultiplexer;
+ private boolean mOnRegisterCalled;
+ private boolean mOnUnRegisterCalled;
+
+ @Before
+ public void setUp() {
+ mMultiplexer = new TestMultiplexer();
+ }
+
+ @Test
+ public void test_addAndRemove() throws RemoteException {
+ Runnable listener = mock(Runnable.class);
+ IBinder binder = mock(IBinder.class);
+ int value = 2;
+ BinderListenerRegistration<Runnable> registration = mMultiplexer.addListener(binder,
+ listener, value);
+ // First element, onRegister should be called
+ assertThat(mOnRegisterCalled).isTrue();
+ verify(binder, times(1)).linkToDeath(any(), anyInt());
+ mMultiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+
+ Runnable listener2 = mock(Runnable.class);
+ IBinder binder2 = mock(IBinder.class);
+ int value2 = 1;
+ BinderListenerRegistration<Runnable> registration2 = mMultiplexer.addListener(binder2,
+ listener2, value2);
+ verify(binder2, times(1)).linkToDeath(any(), anyInt());
+ mMultiplexer.notifyListeners();
+ verify(listener2, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+ reset(listener2);
+
+ registration2.remove();
+ verify(binder2, times(1)).unlinkToDeath(any(), anyInt());
+ // Remove one element, onUnregister should NOT be called
+ assertThat(mOnUnRegisterCalled).isFalse();
+ mMultiplexer.notifyListeners();
+ verify(listener, times(1)).run();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(value);
+ }
+ reset(listener);
+ reset(listener2);
+
+ registration.remove();
+ verify(binder, times(1)).unlinkToDeath(any(), anyInt());
+ // Remove all elements, onUnregister should NOT be called
+ assertThat(mOnUnRegisterCalled).isTrue();
+ synchronized (mMultiplexer.mMultiplexerLock) {
+ assertThat(mMultiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+ }
+ }
+
+ private class TestMultiplexer extends
+ ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+ @Override
+ public void onRegister() {
+ mOnRegisterCalled = true;
+ }
+
+ @Override
+ public void onUnregister() {
+ mOnUnRegisterCalled = true;
+ }
+
+ @Override
+ public Integer mergeRegistrations(
+ @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+ int max = Integer.MIN_VALUE;
+ for (TestListenerRegistration registration : testListenerRegistrations) {
+ max = Math.max(max, registration.getValue());
+ }
+ return max;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+ }
+
+ public BinderListenerRegistration<Runnable> addListener(IBinder binder, Runnable runnable,
+ int value) {
+ TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+ value);
+ putRegistration(binder, registration);
+ return registration;
+ }
+
+ public void notifyListeners() {
+ deliverToListeners(registration -> Runnable::run);
+ }
+
+ private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+ private final int mValue;
+
+ protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+ super(binder, MoreExecutors.directExecutor(), runnable);
+ mValue = value;
+ }
+
+ @Override
+ public TestMultiplexer getOwner() {
+ return TestMultiplexer.this;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
new file mode 100644
index 0000000..03c4f75
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.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 static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.AppOpsManager;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryRegistration} class.
+ */
+public class DiscoveryRegistrationTest {
+ private static final int RSSI = -40;
+ private static final int ACTION = 123;
+ private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+ private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
+ private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final int KEY = 3;
+ private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+ private final PublicCredential mPublicCredential = new PublicCredential.Builder(SECRETE_ID,
+ AUTHENTICITY_KEY, PUBLIC_KEY, ENCRYPTED_METADATA,
+ METADATA_ENCRYPTION_KEY_TAG).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+ private final PresenceScanFilter mFilter = new PresenceScanFilter.Builder().setMaxPathLoss(
+ 50).addCredential(mPublicCredential).addPresenceAction(ACTION).addExtendedProperty(
+ new DataElement(KEY, VALUE)).build();
+ private DiscoveryRegistration mDiscoveryRegistration;
+ private ScanRequest mScanRequest;
+ private TestDiscoveryManager mOwner;
+ private Object mMultiplexLock;
+ @Mock
+ private IScanListener mCallback;
+ @Mock
+ private CallerIdentity mIdentity;
+ @Mock
+ private AppOpsManager mAppOpsManager;
+ @Mock
+ private IBinder mBinder;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ when(mCallback.asBinder()).thenReturn(mBinder);
+ when(mAppOpsManager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+ eq(null))).thenReturn(AppOpsManager.MODE_ALLOWED);
+
+ mOwner = new TestDiscoveryManager();
+ mMultiplexLock = new Object();
+ mScanRequest = new ScanRequest.Builder().setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(mFilter).build();
+ mDiscoveryRegistration = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+ Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, mAppOpsManager);
+ }
+
+ @Test
+ public void test_getScanRequest() {
+ assertThat(mDiscoveryRegistration.getScanRequest()).isEqualTo(mScanRequest);
+ }
+
+ @Test
+ public void test_getActions() {
+ Set<Integer> result = new ArraySet<>();
+ result.add(ACTION);
+ assertThat(mDiscoveryRegistration.getActions()).isEqualTo(result);
+ }
+
+ @Test
+ public void test_getOwner() {
+ assertThat(mDiscoveryRegistration.getOwner()).isEqualTo(mOwner);
+ }
+
+ @Test
+ public void test_getPresenceScanFilters() {
+ Set<ScanFilter> result = new ArraySet<>();
+ result.add(mFilter);
+ assertThat(mDiscoveryRegistration.getPresenceScanFilters()).isEqualTo(result);
+ }
+
+ @Test
+ public void test_presenceFilterMatches_match() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isTrue();
+ }
+
+ @Test
+ public void test_presenceFilterMatches_emptyFilter() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of())).isTrue();
+ }
+
+ @Test
+ public void test_presenceFilterMatches_actionNotMatch() {
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 12).setName("test").setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(5).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isFalse();
+ }
+
+ @Test
+ public void test_onDiscoveredOnUpdatedCalled() throws Exception {
+ final long deviceId = 122;
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder().setDeviceId(
+ deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG);
+ runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.build()));
+
+ verify(mCallback, times(1)).onDiscovered(eq(builder.build()));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, never()).onError(anyInt());
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ reset(mCallback);
+
+ // Update RSSI
+ runOperation(
+ mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.setRssi(RSSI - 1).build()));
+ verify(mCallback, never()).onDiscovered(any());
+ verify(mCallback, times(1)).onUpdated(eq(builder.build()));
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, never()).onError(anyInt());
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ }
+
+ @Test
+ public void test_onLost() throws Exception {
+ final long deviceId = 123;
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(device));
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+ verify(mCallback, times(1)).onDiscovered(eq(device));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onError(anyInt());
+ verify(mCallback, never()).onLost(any());
+ reset(mCallback);
+
+ runOperation(mDiscoveryRegistration.reportDeviceLost(device));
+
+ assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNull();
+ verify(mCallback, never()).onDiscovered(eq(device));
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onError(anyInt());
+ verify(mCallback, times(1)).onLost(eq(device));
+ }
+
+ @Test
+ public void test_onError() throws Exception {
+ AppOpsManager manager = mock(AppOpsManager.class);
+ when(manager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+ eq(null))).thenReturn(AppOpsManager.MODE_IGNORED);
+
+ DiscoveryRegistration r = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+ Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, manager);
+
+ NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+ 123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+ ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+ METADATA_ENCRYPTION_KEY_TAG).build();
+ runOperation(r.onNearbyDeviceDiscovered(device));
+
+ verify(mCallback, never()).onDiscovered(any());
+ verify(mCallback, never()).onUpdated(any());
+ verify(mCallback, never()).onLost(any());
+ verify(mCallback, times(1)).onError(eq(ScanCallback.ERROR_PERMISSION_DENIED));
+ }
+
+ private void runOperation(BinderListenerRegistration.ListenerOperation<IScanListener> operation)
+ throws Exception {
+ if (operation == null) {
+ return;
+ }
+ operation.onScheduled(false);
+ operation.operate(mCallback);
+ operation.onComplete(/* success= */ true);
+ }
+
+ private static class TestDiscoveryManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> {
+
+ @Override
+ public MergedDiscoveryRequest mergeRegistrations(
+ @NonNull Collection<DiscoveryRegistration> discoveryRegistrations) {
+ return null;
+ }
+
+ @Override
+ public void onMergedRegistrationsUpdated() {
+
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
new file mode 100644
index 0000000..e186709
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.nearby.BroadcastRequest;
+
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Unit test for {@link DataElementHeader}.
+ */
+public class DataElementHeaderTest {
+
+ private static final int VERSION = BroadcastRequest.PRESENCE_VERSION_V1;
+
+ @Test
+ public void test_illegalLength() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new DataElementHeader(VERSION, 12, 128));
+ }
+
+ @Test
+ public void test_singeByteConversion() {
+ DataElementHeader header = new DataElementHeader(VERSION, 12, 3);
+ byte[] bytes = header.toBytes();
+ assertThat(bytes).isEqualTo(new byte[]{(byte) 0b00111100});
+
+ DataElementHeader afterConversionHeader = DataElementHeader.fromBytes(VERSION, bytes);
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(3);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(12);
+ }
+
+ @Test
+ public void test_multipleBytesConversion() {
+ DataElementHeader header = new DataElementHeader(VERSION, 6, 100);
+ DataElementHeader afterConversionHeader =
+ DataElementHeader.fromBytes(VERSION, header.toBytes());
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(100);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(6);
+ }
+
+ @Test
+ public void test_fromBytes() {
+ // Single byte case.
+ byte[] singleByte = new byte[]{(byte) 0b01011101};
+ DataElementHeader singeByteHeader = DataElementHeader.fromBytes(VERSION, singleByte);
+ assertThat(singeByteHeader.getDataLength()).isEqualTo(5);
+ assertThat(singeByteHeader.getDataType()).isEqualTo(13);
+
+ // Two bytes case.
+ byte[] twoBytes = new byte[]{(byte) 0b11011101, (byte) 0b01011101};
+ DataElementHeader twoBytesHeader = DataElementHeader.fromBytes(VERSION, twoBytes);
+ assertThat(twoBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(twoBytesHeader.getDataType()).isEqualTo(93);
+
+ // Three bytes case.
+ byte[] threeBytes = new byte[]{(byte) 0b11011101, (byte) 0b11111111, (byte) 0b01011101};
+ DataElementHeader threeBytesHeader = DataElementHeader.fromBytes(VERSION, threeBytes);
+ assertThat(threeBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(threeBytesHeader.getDataType()).isEqualTo(16349);
+
+ // Four bytes case.
+ byte[] fourBytes = new byte[]{
+ (byte) 0b11011101, (byte) 0b11111111, (byte) 0b11111111, (byte) 0b01011101};
+
+ DataElementHeader fourBytesHeader = DataElementHeader.fromBytes(VERSION, fourBytes);
+ assertThat(fourBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(fourBytesHeader.getDataType()).isEqualTo(2097117);
+ }
+
+ @Test
+ public void test_fromBytesIllegal_singleByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION, new byte[]{(byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_twoBytes_wrongFirstByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b01011101, (byte) 0b01011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_twoBytes_wrongLastByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b11011101, (byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_threeBytes() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b11011101, (byte) 0b11011101, (byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_multipleBytesConversion_largeNumber() {
+ DataElementHeader header = new DataElementHeader(VERSION, 22213546, 66);
+ DataElementHeader afterConversionHeader =
+ DataElementHeader.fromBytes(VERSION, header.toBytes());
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(66);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(22213546);
+ }
+
+ @Test
+ public void test_isExtending() {
+ assertThat(DataElementHeader.isExtending((byte) 0b10000100)).isTrue();
+ assertThat(DataElementHeader.isExtending((byte) 0b01110100)).isFalse();
+ assertThat(DataElementHeader.isExtending((byte) 0b00000000)).isFalse();
+ }
+
+ @Test
+ public void test_convertTag() {
+ assertThat(DataElementHeader.convertTag(true)).isEqualTo((byte) 128);
+ assertThat(DataElementHeader.convertTag(false)).isEqualTo(0);
+ }
+
+ @Test
+ public void test_getHeaderValue() {
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b10000100)).isEqualTo(4);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b00000100)).isEqualTo(4);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b11010100)).isEqualTo(84);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b01010100)).isEqualTo(84);
+ }
+
+ @Test
+ public void test_convertTypeMultipleIntList() {
+ List<Byte> list = DataElementHeader.convertTypeMultipleBytes(128);
+ assertThat(list.size()).isEqualTo(2);
+ assertThat(list.get(0)).isEqualTo((byte) 0b10000001);
+ assertThat(list.get(1)).isEqualTo((byte) 0b00000000);
+
+ List<Byte> list2 = DataElementHeader.convertTypeMultipleBytes(10);
+ assertThat(list2.size()).isEqualTo(1);
+ assertThat(list2.get(0)).isEqualTo((byte) 0b00001010);
+
+ List<Byte> list3 = DataElementHeader.convertTypeMultipleBytes(5242398);
+ assertThat(list3.size()).isEqualTo(4);
+ assertThat(list3.get(0)).isEqualTo((byte) 0b10000010);
+ assertThat(list3.get(1)).isEqualTo((byte) 0b10111111);
+ assertThat(list3.get(2)).isEqualTo((byte) 0b11111100);
+ assertThat(list3.get(3)).isEqualTo((byte) 0b00011110);
+ }
+
+ @Test
+ public void test_getTypeMultipleBytes() {
+ byte[] inputBytes = new byte[]{(byte) 0b11011000, (byte) 0b10000000, (byte) 0b00001001};
+ // 0b101100000000000001001
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes)).isEqualTo(1441801);
+
+ byte[] inputBytes2 = new byte[]{(byte) 0b00010010};
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes2)).isEqualTo(18);
+
+ byte[] inputBytes3 = new byte[]{(byte) 0b10000001, (byte) 0b00000000};
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes3)).isEqualTo(128);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
new file mode 100644
index 0000000..895df69
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+import android.nearby.PublicCredential;
+
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ExtendedAdvertisementTest {
+ private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ private static final int DATA_TYPE_MODEL_ID = 7;
+ private static final int DATA_TYPE_BLE_ADDRESS = 101;
+ private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
+ private static final byte[] MODE_ID_DATA =
+ new byte[]{2, 1, 30, 2, 10, -16, 6, 22, 44, -2, -86, -69, -52};
+ private static final byte[] BLE_ADDRESS = new byte[]{124, 4, 56, 60, 120, -29, -90};
+ private static final DataElement MODE_ID_ADDRESS_ELEMENT =
+ new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA);
+ private static final DataElement BLE_ADDRESS_ELEMENT =
+ new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
+
+ private static final byte[] IDENTITY =
+ new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final int MEDIUM_TYPE_BLE = 0;
+ private static final byte[] SALT = {2, 3};
+ private static final int PRESENCE_ACTION_1 = 1;
+ private static final int PRESENCE_ACTION_2 = 2;
+
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+ private static final byte[] PUBLIC_KEY =
+ new byte[] {
+ 48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
+ 66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
+ -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
+ -65, 64, 91, -109, -45, -35, -56, 55, -79, 47, -85, 27, -96, -119, -82, -80,
+ 123, 41, -119, -25, 1, -112, 112
+ };
+ private static final byte[] ENCRYPTED_METADATA_BYTES =
+ new byte[] {
+ -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
+ -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
+ 88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
+ -118, -61, -37, -104, 60, 105, 115, 1, 56, -89, -107, -45, -116, -1, -25, 84,
+ -19, -128, 81, 11, 92, 77, -58, 82, 122, 123, 31, -87, -57, 70, 23, -81, 7, 2,
+ -114, -83, 74, 124, -68, -98, 47, 91, 9, 48, -67, 41, -7, -97, 78, 66, -65, 58,
+ -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
+ };
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
+ new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+ private static final String DEVICE_NAME = "test_device";
+
+ private PresenceBroadcastRequest.Builder mBuilder;
+ private PrivateCredential mPrivateCredential;
+ private PublicCredential mPublicCredential;
+
+ @Before
+ public void setUp() {
+ mPrivateCredential =
+ new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mPublicCredential =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
+ .build();
+ mBuilder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1)
+ .addAction(PRESENCE_ACTION_2)
+ .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
+ .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+ }
+
+ @Test
+ public void test_createFromRequest() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ assertThat(originalAdvertisement.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(originalAdvertisement.getLength()).isEqualTo(66);
+ assertThat(originalAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(originalAdvertisement.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_createFromRequest_encodeAndDecode() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ byte[] generatedBytes = originalAdvertisement.toBytes();
+
+ ExtendedAdvertisement newAdvertisement =
+ ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
+
+ assertThat(newAdvertisement.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(newAdvertisement.getLength()).isEqualTo(66);
+ assertThat(newAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(newAdvertisement.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_createFromRequest_invalidParameter() {
+ // invalid version
+ mBuilder.setVersion(BroadcastRequest.PRESENCE_VERSION_V0);
+ assertThat(ExtendedAdvertisement.createFromRequest(mBuilder.build())).isNull();
+
+ // invalid salt
+ PresenceBroadcastRequest.Builder builder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ new byte[]{1, 2, 3}, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder.build())).isNull();
+
+ // invalid identity
+ PrivateCredential privateCredential =
+ new PrivateCredential.Builder(SECRET_ID,
+ AUTHENTICITY_KEY, new byte[]{1, 2, 3, 4}, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ PresenceBroadcastRequest.Builder builder2 =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ new byte[]{1, 2, 3}, privateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
+
+ // empty action
+ PresenceBroadcastRequest.Builder builder3 =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
+ }
+
+ @Test
+ public void test_toBytes() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
+ }
+
+ @Test
+ public void test_fromBytes() {
+ byte[] originalBytes = getExtendedAdvertisementByteArray();
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
+
+ assertThat(adv.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(adv.getLength()).isEqualTo(66);
+ assertThat(adv.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(adv.getSalt()).isEqualTo(SALT);
+ assertThat(adv.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_toString() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
+ + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
+ + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+ + " actions: [1, 2]>");
+ }
+
+ @Test
+ public void test_getDataElements_accordingToType() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ List<DataElement> dataElements = new ArrayList<>();
+
+ dataElements.add(BLE_ADDRESS_ELEMENT);
+ assertThat(adv.getDataElements(DATA_TYPE_BLE_ADDRESS)).isEqualTo(dataElements);
+ assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
+ }
+
+ private static byte[] getExtendedAdvertisementByteArray() {
+ ByteBuffer buffer = ByteBuffer.allocate(66);
+ buffer.put((byte) 0b00100000); // Header V1
+ buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+ // Salt data
+ buffer.put(SALT);
+ // Identity header: length 16, type 1 (private identity)
+ buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
+ // Identity data
+ buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+
+ ByteBuffer deBuffer = ByteBuffer.allocate(28);
+ // Action1 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action1 data
+ deBuffer.put((byte) PRESENCE_ACTION_1);
+ // Action2 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action2 data
+ deBuffer.put((byte) PRESENCE_ACTION_2);
+ // Ble address header: length 7, type 102
+ deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
+ // Ble address data
+ deBuffer.put(BLE_ADDRESS);
+ // model id header: length 13, type 7
+ deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
+ // model id data
+ deBuffer.put(MODE_ID_DATA);
+
+ byte[] data = deBuffer.array();
+ CryptorImpV1 cryptor = CryptorImpV1.getInstance();
+ buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
+ buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+
+ return buffer.array();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
new file mode 100644
index 0000000..c4fccf7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link ExtendedAdvertisementUtils}.
+ */
+public class ExtendedAdvertisementUtilsTest {
+ private static final byte[] ADVERTISEMENT1 = new byte[]{0b00100000, 12, 34, 78, 10};
+ private static final byte[] ADVERTISEMENT2 = new byte[]{0b00100000, 0b00100011, 34, 78,
+ (byte) 0b10010000, (byte) 0b00000100,
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ private static final int DATA_TYPE_SALT = 3;
+ private static final int DATA_TYPE_PRIVATE_IDENTITY = 4;
+
+ @Test
+ public void test_constructHeader() {
+ assertThat(ExtendedAdvertisementUtils.constructHeader(1)).isEqualTo(0b100000);
+ assertThat(ExtendedAdvertisementUtils.constructHeader(0)).isEqualTo(0);
+ assertThat(ExtendedAdvertisementUtils.constructHeader(6)).isEqualTo((byte) 0b11000000);
+ }
+
+ @Test
+ public void test_getVersion() {
+ assertThat(ExtendedAdvertisementUtils.getVersion(ADVERTISEMENT1)).isEqualTo(1);
+ byte[] adv = new byte[]{(byte) 0b10111100, 9, 19, 90, 23};
+ assertThat(ExtendedAdvertisementUtils.getVersion(adv)).isEqualTo(5);
+ byte[] adv2 = new byte[]{(byte) 0b10011111, 9, 19, 90, 23};
+ assertThat(ExtendedAdvertisementUtils.getVersion(adv2)).isEqualTo(4);
+ }
+
+ @Test
+ public void test_getDataElementHeader_salt() {
+ byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 1);
+ DataElementHeader header = DataElementHeader.fromBytes(
+ BroadcastRequest.PRESENCE_VERSION_V1, saltHeaderArray);
+ assertThat(header.getDataType()).isEqualTo(DATA_TYPE_SALT);
+ assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH);
+ }
+
+ @Test
+ public void test_getDataElementHeader_identity() {
+ byte[] identityHeaderArray =
+ ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 4);
+ DataElementHeader header = DataElementHeader.fromBytes(BroadcastRequest.PRESENCE_VERSION_V1,
+ identityHeaderArray);
+ assertThat(header.getDataType()).isEqualTo(DATA_TYPE_PRIVATE_IDENTITY);
+ assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH);
+ }
+
+ @Test
+ public void test_constructDataElement_salt() {
+ DataElement salt = new DataElement(DATA_TYPE_SALT, new byte[]{13, 14});
+ byte[] saltArray = ExtendedAdvertisementUtils.convertDataElementToBytes(salt);
+ // Data length and salt header length.
+ assertThat(saltArray.length).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH + 1);
+ // Header
+ assertThat(saltArray[0]).isEqualTo((byte) 0b00100011);
+ // Data
+ assertThat(saltArray[1]).isEqualTo((byte) 13);
+ assertThat(saltArray[2]).isEqualTo((byte) 14);
+ }
+
+ @Test
+ public void test_constructDataElement_privateIdentity() {
+ byte[] identityData = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ DataElement identity = new DataElement(DATA_TYPE_PRIVATE_IDENTITY, identityData);
+ byte[] identityArray = ExtendedAdvertisementUtils.convertDataElementToBytes(identity);
+ // Data length and identity header length.
+ assertThat(identityArray.length).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH + 2);
+ // 1st header byte
+ assertThat(identityArray[0]).isEqualTo((byte) 0b10010000);
+ // 2st header byte
+ assertThat(identityArray[1]).isEqualTo((byte) 0b00000100);
+ // Data
+ assertThat(Arrays.copyOfRange(identityArray, 2, identityArray.length))
+ .isEqualTo(identityData);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
index 5e0ccbe..8e3e068 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -75,6 +75,15 @@
assertThat(originalAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V0);
assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(originalAdvertisement.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(originalAdvertisement.toString())
+ .isEqualTo("FastAdvertisement:<VERSION: 0, length: 19,"
+ + " ltvFieldCount: 4,"
+ + " identityType: 1,"
+ + " identity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],"
+ + " salt: [2, 3],"
+ + " actions: [123],"
+ + " txPower: 4");
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
index 39cab94..856c1a8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
import android.nearby.PresenceCredential;
import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
@@ -28,12 +30,15 @@
import org.junit.Before;
import org.junit.Test;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
/**
* Unit tests for {@link PresenceDiscoveryResult}.
*/
public class PresenceDiscoveryResultTest {
+ private static final int DATA_TYPE_ACCOUNT_KEY = 9;
+ private static final int DATA_TYPE_INTENT = 6;
private static final int PRESENCE_ACTION = 123;
private static final int TX_POWER = -1;
private static final int RSSI = -41;
@@ -43,6 +48,8 @@
private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final byte[] META_DATA_ENCRYPTION_KEY =
+ new byte[] {-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
private PresenceDiscoveryResult.Builder mBuilder;
private PublicCredential mCredential;
@@ -59,18 +66,68 @@
.setSalt(SALT)
.setTxPower(TX_POWER)
.setRssi(RSSI)
+ .setEncryptedIdentityTag(METADATA_ENCRYPTION_KEY_TAG)
.addPresenceAction(PRESENCE_ACTION);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToDevice() {
- PresenceDiscoveryResult discoveryResult = mBuilder.build();
- PresenceDevice presenceDevice = discoveryResult.toPresenceDevice();
+ public void testFromDevice() {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ builder.setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+ .setSalt(SALT)
+ .setPublicCredential(mCredential);
- assertThat(presenceDevice.getRssi()).isEqualTo(RSSI);
- assertThat(Arrays.equals(presenceDevice.getSalt(), SALT)).isTrue();
- assertThat(Arrays.equals(presenceDevice.getSecretId(), SECRET_ID)).isTrue();
+ PresenceDiscoveryResult discoveryResult =
+ PresenceDiscoveryResult.fromDevice(builder.build());
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addCredential(mCredential)
+ .build();
+
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFromDevice_presenceDeviceAvailable() {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ PresenceDevice presenceDevice =
+ new PresenceDevice.Builder("123", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY)
+ .addExtendedProperty(new DataElement(
+ DATA_TYPE_INTENT, new byte[]{(byte) PRESENCE_ACTION}))
+ .build();
+ builder.setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+ .setPresenceDevice(presenceDevice)
+ .setPublicCredential(mCredential);
+
+ PresenceDiscoveryResult discoveryResult =
+ PresenceDiscoveryResult.fromDevice(builder.build());
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAccountMatches() {
+ DataElement accountKey = new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4});
+ mBuilder.addExtendedProperties(List.of(accountKey));
+ PresenceDiscoveryResult discoveryResult = mBuilder.build();
+
+ List<DataElement> extendedProperties = new ArrayList<>();
+ extendedProperties.add(new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4}));
+ extendedProperties.add(new DataElement(DATA_TYPE_INTENT,
+ new byte[]{(byte) PRESENCE_ACTION}));
+ assertThat(discoveryResult.accountKeyMatches(extendedProperties)).isTrue();
}
@Test
@@ -86,4 +143,24 @@
assertThat(discoveryResult.matches(scanFilter)).isTrue();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_notMatches() {
+ PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder()
+ .setPublicCredential(mCredential)
+ .setSalt(SALT)
+ .setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptedIdentityTag(new byte[]{5, 4, 3, 2, 1})
+ .addPresenceAction(PRESENCE_ACTION);
+
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ PresenceDiscoveryResult discoveryResult = builder.build();
+ assertThat(discoveryResult.matches(scanFilter)).isFalse();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
new file mode 100644
index 0000000..ca4f077
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.PresenceDevice;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class PresenceManagerTest {
+ private static final byte[] IDENTITY =
+ new byte[] {1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final byte[] SALT = {2, 3};
+ private static final byte[] SECRET_ID =
+ new byte[] {-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+
+ @Mock private Context mContext;
+ private PresenceManager mPresenceManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mPresenceManager = new PresenceManager(mContext);
+ when(mContext.getContentResolver())
+ .thenReturn(InstrumentationRegistry.getInstrumentation()
+ .getContext().getContentResolver());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testInit() {
+ mPresenceManager.initiate();
+
+ verify(mContext, times(1)).registerReceiver(any(), any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDeviceStatusUpdated() {
+ DataElement dataElement1 = new DataElement(1, new byte[] {1, 2});
+ DataElement dataElement2 = new DataElement(2, new byte[] {-1, -2, 3, 4, 5, 6, 7, 8, 9});
+
+ PresenceDevice presenceDevice =
+ new PresenceDevice.Builder(/* deviceId= */ "deviceId", SALT, SECRET_ID, IDENTITY)
+ .addExtendedProperty(dataElement1)
+ .addExtendedProperty(dataElement2)
+ .build();
+
+ mPresenceManager.mScanCallback.onDiscovered(presenceDevice);
+ mPresenceManager.mScanCallback.onUpdated(presenceDevice);
+ mPresenceManager.mScanCallback.onLost(presenceDevice);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index d06a785..05b556b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.provider;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -24,12 +25,14 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSetCallback;
import android.content.Context;
+import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
import androidx.test.core.app.ApplicationProvider;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import com.google.common.util.concurrent.MoreExecutors;
@@ -59,9 +62,10 @@
}
@Test
- public void testOnStatus_success() {
+ public void testOnStatus_success_fastAdv() {
byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
- mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+ advertiseBytes, mBroadcastListener);
AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
mBleBroadcastProvider.onStartSuccess(settings);
@@ -69,15 +73,47 @@
}
@Test
- public void testOnStatus_failure() {
+ public void testOnStatus_success_extendedAdv() {
byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
- mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+ advertiseBytes, mBroadcastListener);
+
+ // advertising set can not be mocked, so we will allow nulls
+ mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+ AdvertisingSetCallback.ADVERTISE_SUCCESS);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ }
+
+ @Test
+ public void testOnStatus_failure_fastAdv() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+ advertiseBytes, mBroadcastListener);
mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
verify(mBroadcastListener, times(1))
.onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
}
+ @Test
+ public void testOnStatus_failure_extendedAdv() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+ advertiseBytes, mBroadcastListener);
+
+ // advertising set can not be mocked, so we will allow nulls
+ mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+ AdvertisingSetCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+ // Can be additional failure if the test device does not support LE Extended Advertising.
+ verify(mBroadcastListener, atLeastOnce())
+ .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+ }
+
+ @Test
+ public void testStop() {
+ mBleBroadcastProvider.stop();
+ }
+
private static class TestInjector implements Injector {
@Override
@@ -88,7 +124,7 @@
}
@Override
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ public ContextHubManager getContextHubManager() {
return null;
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index 902cc33..2d8bd63 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -17,6 +17,9 @@
package com.android.server.nearby.provider;
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+import static android.nearby.ScanCallback.ERROR_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
@@ -30,10 +33,13 @@
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Context;
+import android.hardware.location.ContextHubManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -42,6 +48,8 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
public final class BleDiscoveryProviderTest {
@@ -49,6 +57,8 @@
private BleDiscoveryProvider mBleDiscoveryProvider;
@Mock
private AbstractDiscoveryProvider.Listener mListener;
+// @Mock
+// private BluetoothAdapter mBluetoothAdapter;
@Before
public void setup() {
@@ -61,7 +71,7 @@
}
@Test
- public void test_callback() throws InterruptedException {
+ public void test_callback_found() throws InterruptedException {
mBleDiscoveryProvider.getController().setListener(mListener);
mBleDiscoveryProvider.onStart();
mBleDiscoveryProvider.getScanCallback()
@@ -73,11 +83,39 @@
}
@Test
+ public void test_callback_failed() throws InterruptedException {
+ mBleDiscoveryProvider.getController().setListener(mListener);
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.getScanCallback().onScanFailed(1);
+
+
+ // Wait for callback to be invoked
+ Thread.sleep(500);
+ verify(mListener, times(1)).onError(ERROR_UNKNOWN);
+ }
+
+ @Test
public void test_stopScan() {
mBleDiscoveryProvider.onStart();
mBleDiscoveryProvider.onStop();
}
+ @Test
+ public void test_stopScan_filersReset() {
+ List<ScanFilter> filterList = new ArrayList<>();
+ filterList.add(getSanFilter());
+
+ mBleDiscoveryProvider.getController().setProviderScanFilters(filterList);
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.onStop();
+ assertThat(mBleDiscoveryProvider.getFiltersLocked()).isNull();
+ }
+
+ @Test
+ public void testInvalidateScanMode() {
+ mBleDiscoveryProvider.invalidateScanMode();
+ }
+
private class TestInjector implements Injector {
@Override
public BluetoothAdapter getBluetoothAdapter() {
@@ -85,7 +123,7 @@
}
@Override
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ public ContextHubManager getContextHubManager() {
return null;
}
@@ -125,4 +163,22 @@
return null;
}
}
+
+ private static PresenceScanFilter getSanFilter() {
+ return new PresenceScanFilter.Builder()
+ .setMaxPathLoss(70)
+ .addCredential(getPublicCredential())
+ .addPresenceAction(124)
+ .build();
+ }
+
+ private static PublicCredential getPublicCredential() {
+ return new PublicCredential.Builder(
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2})
+ .build();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
index 1b29b52..ce479c8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -16,18 +16,33 @@
package com.android.server.nearby.provider;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+import static com.android.server.nearby.provider.ChreCommunication.INVALID_NANO_APP_VERSION;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.Context;
import android.hardware.location.ContextHubClient;
import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
+import android.os.Build;
+import android.provider.DeviceConfig;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -42,8 +57,12 @@
import java.util.concurrent.Executor;
public class ChreCommunicationTest {
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private static final int APP_VERSION = 1;
+
@Mock Injector mInjector;
- @Mock ContextHubManagerAdapter mManager;
+ @Mock Context mContext;
+ @Mock ContextHubManager mManager;
@Mock ContextHubTransaction<List<NanoAppState>> mTransaction;
@Mock ContextHubTransaction.Response<List<NanoAppState>> mTransactionResponse;
@Mock ContextHubClient mClient;
@@ -56,38 +75,78 @@
@Before
public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ DeviceConfig.setProperty(
+ NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "1", false);
+
MockitoAnnotations.initMocks(this);
- when(mInjector.getContextHubManagerAdapter()).thenReturn(mManager);
+ when(mInjector.getContextHubManager()).thenReturn(mManager);
when(mManager.getContextHubs()).thenReturn(Collections.singletonList(new ContextHubInfo()));
when(mManager.queryNanoApps(any())).thenReturn(mTransaction);
- when(mManager.createClient(any(), any(), any())).thenReturn(mClient);
+ when(mManager.createClient(any(), any(), any(), any())).thenReturn(mClient);
when(mTransactionResponse.getResult()).thenReturn(ContextHubTransaction.RESULT_SUCCESS);
when(mTransactionResponse.getContents())
.thenReturn(
Collections.singletonList(
- new NanoAppState(ChreDiscoveryProvider.NANOAPP_ID, 1, true)));
+ new NanoAppState(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ APP_VERSION,
+ true)));
- mChreCommunication = new ChreCommunication(mInjector, new InlineExecutor());
+ mChreCommunication = new ChreCommunication(mInjector, mContext, new InlineExecutor());
+ }
+
+ @Test
+ public void testStart() {
mChreCommunication.start(
mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
- }
-
- @Test
- public void testStart() {
verify(mChreCallback).started(true);
}
@Test
public void testStop() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
mChreCommunication.stop();
verify(mClient).close();
}
@Test
+ public void testNotReachMinVersion() {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "3", false);
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+ verify(mChreCallback).started(false);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void test_getNanoVersion() {
+ assertThat(mChreCommunication.queryNanoAppVersion()).isEqualTo(INVALID_NANO_APP_VERSION);
+
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+
+ assertThat(mChreCommunication.queryNanoAppVersion()).isEqualTo(APP_VERSION);
+ }
+
+ @Test
public void testSendMessageToNanApp() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
ChreDiscoveryProvider.NANOAPP_ID,
@@ -99,6 +158,8 @@
@Test
public void testOnMessageFromNanoApp() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
ChreDiscoveryProvider.NANOAPP_ID,
@@ -109,13 +170,60 @@
}
@Test
+ public void testContextHubTransactionResultToString() {
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_SUCCESS))
+ .isEqualTo("RESULT_SUCCESS");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_UNKNOWN))
+ .isEqualTo("RESULT_FAILED_UNKNOWN");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_BAD_PARAMS))
+ .isEqualTo("RESULT_FAILED_BAD_PARAMS");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_UNINITIALIZED))
+ .isEqualTo("RESULT_FAILED_UNINITIALIZED");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_BUSY))
+ .isEqualTo("RESULT_FAILED_BUSY");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_AT_HUB))
+ .isEqualTo("RESULT_FAILED_AT_HUB");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_TIMEOUT))
+ .isEqualTo("RESULT_FAILED_TIMEOUT");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE))
+ .isEqualTo("RESULT_FAILED_SERVICE_INTERNAL_FAILURE");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE))
+ .isEqualTo("RESULT_FAILED_HAL_UNAVAILABLE");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(9))
+ .isEqualTo("UNKNOWN_RESULT value=9");
+ }
+
+ @Test
public void testOnHubReset() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
mChreCommunication.onHubReset(mClient);
verify(mChreCallback).onHubReset();
}
@Test
public void testOnNanoAppLoaded() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
mChreCommunication.onNanoAppLoaded(mClient, ChreDiscoveryProvider.NANOAPP_ID);
verify(mChreCallback).onNanoAppRestart(eq(ChreDiscoveryProvider.NANOAPP_ID));
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 7c0dd92..154441b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -16,16 +16,34 @@
package com.android.server.nearby.provider;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.content.Context;
import android.hardware.location.NanoAppMessage;
-import android.nearby.ScanFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.OffloadCapability;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+
import com.google.protobuf.ByteString;
import org.junit.Before;
@@ -35,22 +53,39 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import service.proto.Blefilter;
-
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import service.proto.Blefilter;
+
public class ChreDiscoveryProviderTest {
@Mock AbstractDiscoveryProvider.Listener mListener;
@Mock ChreCommunication mChreCommunication;
+ @Mock IBinder mIBinder;
@Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
+ @Captor ArgumentCaptor<NearbyDeviceParcelable> mNearbyDevice;
+ private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private static final int DATA_TYPE_CONNECTION_STATUS_KEY = 10;
+ private static final int DATA_TYPE_BATTERY_KEY = 11;
+ private static final int DATA_TYPE_TX_POWER_KEY = 5;
+ private static final int DATA_TYPE_BLUETOOTH_ADDR_KEY = 101;
+ private static final int DATA_TYPE_FP_ACCOUNT_KEY = 9;
+ private static final int DATA_TYPE_BLE_SERVICE_DATA_KEY = 100;
+ private static final int DATA_TYPE_TEST_DE_BEGIN_KEY = 2147483520;
+ private static final int DATA_TYPE_TEST_DE_END_KEY = 2147483647;
+
+ private final Object mLock = new Object();
private ChreDiscoveryProvider mChreDiscoveryProvider;
+ private OffloadCapability mOffloadCapability;
@Before
public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getInstrumentation().getContext();
mChreDiscoveryProvider =
@@ -59,13 +94,68 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testOnStart() {
- List<ScanFilter> scanFilters = new ArrayList<>();
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.onStart();
+ public void testInit() {
+ mChreDiscoveryProvider.init();
verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
mChreCallbackCaptor.getValue().started(true);
- verify(mChreCommunication).sendMessageToNanoApp(any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void test_queryInvalidVersion() {
+ when(mChreCommunication.queryNanoAppVersion()).thenReturn(
+ (long) ChreCommunication.INVALID_NANO_APP_VERSION);
+ IOffloadCallback callback = new IOffloadCallback() {
+ @Override
+ public void onQueryComplete(OffloadCapability capability) throws RemoteException {
+ synchronized (mLock) {
+ mOffloadCapability = capability;
+ mLock.notify();
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return mIBinder;
+ }
+ };
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ OffloadCapability capability =
+ new OffloadCapability
+ .Builder()
+ .setFastPairSupported(false)
+ .setVersion(ChreCommunication.INVALID_NANO_APP_VERSION)
+ .build();
+ assertThat(mOffloadCapability).isEqualTo(capability);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void test_queryVersion() {
+ long version = 500L;
+ when(mChreCommunication.queryNanoAppVersion()).thenReturn(version);
+ IOffloadCallback callback = new IOffloadCallback() {
+ @Override
+ public void onQueryComplete(OffloadCapability capability) throws RemoteException {
+ synchronized (mLock) {
+ mOffloadCapability = capability;
+ mLock.notify();
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return mIBinder;
+ }
+ };
+ mChreDiscoveryProvider.queryOffloadCapability(callback);
+ OffloadCapability capability =
+ new OffloadCapability
+ .Builder()
+ .setFastPairSupported(true)
+ .setVersion(version)
+ .build();
+ assertThat(mOffloadCapability).isEqualTo(capability);
}
@Test
@@ -93,12 +183,220 @@
ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
results.toByteArray());
mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
mChreDiscoveryProvider.onStart();
verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
verify(mListener).onNearbyDeviceDiscovered(any());
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnNearbyDeviceDiscoveredWithDataElements() {
+ // Disables the setting of test app support
+ boolean isSupportedTestApp = getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ if (isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+ }
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
+
+ final byte [] connectionStatus = new byte[] {1, 2, 3};
+ final byte [] batteryStatus = new byte[] {4, 5, 6};
+ final byte [] txPower = new byte[] {2};
+ final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+ final byte [] fastPairAccountKey = new byte[16];
+ // First byte is length of service data, padding zeros should be thrown away.
+ final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+ final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+ connectionStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+
+ Blefilter.PublicCredential credential =
+ Blefilter.PublicCredential.newBuilder()
+ .setSecretId(ByteString.copyFrom(new byte[] {1}))
+ .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+ .setPublicKey(ByteString.copyFrom(new byte[3]))
+ .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+ .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+ .build();
+ Blefilter.BleFilterResult result =
+ Blefilter.BleFilterResult.newBuilder()
+ .setTxPower(2)
+ .setRssi(1)
+ .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+ .setBleServiceData(ByteString.copyFrom(bleServiceData))
+ .setPublicCredential(credential)
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_CONNECTION_STATUS_KEY)
+ .setValue(ByteString.copyFrom(connectionStatus))
+ .setValueLength(connectionStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_BATTERY_KEY)
+ .setValue(ByteString.copyFrom(batteryStatus))
+ .setValueLength(batteryStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_FP_ACCOUNT_KEY)
+ .setValue(ByteString.copyFrom(fastPairAccountKey))
+ .setValueLength(fastPairAccountKey.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_BEGIN_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_END_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .build();
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ results.toByteArray());
+ mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+ List<DataElement> extendedProperties = PresenceDiscoveryResult
+ .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+ assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+ // Reverts the setting of test app support
+ if (isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+ // Enables the setting of test app support
+ boolean isSupportedTestApp = getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+ if (!isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
+ }
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
+
+ final byte [] connectionStatus = new byte[] {1, 2, 3};
+ final byte [] batteryStatus = new byte[] {4, 5, 6};
+ final byte [] txPower = new byte[] {2};
+ final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+ final byte [] fastPairAccountKey = new byte[16];
+ // First byte is length of service data, padding zeros should be thrown away.
+ final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+ final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+ final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+ connectionStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_TEST_DE_BEGIN_KEY, testData));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_TEST_DE_END_KEY, testData));
+
+ Blefilter.PublicCredential credential =
+ Blefilter.PublicCredential.newBuilder()
+ .setSecretId(ByteString.copyFrom(new byte[] {1}))
+ .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+ .setPublicKey(ByteString.copyFrom(new byte[3]))
+ .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+ .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+ .build();
+ Blefilter.BleFilterResult result =
+ Blefilter.BleFilterResult.newBuilder()
+ .setTxPower(2)
+ .setRssi(1)
+ .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+ .setBleServiceData(ByteString.copyFrom(bleServiceData))
+ .setPublicCredential(credential)
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_CONNECTION_STATUS_KEY)
+ .setValue(ByteString.copyFrom(connectionStatus))
+ .setValueLength(connectionStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_BATTERY_KEY)
+ .setValue(ByteString.copyFrom(batteryStatus))
+ .setValueLength(batteryStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_FP_ACCOUNT_KEY)
+ .setValue(ByteString.copyFrom(fastPairAccountKey))
+ .setValueLength(fastPairAccountKey.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_BEGIN_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(DATA_TYPE_TEST_DE_END_KEY)
+ .setValue(ByteString.copyFrom(testData))
+ .setValueLength(testData.length)
+ )
+ .build();
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ results.toByteArray());
+ mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+ List<DataElement> extendedProperties = PresenceDiscoveryResult
+ .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+ assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+ // Reverts the setting of test app support
+ if (!isSupportedTestApp) {
+ DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+ assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
+ }
+ }
+
+ private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Boolean.parseBoolean(value) : defaultValue;
+ }
+
+ private String getDeviceConfigProperty(String name) {
+ return DeviceConfig.getProperty(NAMESPACE, name);
+ }
+
private static class InLineExecutor implements Executor {
@Override
public void execute(Runnable command) {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
new file mode 100644
index 0000000..a759baf
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+public final class ArrayUtilsTest {
+
+ private static final byte[] BYTES_ONE = new byte[] {7, 9};
+ private static final byte[] BYTES_TWO = new byte[] {8};
+ private static final byte[] BYTES_EMPTY = new byte[] {};
+ private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysNoInput() {
+ assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysOneEmptyArray() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysOneNonEmptyArray() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysMultipleNonEmptyArrays() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysMultipleArrays() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
+ .isEqualTo(BYTES_ALL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsEmptyNull_returnsTrue() {
+ assertThat(ArrayUtils.isEmpty(null)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsEmpty_returnsTrue() {
+ assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsEmpty_returnsFalse() {
+ assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
index 1a22412..71ade2a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
@@ -93,4 +93,9 @@
assertThat(BroadcastPermissions.getPermissionLevel(mMockContext, UID, PID))
.isEqualTo(PERMISSION_BLUETOOTH_ADVERTISE);
}
+
+ @Test
+ public void test_enforceBroadcastPermission() {
+ BroadcastPermissions.enforceBroadcastPermission(mMockContext, mCallerIdentity);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
new file mode 100644
index 0000000..f0294fc
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class CryptorImpIdentityV1Test {
+ private static final String TAG = "CryptorImpIdentityV1Test";
+ private static final byte[] SALT = new byte[] {102, 22};
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_encrypt_decrypt() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_encryption() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ // for debugging
+ Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_decryption() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] decryptedData =
+ identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+ // for debugging
+ Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void generateHmacTag() {
+ CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] generatedTag = identityCryptor.sign(DATA);
+ byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
+ assertThat(generatedTag).isEqualTo(expectedTag);
+ }
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
new file mode 100644
index 0000000..3ca2575
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link CryptorImpV1}
+ */
+public final class CryptorImpV1Test {
+ private static final String TAG = "CryptorImpV1Test";
+ private static final byte[] SALT = new byte[] {102, 22};
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_encryption() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ // for debugging
+ Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_encryption_invalidInput() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+ }
+
+ @Test
+ public void test_decryption() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ byte[] decryptedData =
+ v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+ // for debugging
+ Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_decryption_invalidInput() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+ }
+
+ @Test
+ public void generateSign() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
+ byte[] expectedTag = new byte[]{
+ 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+ assertThat(generatedTag).isEqualTo(expectedTag);
+ }
+
+ @Test
+ public void test_verify() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] expectedTag = new byte[]{
+ 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+ }
+
+ @Test
+ public void test_generateHmacTag_sameResult() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+ assertThat(res1)
+ .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+ }
+
+ @Test
+ public void test_generateHmacTag_nullData() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+ }
+
+ @Test
+ public void test_generateHmacTag_nullKey() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+ }
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
new file mode 100644
index 0000000..ca612e3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link Cryptor}
+ */
+public final class CryptorTest {
+
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_computeHkdf() {
+ int outputSize = 16;
+ byte[] res1 = Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize);
+ byte[] res2 = Cryptor.computeHkdf(DATA,
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26},
+ outputSize);
+
+ assertThat(res1).hasLength(outputSize);
+ assertThat(res2).hasLength(outputSize);
+ assertThat(res1).isNotEqualTo(res2);
+ assertThat(res1)
+ .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+ }
+
+ @Test
+ public void test_computeHkdf_invalidInput() {
+ assertThat(Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, /* size= */ 256000))
+ .isNull();
+ assertThat(Cryptor.computeHkdf(DATA, new byte[0], /* size= */ 255))
+ .isNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
new file mode 100644
index 0000000..c29cb92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.identity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CallerIdentityTest {
+ private static final int UID = 100;
+ private static final int PID = 10002;
+ private static final String PACKAGE_NAME = "package_name";
+ private static final String ATTRIBUTION_TAG = "attribution_tag";
+
+ @Test
+ public void testToString() {
+ CallerIdentity callerIdentity =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ assertThat(callerIdentity.toString()).isEqualTo("100/package_name[attribution_tag]");
+ assertThat(callerIdentity.isSystemServer()).isFalse();
+ }
+
+ @Test
+ public void testHashCode() {
+ CallerIdentity callerIdentity =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ CallerIdentity callerIdentity1 =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ assertThat(callerIdentity.hashCode()).isEqualTo(callerIdentity1.hashCode());
+ }
+}
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
new file mode 100644
index 0000000..1f92374
--- /dev/null
+++ b/netbpfload/Android.bp
@@ -0,0 +1,51 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_binary {
+ name: "netbpfload",
+
+ defaults: ["bpf_defaults"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wthread-safety",
+ ],
+ sanitize: {
+ integer_overflow: true,
+ },
+
+ header_libs: ["bpf_headers"],
+ shared_libs: [
+ "libbase",
+ "liblog",
+ ],
+ srcs: [
+ "loader.cpp",
+ "NetBpfLoad.cpp",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ // really should be Android 14/U (34), but we cannot include binaries built
+ // against newer sdk in the apex, which still targets 30(R):
+ // module "netbpfload" variant "android_x86_apex30": should support
+ // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
+ min_sdk_version: "30",
+
+ init_rc: ["netbpfload.rc"],
+ required: ["bpfloader"],
+}
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
new file mode 100644
index 0000000..6152287
--- /dev/null
+++ b/netbpfload/NetBpfLoad.cpp
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2017-2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LOG_TAG
+#define LOG_TAG "NetBpfLoad"
+#endif
+
+#include <arpa/inet.h>
+#include <dirent.h>
+#include <elf.h>
+#include <error.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/bpf.h>
+#include <linux/unistd.h>
+#include <net/if.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <android/api-level.h>
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+#include <android-base/properties.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <log/log.h>
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "loader.h"
+
+using android::base::EndsWith;
+using android::bpf::domain;
+using std::string;
+
+bool exists(const char* const path) {
+ int v = access(path, F_OK);
+ if (!v) {
+ ALOGI("%s exists.", path);
+ return true;
+ }
+ if (errno == ENOENT) return false;
+ ALOGE("FATAL: access(%s, F_OK) -> %d [%d:%s]", path, v, errno, strerror(errno));
+ abort(); // can only hit this if permissions (likely selinux) are screwed up
+}
+
+
+const android::bpf::Location locations[] = {
+ // S+ Tethering mainline module (network_stack): tether offload
+ {
+ .dir = "/apex/com.android.tethering/etc/bpf/",
+ .prefix = "tethering/",
+ },
+ // T+ Tethering mainline module (shared with netd & system server)
+ // netutils_wrapper (for iptables xt_bpf) has access to programs
+ {
+ .dir = "/apex/com.android.tethering/etc/bpf/netd_shared/",
+ .prefix = "netd_shared/",
+ },
+ // T+ Tethering mainline module (shared with netd & system server)
+ // netutils_wrapper has no access, netd has read only access
+ {
+ .dir = "/apex/com.android.tethering/etc/bpf/netd_readonly/",
+ .prefix = "netd_readonly/",
+ },
+ // T+ Tethering mainline module (shared with system server)
+ {
+ .dir = "/apex/com.android.tethering/etc/bpf/net_shared/",
+ .prefix = "net_shared/",
+ },
+ // T+ Tethering mainline module (not shared, just network_stack)
+ {
+ .dir = "/apex/com.android.tethering/etc/bpf/net_private/",
+ .prefix = "net_private/",
+ },
+};
+
+int loadAllElfObjects(const android::bpf::Location& location) {
+ int retVal = 0;
+ DIR* dir;
+ struct dirent* ent;
+
+ if ((dir = opendir(location.dir)) != NULL) {
+ while ((ent = readdir(dir)) != NULL) {
+ string s = ent->d_name;
+ if (!EndsWith(s, ".o")) continue;
+
+ string progPath(location.dir);
+ progPath += s;
+
+ bool critical;
+ int ret = android::bpf::loadProg(progPath.c_str(), &critical, location);
+ if (ret) {
+ if (critical) retVal = ret;
+ ALOGE("Failed to load object: %s, ret: %s", progPath.c_str(), std::strerror(-ret));
+ } else {
+ ALOGI("Loaded object: %s", progPath.c_str());
+ }
+ }
+ closedir(dir);
+ }
+ return retVal;
+}
+
+int createSysFsBpfSubDir(const char* const prefix) {
+ if (*prefix) {
+ mode_t prevUmask = umask(0);
+
+ string s = "/sys/fs/bpf/";
+ s += prefix;
+
+ errno = 0;
+ int ret = mkdir(s.c_str(), S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO);
+ if (ret && errno != EEXIST) {
+ const int err = errno;
+ ALOGE("Failed to create directory: %s, ret: %s", s.c_str(), std::strerror(err));
+ return -err;
+ }
+
+ umask(prevUmask);
+ }
+ return 0;
+}
+
+// Technically 'value' doesn't need to be newline terminated, but it's best
+// to include a newline to match 'echo "value" > /proc/sys/...foo' behaviour,
+// which is usually how kernel devs test the actual sysctl interfaces.
+int writeProcSysFile(const char *filename, const char *value) {
+ android::base::unique_fd fd(open(filename, O_WRONLY | O_CLOEXEC));
+ if (fd < 0) {
+ const int err = errno;
+ ALOGE("open('%s', O_WRONLY | O_CLOEXEC) -> %s", filename, strerror(err));
+ return -err;
+ }
+ int len = strlen(value);
+ int v = write(fd, value, len);
+ if (v < 0) {
+ const int err = errno;
+ ALOGE("write('%s', '%s', %d) -> %s", filename, value, len, strerror(err));
+ return -err;
+ }
+ if (v != len) {
+ // In practice, due to us only using this for /proc/sys/... files, this can't happen.
+ ALOGE("write('%s', '%s', %d) -> short write [%d]", filename, value, len, v);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+int main(int argc, char** argv, char * const envp[]) {
+ (void)argc;
+ android::base::InitLogging(argv, &android::base::KernelLogger);
+
+ const int device_api_level = android_get_device_api_level();
+ const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
+
+ if (!android::bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ ALOGE("Android U QPR2 requires kernel 4.19.");
+ return 1;
+ }
+
+ if (android::bpf::isUserspace32bit() && android::bpf::isAtLeastKernelVersion(6, 2, 0)) {
+ /* Android 14/U should only launch on 64-bit kernels
+ * T launches on 5.10/5.15
+ * U launches on 5.15/6.1
+ * So >=5.16 implies isKernel64Bit()
+ *
+ * We thus added a test to V VTS which requires 5.16+ devices to use 64-bit kernels.
+ *
+ * Starting with Android V, which is the first to support a post 6.1 Linux Kernel,
+ * we also require 64-bit userspace.
+ *
+ * There are various known issues with 32-bit userspace talking to various
+ * kernel interfaces (especially CAP_NET_ADMIN ones) on a 64-bit kernel.
+ * Some of these have userspace or kernel workarounds/hacks.
+ * Some of them don't...
+ * We're going to be removing the hacks.
+ *
+ * Additionally the 32-bit kernel jit support is poor,
+ * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+ */
+ ALOGE("64-bit userspace required on 6.2+ kernels.");
+ return 1;
+ }
+
+ // Ensure we can determine the Android build type.
+ if (!android::bpf::isEng() && !android::bpf::isUser() && !android::bpf::isUserdebug()) {
+ ALOGE("Failed to determine the build type: got %s, want 'eng', 'user', or 'userdebug'",
+ android::bpf::getBuildType().c_str());
+ return 1;
+ }
+
+ if (isAtLeastU) {
+ // Linux 5.16-rc1 changed the default to 2 (disabled but changeable),
+ // but we need 0 (enabled)
+ // (this writeFile is known to fail on at least 4.19, but always defaults to 0 on
+ // pre-5.13, on 5.13+ it depends on CONFIG_BPF_UNPRIV_DEFAULT_OFF)
+ if (writeProcSysFile("/proc/sys/kernel/unprivileged_bpf_disabled", "0\n") &&
+ android::bpf::isAtLeastKernelVersion(5, 13, 0)) return 1;
+
+ // Enable the eBPF JIT -- but do note that on 64-bit kernels it is likely
+ // already force enabled by the kernel config option BPF_JIT_ALWAYS_ON.
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_BPF_JIT=y)
+ // BPF_JIT is required by R VINTF (which means 4.14/4.19/5.4 kernels),
+ // but 4.14/4.19 were released with P & Q, and only 5.4 is new in R+.
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_enable", "1\n")) return 1;
+
+ // Enable JIT kallsyms export for privileged users only
+ // (Note: this (open) will fail with ENOENT 'No such file or directory' if
+ // kernel does not have CONFIG_HAVE_EBPF_JIT=y)
+ if (writeProcSysFile("/proc/sys/net/core/bpf_jit_kallsyms", "1\n")) return 1;
+ }
+
+ // Create all the pin subdirectories
+ // (this must be done first to allow selinux_context and pin_subdir functionality,
+ // which could otherwise fail with ENOENT during object pinning or renaming,
+ // due to ordering issues)
+ for (const auto& location : locations) {
+ if (createSysFsBpfSubDir(location.prefix)) return 1;
+ }
+
+ // Load all ELF objects, create programs and maps, and pin them
+ for (const auto& location : locations) {
+ if (loadAllElfObjects(location) != 0) {
+ ALOGE("=== CRITICAL FAILURE LOADING BPF PROGRAMS FROM %s ===", location.dir);
+ ALOGE("If this triggers reliably, you're probably missing kernel options or patches.");
+ ALOGE("If this triggers randomly, you might be hitting some memory allocation "
+ "problems or startup script race.");
+ ALOGE("--- DO NOT EXPECT SYSTEM TO BOOT SUCCESSFULLY ---");
+ sleep(20);
+ return 2;
+ }
+ }
+
+ int key = 1;
+ int value = 123;
+ android::base::unique_fd map(
+ android::bpf::createMap(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 2, 0));
+ if (android::bpf::writeToMapEntry(map, &key, &value, BPF_ANY)) {
+ ALOGE("Critical kernel bug - failure to write into index 1 of 2 element bpf map array.");
+ return 1;
+ }
+
+ ALOGI("done, transferring control to platform bpfloader.");
+
+ const char * args[] = { "/system/bin/bpfloader", NULL, };
+ if (execve(args[0], (char**)args, envp)) {
+ ALOGE("FATAL: execve('/system/bin/bpfloader'): %d[%s]", errno, strerror(errno));
+ }
+
+ return 1;
+}
diff --git a/netbpfload/initrc-doc/README.txt b/netbpfload/initrc-doc/README.txt
new file mode 100644
index 0000000..42e1fc2
--- /dev/null
+++ b/netbpfload/initrc-doc/README.txt
@@ -0,0 +1,62 @@
+This directory contains comment stripped versions of
+ //system/bpf/bpfloader/bpfloader.rc
+from previous versions of Android.
+
+Generated via:
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+ (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+
+this is entirely equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+
+it is also equivalent to:
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-v2-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+ (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+
+ie. there were no changes between R/S/T and R/S/T QPR3, and no change between U and U QPR1.
+
+Note: Sv2 sdk/api level is actually 32, it just didn't change anything wrt. bpf, so doesn't matter.
+
+
+Key takeaways:
+
+= R bpfloader:
+ - CHOWN + SYS_ADMIN
+ - asynchronous startup
+ - platform only
+ - proc file setup handled by initrc
+
+= S bpfloader
+ - adds NET_ADMIN
+ - synchronous startup
+ - platform + mainline tethering offload
+
+= T bpfloader
+ - platform + mainline networking (including tethering offload)
+ - supported btf for maps via exec of btfloader
+
+= U bpfloader
+ - proc file setup moved into bpfloader binary
+ - explicitly specified user and groups:
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+
+= U QPR2 bpfloader
+ - drops support of btf for maps
+ - invocation of /system/bin/netbpfload binary, which after handling *all*
+ networking bpf related things executes the platform /system/bin/bpfloader
+ which handles non-networking bpf.
+
+Note that there is now a copy of 'netbpfload' provided by the tethering apex
+mainline module at /apex/com.android.tethering/bin/netbpfload, which due
+to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
+added for btf map support (specifically the ability to exec the "btfloader").
diff --git a/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc b/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
new file mode 100644
index 0000000..482a7db
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk30-11-R.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ write /proc/sys/net/core/bpf_jit_enable 1
+ write /proc/sys/net/core/bpf_jit_kallsyms 1
+ start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc b/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
new file mode 100644
index 0000000..4117887
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk31-12-S.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ write /proc/sys/net/core/bpf_jit_enable 1
+ write /proc/sys/net/core/bpf_jit_kallsyms 1
+ exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc b/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
new file mode 100644
index 0000000..f0b6700
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk33-13-T.rc
@@ -0,0 +1,12 @@
+on load_bpf_programs
+ write /proc/sys/kernel/unprivileged_bpf_disabled 0
+ write /proc/sys/net/core/bpf_jit_enable 1
+ write /proc/sys/net/core/bpf_jit_kallsyms 1
+ exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
new file mode 100644
index 0000000..8f3f462
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/netbpfload
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc b/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
new file mode 100644
index 0000000..592303e
--- /dev/null
+++ b/netbpfload/initrc-doc/bpfloader-sdk34-14-U.rc
@@ -0,0 +1,11 @@
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ reboot_on_failure reboot,bpfloader-failed
+ updatable
diff --git a/netbpfload/loader.cpp b/netbpfload/loader.cpp
new file mode 100644
index 0000000..c534b2c
--- /dev/null
+++ b/netbpfload/loader.cpp
@@ -0,0 +1,1185 @@
+/*
+ * Copyright (C) 2018-2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "NetBpfLoader"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/bpf.h>
+#include <linux/elf.h>
+#include <log/log.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <sys/stat.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+// This is BpfLoader v0.41
+// WARNING: If you ever hit cherrypick conflicts here you're doing it wrong:
+// You are NOT allowed to cherrypick bpfloader related patches out of order.
+// (indeed: cherrypicking is probably a bad idea and you should merge instead)
+// Mainline supports ONLY the published versions of the bpfloader for each Android release.
+#define BPFLOADER_VERSION_MAJOR 0u
+#define BPFLOADER_VERSION_MINOR 41u
+#define BPFLOADER_VERSION ((BPFLOADER_VERSION_MAJOR << 16) | BPFLOADER_VERSION_MINOR)
+
+#include "BpfSyscallWrappers.h"
+#include "bpf/BpfUtils.h"
+#include "bpf/bpf_map_def.h"
+#include "loader.h"
+
+#if BPFLOADER_VERSION < COMPILE_FOR_BPFLOADER_VERSION
+#error "BPFLOADER_VERSION is less than COMPILE_FOR_BPFLOADER_VERSION"
+#endif
+
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <android-base/cmsg.h>
+#include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+
+#define BPF_FS_PATH "/sys/fs/bpf/"
+
+// Size of the BPF log buffer for verifier logging
+#define BPF_LOAD_LOG_SZ 0xfffff
+
+// Unspecified attach type is 0 which is BPF_CGROUP_INET_INGRESS.
+#define BPF_ATTACH_TYPE_UNSPEC BPF_CGROUP_INET_INGRESS
+
+using android::base::StartsWith;
+using android::base::unique_fd;
+using std::ifstream;
+using std::ios;
+using std::optional;
+using std::string;
+using std::vector;
+
+namespace android {
+namespace bpf {
+
+const std::string& getBuildType() {
+ static std::string t = android::base::GetProperty("ro.build.type", "unknown");
+ return t;
+}
+
+static unsigned int page_size = static_cast<unsigned int>(getpagesize());
+
+constexpr const char* lookupSelinuxContext(const domain d, const char* const unspecified = "") {
+ switch (d) {
+ case domain::unspecified: return unspecified;
+ case domain::tethering: return "fs_bpf_tethering";
+ case domain::net_private: return "fs_bpf_net_private";
+ case domain::net_shared: return "fs_bpf_net_shared";
+ case domain::netd_readonly: return "fs_bpf_netd_readonly";
+ case domain::netd_shared: return "fs_bpf_netd_shared";
+ default: return "(unrecognized)";
+ }
+}
+
+domain getDomainFromSelinuxContext(const char s[BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE]) {
+ for (domain d : AllDomains) {
+ // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+ if (strlen(lookupSelinuxContext(d)) >= BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE) abort();
+ if (!strncmp(s, lookupSelinuxContext(d), BPF_SELINUX_CONTEXT_CHAR_ARRAY_SIZE)) return d;
+ }
+ ALOGW("ignoring unrecognized selinux_context '%-32s'", s);
+ // We should return 'unrecognized' here, however: returning unspecified will
+ // result in the system simply using the default context, which in turn
+ // will allow future expansion by adding more restrictive selinux types.
+ // Older bpfloader will simply ignore that, and use the less restrictive default.
+ // This does mean you CANNOT later add a *less* restrictive type than the default.
+ //
+ // Note: we cannot just abort() here as this might be a mainline module shipped optional update
+ return domain::unspecified;
+}
+
+constexpr const char* lookupPinSubdir(const domain d, const char* const unspecified = "") {
+ switch (d) {
+ case domain::unspecified: return unspecified;
+ case domain::tethering: return "tethering/";
+ case domain::net_private: return "net_private/";
+ case domain::net_shared: return "net_shared/";
+ case domain::netd_readonly: return "netd_readonly/";
+ case domain::netd_shared: return "netd_shared/";
+ default: return "(unrecognized)";
+ }
+};
+
+domain getDomainFromPinSubdir(const char s[BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE]) {
+ for (domain d : AllDomains) {
+ // Not sure how to enforce this at compile time, so abort() bpfloader at boot instead
+ if (strlen(lookupPinSubdir(d)) >= BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE) abort();
+ if (!strncmp(s, lookupPinSubdir(d), BPF_PIN_SUBDIR_CHAR_ARRAY_SIZE)) return d;
+ }
+ ALOGE("unrecognized pin_subdir '%-32s'", s);
+ // pin_subdir affects the object's full pathname,
+ // and thus using the default would change the location and thus our code's ability to find it,
+ // hence this seems worth treating as a true error condition.
+ //
+ // Note: we cannot just abort() here as this might be a mainline module shipped optional update
+ // However, our callers will treat this as an error, and stop loading the specific .o,
+ // which will fail bpfloader if the .o is marked critical.
+ return domain::unrecognized;
+}
+
+static string pathToObjName(const string& path) {
+ // extract everything after the final slash, ie. this is the filename 'foo@1.o' or 'bar.o'
+ string filename = android::base::Split(path, "/").back();
+ // strip off everything from the final period onwards (strip '.o' suffix), ie. 'foo@1' or 'bar'
+ string name = filename.substr(0, filename.find_last_of('.'));
+ // strip any potential @1 suffix, this will leave us with just 'foo' or 'bar'
+ // this can be used to provide duplicate programs (mux based on the bpfloader version)
+ return name.substr(0, name.find_last_of('@'));
+}
+
+typedef struct {
+ const char* name;
+ enum bpf_prog_type type;
+ enum bpf_attach_type expected_attach_type;
+} sectionType;
+
+/*
+ * Map section name prefixes to program types, the section name will be:
+ * SECTION(<prefix>/<name-of-program>)
+ * For example:
+ * SECTION("tracepoint/sched_switch_func") where sched_switch_funcs
+ * is the name of the program, and tracepoint is the type.
+ *
+ * However, be aware that you should not be directly using the SECTION() macro.
+ * Instead use the DEFINE_(BPF|XDP)_(PROG|MAP)... & LICENSE/CRITICAL macros.
+ *
+ * Programs shipped inside the tethering apex should be limited to networking stuff,
+ * as KPROBE, PERF_EVENT, TRACEPOINT are dangerous to use from mainline updatable code,
+ * since they are less stable abi/api and may conflict with platform uses of bpf.
+ */
+sectionType sectionNameTypes[] = {
+ {"bind4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_BIND},
+ {"bind6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_BIND},
+ {"cgroupskb/", BPF_PROG_TYPE_CGROUP_SKB, BPF_ATTACH_TYPE_UNSPEC},
+ {"cgroupsock/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_ATTACH_TYPE_UNSPEC},
+ {"connect4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET4_CONNECT},
+ {"connect6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_INET6_CONNECT},
+ {"egress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS},
+ {"getsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_GETSOCKOPT},
+ {"ingress/", BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS},
+ {"lwt_in/", BPF_PROG_TYPE_LWT_IN, BPF_ATTACH_TYPE_UNSPEC},
+ {"lwt_out/", BPF_PROG_TYPE_LWT_OUT, BPF_ATTACH_TYPE_UNSPEC},
+ {"lwt_seg6local/", BPF_PROG_TYPE_LWT_SEG6LOCAL, BPF_ATTACH_TYPE_UNSPEC},
+ {"lwt_xmit/", BPF_PROG_TYPE_LWT_XMIT, BPF_ATTACH_TYPE_UNSPEC},
+ {"postbind4/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET4_POST_BIND},
+ {"postbind6/", BPF_PROG_TYPE_CGROUP_SOCK, BPF_CGROUP_INET6_POST_BIND},
+ {"recvmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_RECVMSG},
+ {"recvmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_RECVMSG},
+ {"schedact/", BPF_PROG_TYPE_SCHED_ACT, BPF_ATTACH_TYPE_UNSPEC},
+ {"schedcls/", BPF_PROG_TYPE_SCHED_CLS, BPF_ATTACH_TYPE_UNSPEC},
+ {"sendmsg4/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP4_SENDMSG},
+ {"sendmsg6/", BPF_PROG_TYPE_CGROUP_SOCK_ADDR, BPF_CGROUP_UDP6_SENDMSG},
+ {"setsockopt/", BPF_PROG_TYPE_CGROUP_SOCKOPT, BPF_CGROUP_SETSOCKOPT},
+ {"skfilter/", BPF_PROG_TYPE_SOCKET_FILTER, BPF_ATTACH_TYPE_UNSPEC},
+ {"sockops/", BPF_PROG_TYPE_SOCK_OPS, BPF_CGROUP_SOCK_OPS},
+ {"sysctl", BPF_PROG_TYPE_CGROUP_SYSCTL, BPF_CGROUP_SYSCTL},
+ {"xdp/", BPF_PROG_TYPE_XDP, BPF_ATTACH_TYPE_UNSPEC},
+};
+
+typedef struct {
+ enum bpf_prog_type type;
+ enum bpf_attach_type expected_attach_type;
+ string name;
+ vector<char> data;
+ vector<char> rel_data;
+ optional<struct bpf_prog_def> prog_def;
+
+ unique_fd prog_fd; /* fd after loading */
+} codeSection;
+
+static int readElfHeader(ifstream& elfFile, Elf64_Ehdr* eh) {
+ elfFile.seekg(0);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)eh, sizeof(*eh))) return -1;
+
+ return 0;
+}
+
+/* Reads all section header tables into an Shdr array */
+static int readSectionHeadersAll(ifstream& elfFile, vector<Elf64_Shdr>& shTable) {
+ Elf64_Ehdr eh;
+ int ret = 0;
+
+ ret = readElfHeader(elfFile, &eh);
+ if (ret) return ret;
+
+ elfFile.seekg(eh.e_shoff);
+ if (elfFile.fail()) return -1;
+
+ /* Read shdr table entries */
+ shTable.resize(eh.e_shnum);
+
+ if (!elfFile.read((char*)shTable.data(), (eh.e_shnum * eh.e_shentsize))) return -ENOMEM;
+
+ return 0;
+}
+
+/* Read a section by its index - for ex to get sec hdr strtab blob */
+static int readSectionByIdx(ifstream& elfFile, int id, vector<char>& sec) {
+ vector<Elf64_Shdr> shTable;
+ int ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ elfFile.seekg(shTable[id].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ sec.resize(shTable[id].sh_size);
+ if (!elfFile.read(sec.data(), shTable[id].sh_size)) return -1;
+
+ return 0;
+}
+
+/* Read whole section header string table */
+static int readSectionHeaderStrtab(ifstream& elfFile, vector<char>& strtab) {
+ Elf64_Ehdr eh;
+ int ret = readElfHeader(elfFile, &eh);
+ if (ret) return ret;
+
+ ret = readSectionByIdx(elfFile, eh.e_shstrndx, strtab);
+ if (ret) return ret;
+
+ return 0;
+}
+
+/* Get name from offset in strtab */
+static int getSymName(ifstream& elfFile, int nameOff, string& name) {
+ int ret;
+ vector<char> secStrTab;
+
+ ret = readSectionHeaderStrtab(elfFile, secStrTab);
+ if (ret) return ret;
+
+ if (nameOff >= (int)secStrTab.size()) return -1;
+
+ name = string((char*)secStrTab.data() + nameOff);
+ return 0;
+}
+
+/* Reads a full section by name - example to get the GPL license */
+static int readSectionByName(const char* name, ifstream& elfFile, vector<char>& data) {
+ vector<char> secStrTab;
+ vector<Elf64_Shdr> shTable;
+ int ret;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ ret = readSectionHeaderStrtab(elfFile, secStrTab);
+ if (ret) return ret;
+
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ char* secname = secStrTab.data() + shTable[i].sh_name;
+ if (!secname) continue;
+
+ if (!strcmp(secname, name)) {
+ vector<char> dataTmp;
+ dataTmp.resize(shTable[i].sh_size);
+
+ elfFile.seekg(shTable[i].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+ data = dataTmp;
+ return 0;
+ }
+ }
+ return -2;
+}
+
+unsigned int readSectionUint(const char* name, ifstream& elfFile, unsigned int defVal) {
+ vector<char> theBytes;
+ int ret = readSectionByName(name, elfFile, theBytes);
+ if (ret) {
+ ALOGD("Couldn't find section %s (defaulting to %u [0x%x]).", name, defVal, defVal);
+ return defVal;
+ } else if (theBytes.size() < sizeof(unsigned int)) {
+ ALOGE("Section %s too short (defaulting to %u [0x%x]).", name, defVal, defVal);
+ return defVal;
+ } else {
+ // decode first 4 bytes as LE32 uint, there will likely be more bytes due to alignment.
+ unsigned int value = static_cast<unsigned char>(theBytes[3]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[2]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[1]);
+ value <<= 8;
+ value += static_cast<unsigned char>(theBytes[0]);
+ ALOGI("Section %s value is %u [0x%x]", name, value, value);
+ return value;
+ }
+}
+
+static int readSectionByType(ifstream& elfFile, int type, vector<char>& data) {
+ int ret;
+ vector<Elf64_Shdr> shTable;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ if ((int)shTable[i].sh_type != type) continue;
+
+ vector<char> dataTmp;
+ dataTmp.resize(shTable[i].sh_size);
+
+ elfFile.seekg(shTable[i].sh_offset);
+ if (elfFile.fail()) return -1;
+
+ if (!elfFile.read((char*)dataTmp.data(), shTable[i].sh_size)) return -1;
+
+ data = dataTmp;
+ return 0;
+ }
+ return -2;
+}
+
+static bool symCompare(Elf64_Sym a, Elf64_Sym b) {
+ return (a.st_value < b.st_value);
+}
+
+static int readSymTab(ifstream& elfFile, int sort, vector<Elf64_Sym>& data) {
+ int ret, numElems;
+ Elf64_Sym* buf;
+ vector<char> secData;
+
+ ret = readSectionByType(elfFile, SHT_SYMTAB, secData);
+ if (ret) return ret;
+
+ buf = (Elf64_Sym*)secData.data();
+ numElems = (secData.size() / sizeof(Elf64_Sym));
+ data.assign(buf, buf + numElems);
+
+ if (sort) std::sort(data.begin(), data.end(), symCompare);
+ return 0;
+}
+
+static enum bpf_prog_type getSectionType(string& name) {
+ for (auto& snt : sectionNameTypes)
+ if (StartsWith(name, snt.name)) return snt.type;
+
+ return BPF_PROG_TYPE_UNSPEC;
+}
+
+static enum bpf_attach_type getExpectedAttachType(string& name) {
+ for (auto& snt : sectionNameTypes)
+ if (StartsWith(name, snt.name)) return snt.expected_attach_type;
+ return BPF_ATTACH_TYPE_UNSPEC;
+}
+
+/*
+static string getSectionName(enum bpf_prog_type type)
+{
+ for (auto& snt : sectionNameTypes)
+ if (snt.type == type)
+ return string(snt.name);
+
+ return "UNKNOWN SECTION NAME " + std::to_string(type);
+}
+*/
+
+static int readProgDefs(ifstream& elfFile, vector<struct bpf_prog_def>& pd,
+ size_t sizeOfBpfProgDef) {
+ vector<char> pdData;
+ int ret = readSectionByName("progs", elfFile, pdData);
+ // Older file formats do not require a 'progs' section at all.
+ // (We should probably figure out whether this is behaviour which is safe to remove now.)
+ if (ret == -2) return 0;
+ if (ret) return ret;
+
+ if (pdData.size() % sizeOfBpfProgDef) {
+ ALOGE("readProgDefs failed due to improper sized progs section, %zu %% %zu != 0",
+ pdData.size(), sizeOfBpfProgDef);
+ return -1;
+ };
+
+ int progCount = pdData.size() / sizeOfBpfProgDef;
+ pd.resize(progCount);
+ size_t trimmedSize = std::min(sizeOfBpfProgDef, sizeof(struct bpf_prog_def));
+
+ const char* dataPtr = pdData.data();
+ for (auto& p : pd) {
+ // First we zero initialize
+ memset(&p, 0, sizeof(p));
+ // Then we set non-zero defaults
+ p.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER; // v1.0
+ // Then we copy over the structure prefix from the ELF file.
+ memcpy(&p, dataPtr, trimmedSize);
+ // Move to next struct in the ELF file
+ dataPtr += sizeOfBpfProgDef;
+ }
+ return 0;
+}
+
+static int getSectionSymNames(ifstream& elfFile, const string& sectionName, vector<string>& names,
+ optional<unsigned> symbolType = std::nullopt) {
+ int ret;
+ string name;
+ vector<Elf64_Sym> symtab;
+ vector<Elf64_Shdr> shTable;
+
+ ret = readSymTab(elfFile, 1 /* sort */, symtab);
+ if (ret) return ret;
+
+ /* Get index of section */
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+
+ int sec_idx = -1;
+ for (int i = 0; i < (int)shTable.size(); i++) {
+ ret = getSymName(elfFile, shTable[i].sh_name, name);
+ if (ret) return ret;
+
+ if (!name.compare(sectionName)) {
+ sec_idx = i;
+ break;
+ }
+ }
+
+ /* No section found with matching name*/
+ if (sec_idx == -1) {
+ ALOGW("No %s section could be found in elf object", sectionName.c_str());
+ return -1;
+ }
+
+ for (int i = 0; i < (int)symtab.size(); i++) {
+ if (symbolType.has_value() && ELF_ST_TYPE(symtab[i].st_info) != symbolType) continue;
+
+ if (symtab[i].st_shndx == sec_idx) {
+ string s;
+ ret = getSymName(elfFile, symtab[i].st_name, s);
+ if (ret) return ret;
+ names.push_back(s);
+ }
+ }
+
+ return 0;
+}
+
+/* Read a section by its index - for ex to get sec hdr strtab blob */
+static int readCodeSections(ifstream& elfFile, vector<codeSection>& cs, size_t sizeOfBpfProgDef) {
+ vector<Elf64_Shdr> shTable;
+ int entries, ret = 0;
+
+ ret = readSectionHeadersAll(elfFile, shTable);
+ if (ret) return ret;
+ entries = shTable.size();
+
+ vector<struct bpf_prog_def> pd;
+ ret = readProgDefs(elfFile, pd, sizeOfBpfProgDef);
+ if (ret) return ret;
+ vector<string> progDefNames;
+ ret = getSectionSymNames(elfFile, "progs", progDefNames);
+ if (!pd.empty() && ret) return ret;
+
+ for (int i = 0; i < entries; i++) {
+ string name;
+ codeSection cs_temp;
+ cs_temp.type = BPF_PROG_TYPE_UNSPEC;
+
+ ret = getSymName(elfFile, shTable[i].sh_name, name);
+ if (ret) return ret;
+
+ enum bpf_prog_type ptype = getSectionType(name);
+
+ if (ptype == BPF_PROG_TYPE_UNSPEC) continue;
+
+ // This must be done before '/' is replaced with '_'.
+ cs_temp.expected_attach_type = getExpectedAttachType(name);
+
+ string oldName = name;
+
+ // convert all slashes to underscores
+ std::replace(name.begin(), name.end(), '/', '_');
+
+ cs_temp.type = ptype;
+ cs_temp.name = name;
+
+ ret = readSectionByIdx(elfFile, i, cs_temp.data);
+ if (ret) return ret;
+ ALOGD("Loaded code section %d (%s)", i, name.c_str());
+
+ vector<string> csSymNames;
+ ret = getSectionSymNames(elfFile, oldName, csSymNames, STT_FUNC);
+ if (ret || !csSymNames.size()) return ret;
+ for (size_t i = 0; i < progDefNames.size(); ++i) {
+ if (!progDefNames[i].compare(csSymNames[0] + "_def")) {
+ cs_temp.prog_def = pd[i];
+ break;
+ }
+ }
+
+ /* Check for rel section */
+ if (cs_temp.data.size() > 0 && i < entries) {
+ ret = getSymName(elfFile, shTable[i + 1].sh_name, name);
+ if (ret) return ret;
+
+ if (name == (".rel" + oldName)) {
+ ret = readSectionByIdx(elfFile, i + 1, cs_temp.rel_data);
+ if (ret) return ret;
+ ALOGD("Loaded relo section %d (%s)", i, name.c_str());
+ }
+ }
+
+ if (cs_temp.data.size() > 0) {
+ cs.push_back(std::move(cs_temp));
+ ALOGD("Adding section %d to cs list", i);
+ }
+ }
+ return 0;
+}
+
+static int getSymNameByIdx(ifstream& elfFile, int index, string& name) {
+ vector<Elf64_Sym> symtab;
+ int ret = 0;
+
+ ret = readSymTab(elfFile, 0 /* !sort */, symtab);
+ if (ret) return ret;
+
+ if (index >= (int)symtab.size()) return -1;
+
+ return getSymName(elfFile, symtab[index].st_name, name);
+}
+
+static bool mapMatchesExpectations(const unique_fd& fd, const string& mapName,
+ const struct bpf_map_def& mapDef, const enum bpf_map_type type) {
+ // Assuming fd is a valid Bpf Map file descriptor then
+ // all the following should always succeed on a 4.14+ kernel.
+ // If they somehow do fail, they'll return -1 (and set errno),
+ // which should then cause (among others) a key_size mismatch.
+ int fd_type = bpfGetFdMapType(fd);
+ int fd_key_size = bpfGetFdKeySize(fd);
+ int fd_value_size = bpfGetFdValueSize(fd);
+ int fd_max_entries = bpfGetFdMaxEntries(fd);
+ int fd_map_flags = bpfGetFdMapFlags(fd);
+
+ // DEVMAPs are readonly from the bpf program side's point of view, as such
+ // the kernel in kernel/bpf/devmap.c dev_map_init_map() will set the flag
+ int desired_map_flags = (int)mapDef.map_flags;
+ if (type == BPF_MAP_TYPE_DEVMAP || type == BPF_MAP_TYPE_DEVMAP_HASH)
+ desired_map_flags |= BPF_F_RDONLY_PROG;
+
+ // The .h file enforces that this is a power of two, and page size will
+ // also always be a power of two, so this logic is actually enough to
+ // force it to be a multiple of the page size, as required by the kernel.
+ unsigned int desired_max_entries = mapDef.max_entries;
+ if (type == BPF_MAP_TYPE_RINGBUF) {
+ if (desired_max_entries < page_size) desired_max_entries = page_size;
+ }
+
+ // The following checks should *never* trigger, if one of them somehow does,
+ // it probably means a bpf .o file has been changed/replaced at runtime
+ // and bpfloader was manually rerun (normally it should only run *once*
+ // early during the boot process).
+ // Another possibility is that something is misconfigured in the code:
+ // most likely a shared map is declared twice differently.
+ // But such a change should never be checked into the source tree...
+ if ((fd_type == type) &&
+ (fd_key_size == (int)mapDef.key_size) &&
+ (fd_value_size == (int)mapDef.value_size) &&
+ (fd_max_entries == (int)desired_max_entries) &&
+ (fd_map_flags == desired_map_flags)) {
+ return true;
+ }
+
+ ALOGE("bpf map name %s mismatch: desired/found: "
+ "type:%d/%d key:%u/%d value:%u/%d entries:%u/%d flags:%u/%d",
+ mapName.c_str(), type, fd_type, mapDef.key_size, fd_key_size, mapDef.value_size,
+ fd_value_size, mapDef.max_entries, fd_max_entries, desired_map_flags, fd_map_flags);
+ return false;
+}
+
+static int createMaps(const char* elfPath, ifstream& elfFile, vector<unique_fd>& mapFds,
+ const char* prefix, const size_t sizeOfBpfMapDef) {
+ int ret;
+ vector<char> mdData;
+ vector<struct bpf_map_def> md;
+ vector<string> mapNames;
+ string objName = pathToObjName(string(elfPath));
+
+ ret = readSectionByName("maps", elfFile, mdData);
+ if (ret == -2) return 0; // no maps to read
+ if (ret) return ret;
+
+ if (mdData.size() % sizeOfBpfMapDef) {
+ ALOGE("createMaps failed due to improper sized maps section, %zu %% %zu != 0",
+ mdData.size(), sizeOfBpfMapDef);
+ return -1;
+ };
+
+ int mapCount = mdData.size() / sizeOfBpfMapDef;
+ md.resize(mapCount);
+ size_t trimmedSize = std::min(sizeOfBpfMapDef, sizeof(struct bpf_map_def));
+
+ const char* dataPtr = mdData.data();
+ for (auto& m : md) {
+ // First we zero initialize
+ memset(&m, 0, sizeof(m));
+ // Then we set non-zero defaults
+ m.bpfloader_max_ver = DEFAULT_BPFLOADER_MAX_VER; // v1.0
+ m.max_kver = 0xFFFFFFFFu; // matches KVER_INF from bpf_helpers.h
+ // Then we copy over the structure prefix from the ELF file.
+ memcpy(&m, dataPtr, trimmedSize);
+ // Move to next struct in the ELF file
+ dataPtr += sizeOfBpfMapDef;
+ }
+
+ ret = getSectionSymNames(elfFile, "maps", mapNames);
+ if (ret) return ret;
+
+ unsigned kvers = kernelVersion();
+
+ for (int i = 0; i < (int)mapNames.size(); i++) {
+ if (md[i].zero != 0) abort();
+
+ if (BPFLOADER_VERSION < md[i].bpfloader_min_ver) {
+ ALOGI("skipping map %s which requires bpfloader min ver 0x%05x", mapNames[i].c_str(),
+ md[i].bpfloader_min_ver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (BPFLOADER_VERSION >= md[i].bpfloader_max_ver) {
+ ALOGI("skipping map %s which requires bpfloader max ver 0x%05x", mapNames[i].c_str(),
+ md[i].bpfloader_max_ver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (kvers < md[i].min_kver) {
+ ALOGI("skipping map %s which requires kernel version 0x%x >= 0x%x",
+ mapNames[i].c_str(), kvers, md[i].min_kver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if (kvers >= md[i].max_kver) {
+ ALOGI("skipping map %s which requires kernel version 0x%x < 0x%x",
+ mapNames[i].c_str(), kvers, md[i].max_kver);
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if ((md[i].ignore_on_eng && isEng()) || (md[i].ignore_on_user && isUser()) ||
+ (md[i].ignore_on_userdebug && isUserdebug())) {
+ ALOGI("skipping map %s which is ignored on %s builds", mapNames[i].c_str(),
+ getBuildType().c_str());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ if ((isArm() && isKernel32Bit() && md[i].ignore_on_arm32) ||
+ (isArm() && isKernel64Bit() && md[i].ignore_on_aarch64) ||
+ (isX86() && isKernel32Bit() && md[i].ignore_on_x86_32) ||
+ (isX86() && isKernel64Bit() && md[i].ignore_on_x86_64) ||
+ (isRiscV() && md[i].ignore_on_riscv64)) {
+ ALOGI("skipping map %s which is ignored on %s", mapNames[i].c_str(),
+ describeArch());
+ mapFds.push_back(unique_fd());
+ continue;
+ }
+
+ enum bpf_map_type type = md[i].type;
+ if (type == BPF_MAP_TYPE_DEVMAP_HASH && !isAtLeastKernelVersion(5, 4, 0)) {
+ // On Linux Kernels older than 5.4 this map type doesn't exist, but it can kind
+ // of be approximated: HASH has the same userspace visible api.
+ // However it cannot be used by ebpf programs in the same way.
+ // Since bpf_redirect_map() only requires 4.14, a program using a DEVMAP_HASH map
+ // would fail to load (due to trying to redirect to a HASH instead of DEVMAP_HASH).
+ // One must thus tag any BPF_MAP_TYPE_DEVMAP_HASH + bpf_redirect_map() using
+ // programs as being 5.4+...
+ type = BPF_MAP_TYPE_HASH;
+ }
+
+ // The .h file enforces that this is a power of two, and page size will
+ // also always be a power of two, so this logic is actually enough to
+ // force it to be a multiple of the page size, as required by the kernel.
+ unsigned int max_entries = md[i].max_entries;
+ if (type == BPF_MAP_TYPE_RINGBUF) {
+ if (max_entries < page_size) max_entries = page_size;
+ }
+
+ domain selinux_context = getDomainFromSelinuxContext(md[i].selinux_context);
+ if (specified(selinux_context)) {
+ ALOGI("map %s selinux_context [%-32s] -> %d -> '%s' (%s)", mapNames[i].c_str(),
+ md[i].selinux_context, selinux_context, lookupSelinuxContext(selinux_context),
+ lookupPinSubdir(selinux_context));
+ }
+
+ domain pin_subdir = getDomainFromPinSubdir(md[i].pin_subdir);
+ if (unrecognized(pin_subdir)) return -ENOTDIR;
+ if (specified(pin_subdir)) {
+ ALOGI("map %s pin_subdir [%-32s] -> %d -> '%s'", mapNames[i].c_str(), md[i].pin_subdir,
+ pin_subdir, lookupPinSubdir(pin_subdir));
+ }
+
+ // Format of pin location is /sys/fs/bpf/<pin_subdir|prefix>map_<objName>_<mapName>
+ // except that maps shared across .o's have empty <objName>
+ // Note: <objName> refers to the extension-less basename of the .o file (without @ suffix).
+ string mapPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "map_" +
+ (md[i].shared ? "" : objName) + "_" + mapNames[i];
+ bool reuse = false;
+ unique_fd fd;
+ int saved_errno;
+
+ if (access(mapPinLoc.c_str(), F_OK) == 0) {
+ fd.reset(mapRetrieveRO(mapPinLoc.c_str()));
+ saved_errno = errno;
+ ALOGD("bpf_create_map reusing map %s, ret: %d", mapNames[i].c_str(), fd.get());
+ reuse = true;
+ } else {
+ union bpf_attr req = {
+ .map_type = type,
+ .key_size = md[i].key_size,
+ .value_size = md[i].value_size,
+ .max_entries = max_entries,
+ .map_flags = md[i].map_flags,
+ };
+ strlcpy(req.map_name, mapNames[i].c_str(), sizeof(req.map_name));
+ fd.reset(bpf(BPF_MAP_CREATE, req));
+ saved_errno = errno;
+ ALOGD("bpf_create_map name %s, ret: %d", mapNames[i].c_str(), fd.get());
+ }
+
+ if (!fd.ok()) return -saved_errno;
+
+ // When reusing a pinned map, we need to check the map type/sizes/etc match, but for
+ // safety (since reuse code path is rare) run these checks even if we just created it.
+ // We assume failure is due to pinned map mismatch, hence the 'NOT UNIQUE' return code.
+ if (!mapMatchesExpectations(fd, mapNames[i], md[i], type)) return -ENOTUNIQ;
+
+ if (!reuse) {
+ if (specified(selinux_context)) {
+ string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+ "tmp_map_" + objName + "_" + mapNames[i];
+ ret = bpfFdPin(fd, createLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ ret = renameat2(AT_FDCWD, createLoc.c_str(),
+ AT_FDCWD, mapPinLoc.c_str(), RENAME_NOREPLACE);
+ if (ret) {
+ int err = errno;
+ ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), mapPinLoc.c_str(), ret,
+ err, strerror(err));
+ return -err;
+ }
+ } else {
+ ret = bpfFdPin(fd, mapPinLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("pin %s -> %d [%d:%s]", mapPinLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ }
+ ret = chmod(mapPinLoc.c_str(), md[i].mode);
+ if (ret) {
+ int err = errno;
+ ALOGE("chmod(%s, 0%o) = %d [%d:%s]", mapPinLoc.c_str(), md[i].mode, ret, err,
+ strerror(err));
+ return -err;
+ }
+ ret = chown(mapPinLoc.c_str(), (uid_t)md[i].uid, (gid_t)md[i].gid);
+ if (ret) {
+ int err = errno;
+ ALOGE("chown(%s, %u, %u) = %d [%d:%s]", mapPinLoc.c_str(), md[i].uid, md[i].gid,
+ ret, err, strerror(err));
+ return -err;
+ }
+ }
+
+ int mapId = bpfGetFdMapId(fd);
+ if (mapId == -1) {
+ ALOGE("bpfGetFdMapId failed, ret: %d [%d]", mapId, errno);
+ } else {
+ ALOGI("map %s id %d", mapPinLoc.c_str(), mapId);
+ }
+
+ mapFds.push_back(std::move(fd));
+ }
+
+ return ret;
+}
+
+/* For debugging, dump all instructions */
+static void dumpIns(char* ins, int size) {
+ for (int row = 0; row < size / 8; row++) {
+ ALOGE("%d: ", row);
+ for (int j = 0; j < 8; j++) {
+ ALOGE("%3x ", ins[(row * 8) + j]);
+ }
+ ALOGE("\n");
+ }
+}
+
+/* For debugging, dump all code sections from cs list */
+static void dumpAllCs(vector<codeSection>& cs) {
+ for (int i = 0; i < (int)cs.size(); i++) {
+ ALOGE("Dumping cs %d, name %s", int(i), cs[i].name.c_str());
+ dumpIns((char*)cs[i].data.data(), cs[i].data.size());
+ ALOGE("-----------");
+ }
+}
+
+static void applyRelo(void* insnsPtr, Elf64_Addr offset, int fd) {
+ int insnIndex;
+ struct bpf_insn *insn, *insns;
+
+ insns = (struct bpf_insn*)(insnsPtr);
+
+ insnIndex = offset / sizeof(struct bpf_insn);
+ insn = &insns[insnIndex];
+
+ // Occasionally might be useful for relocation debugging, but pretty spammy
+ if (0) {
+ ALOGD("applying relo to instruction at byte offset: %llu, "
+ "insn offset %d, insn %llx",
+ (unsigned long long)offset, insnIndex, *(unsigned long long*)insn);
+ }
+
+ if (insn->code != (BPF_LD | BPF_IMM | BPF_DW)) {
+ ALOGE("Dumping all instructions till ins %d", insnIndex);
+ ALOGE("invalid relo for insn %d: code 0x%x", insnIndex, insn->code);
+ dumpIns((char*)insnsPtr, (insnIndex + 3) * 8);
+ return;
+ }
+
+ insn->imm = fd;
+ insn->src_reg = BPF_PSEUDO_MAP_FD;
+}
+
+static void applyMapRelo(ifstream& elfFile, vector<unique_fd> &mapFds, vector<codeSection>& cs) {
+ vector<string> mapNames;
+
+ int ret = getSectionSymNames(elfFile, "maps", mapNames);
+ if (ret) return;
+
+ for (int k = 0; k != (int)cs.size(); k++) {
+ Elf64_Rel* rel = (Elf64_Rel*)(cs[k].rel_data.data());
+ int n_rel = cs[k].rel_data.size() / sizeof(*rel);
+
+ for (int i = 0; i < n_rel; i++) {
+ int symIndex = ELF64_R_SYM(rel[i].r_info);
+ string symName;
+
+ ret = getSymNameByIdx(elfFile, symIndex, symName);
+ if (ret) return;
+
+ /* Find the map fd and apply relo */
+ for (int j = 0; j < (int)mapNames.size(); j++) {
+ if (!mapNames[j].compare(symName)) {
+ applyRelo(cs[k].data.data(), rel[i].r_offset, mapFds[j]);
+ break;
+ }
+ }
+ }
+ }
+}
+
+static int loadCodeSections(const char* elfPath, vector<codeSection>& cs, const string& license,
+ const char* prefix) {
+ unsigned kvers = kernelVersion();
+
+ if (!kvers) {
+ ALOGE("unable to get kernel version");
+ return -EINVAL;
+ }
+
+ string objName = pathToObjName(string(elfPath));
+
+ for (int i = 0; i < (int)cs.size(); i++) {
+ unique_fd& fd = cs[i].prog_fd;
+ int ret;
+ string name = cs[i].name;
+
+ if (!cs[i].prog_def.has_value()) {
+ ALOGE("[%d] '%s' missing program definition! bad bpf.o build?", i, name.c_str());
+ return -EINVAL;
+ }
+
+ unsigned min_kver = cs[i].prog_def->min_kver;
+ unsigned max_kver = cs[i].prog_def->max_kver;
+ ALOGD("cs[%d].name:%s min_kver:%x .max_kver:%x (kvers:%x)", i, name.c_str(), min_kver,
+ max_kver, kvers);
+ if (kvers < min_kver) continue;
+ if (kvers >= max_kver) continue;
+
+ unsigned bpfMinVer = cs[i].prog_def->bpfloader_min_ver;
+ unsigned bpfMaxVer = cs[i].prog_def->bpfloader_max_ver;
+ domain selinux_context = getDomainFromSelinuxContext(cs[i].prog_def->selinux_context);
+ domain pin_subdir = getDomainFromPinSubdir(cs[i].prog_def->pin_subdir);
+ // Note: make sure to only check for unrecognized *after* verifying bpfloader
+ // version limits include this bpfloader's version.
+
+ ALOGD("cs[%d].name:%s requires bpfloader version [0x%05x,0x%05x)", i, name.c_str(),
+ bpfMinVer, bpfMaxVer);
+ if (BPFLOADER_VERSION < bpfMinVer) continue;
+ if (BPFLOADER_VERSION >= bpfMaxVer) continue;
+
+ if ((cs[i].prog_def->ignore_on_eng && isEng()) ||
+ (cs[i].prog_def->ignore_on_user && isUser()) ||
+ (cs[i].prog_def->ignore_on_userdebug && isUserdebug())) {
+ ALOGD("cs[%d].name:%s is ignored on %s builds", i, name.c_str(),
+ getBuildType().c_str());
+ continue;
+ }
+
+ if ((isArm() && isKernel32Bit() && cs[i].prog_def->ignore_on_arm32) ||
+ (isArm() && isKernel64Bit() && cs[i].prog_def->ignore_on_aarch64) ||
+ (isX86() && isKernel32Bit() && cs[i].prog_def->ignore_on_x86_32) ||
+ (isX86() && isKernel64Bit() && cs[i].prog_def->ignore_on_x86_64) ||
+ (isRiscV() && cs[i].prog_def->ignore_on_riscv64)) {
+ ALOGD("cs[%d].name:%s is ignored on %s", i, name.c_str(), describeArch());
+ continue;
+ }
+
+ if (unrecognized(pin_subdir)) return -ENOTDIR;
+
+ if (specified(selinux_context)) {
+ ALOGI("prog %s selinux_context [%-32s] -> %d -> '%s' (%s)", name.c_str(),
+ cs[i].prog_def->selinux_context, selinux_context,
+ lookupSelinuxContext(selinux_context), lookupPinSubdir(selinux_context));
+ }
+
+ if (specified(pin_subdir)) {
+ ALOGI("prog %s pin_subdir [%-32s] -> %d -> '%s'", name.c_str(),
+ cs[i].prog_def->pin_subdir, pin_subdir, lookupPinSubdir(pin_subdir));
+ }
+
+ // strip any potential $foo suffix
+ // this can be used to provide duplicate programs
+ // conditionally loaded based on running kernel version
+ name = name.substr(0, name.find_last_of('$'));
+
+ bool reuse = false;
+ // Format of pin location is
+ // /sys/fs/bpf/<prefix>prog_<objName>_<progName>
+ string progPinLoc = string(BPF_FS_PATH) + lookupPinSubdir(pin_subdir, prefix) + "prog_" +
+ objName + '_' + string(name);
+ if (access(progPinLoc.c_str(), F_OK) == 0) {
+ fd.reset(retrieveProgram(progPinLoc.c_str()));
+ ALOGD("New bpf prog load reusing prog %s, ret: %d (%s)", progPinLoc.c_str(), fd.get(),
+ (!fd.ok() ? std::strerror(errno) : "no error"));
+ reuse = true;
+ } else {
+ vector<char> log_buf(BPF_LOAD_LOG_SZ, 0);
+
+ union bpf_attr req = {
+ .prog_type = cs[i].type,
+ .kern_version = kvers,
+ .license = ptr_to_u64(license.c_str()),
+ .insns = ptr_to_u64(cs[i].data.data()),
+ .insn_cnt = static_cast<__u32>(cs[i].data.size() / sizeof(struct bpf_insn)),
+ .log_level = 1,
+ .log_buf = ptr_to_u64(log_buf.data()),
+ .log_size = static_cast<__u32>(log_buf.size()),
+ .expected_attach_type = cs[i].expected_attach_type,
+ };
+ strlcpy(req.prog_name, cs[i].name.c_str(), sizeof(req.prog_name));
+ fd.reset(bpf(BPF_PROG_LOAD, req));
+
+ ALOGD("BPF_PROG_LOAD call for %s (%s) returned fd: %d (%s)", elfPath,
+ cs[i].name.c_str(), fd.get(), (!fd.ok() ? std::strerror(errno) : "no error"));
+
+ if (!fd.ok()) {
+ vector<string> lines = android::base::Split(log_buf.data(), "\n");
+
+ ALOGW("BPF_PROG_LOAD - BEGIN log_buf contents:");
+ for (const auto& line : lines) ALOGW("%s", line.c_str());
+ ALOGW("BPF_PROG_LOAD - END log_buf contents.");
+
+ if (cs[i].prog_def->optional) {
+ ALOGW("failed program is marked optional - continuing...");
+ continue;
+ }
+ ALOGE("non-optional program failed to load.");
+ }
+ }
+
+ if (!fd.ok()) return fd.get();
+
+ if (!reuse) {
+ if (specified(selinux_context)) {
+ string createLoc = string(BPF_FS_PATH) + lookupPinSubdir(selinux_context) +
+ "tmp_prog_" + objName + '_' + string(name);
+ ret = bpfFdPin(fd, createLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", createLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ ret = renameat2(AT_FDCWD, createLoc.c_str(),
+ AT_FDCWD, progPinLoc.c_str(), RENAME_NOREPLACE);
+ if (ret) {
+ int err = errno;
+ ALOGE("rename %s %s -> %d [%d:%s]", createLoc.c_str(), progPinLoc.c_str(), ret,
+ err, strerror(err));
+ return -err;
+ }
+ } else {
+ ret = bpfFdPin(fd, progPinLoc.c_str());
+ if (ret) {
+ int err = errno;
+ ALOGE("create %s -> %d [%d:%s]", progPinLoc.c_str(), ret, err, strerror(err));
+ return -err;
+ }
+ }
+ if (chmod(progPinLoc.c_str(), 0440)) {
+ int err = errno;
+ ALOGE("chmod %s 0440 -> [%d:%s]", progPinLoc.c_str(), err, strerror(err));
+ return -err;
+ }
+ if (chown(progPinLoc.c_str(), (uid_t)cs[i].prog_def->uid,
+ (gid_t)cs[i].prog_def->gid)) {
+ int err = errno;
+ ALOGE("chown %s %d %d -> [%d:%s]", progPinLoc.c_str(), cs[i].prog_def->uid,
+ cs[i].prog_def->gid, err, strerror(err));
+ return -err;
+ }
+ }
+
+ int progId = bpfGetFdProgId(fd);
+ if (progId == -1) {
+ ALOGE("bpfGetFdProgId failed, ret: %d [%d]", progId, errno);
+ } else {
+ ALOGI("prog %s id %d", progPinLoc.c_str(), progId);
+ }
+ }
+
+ return 0;
+}
+
+int loadProg(const char* elfPath, bool* isCritical, const Location& location) {
+ vector<char> license;
+ vector<char> critical;
+ vector<codeSection> cs;
+ vector<unique_fd> mapFds;
+ int ret;
+
+ if (!isCritical) return -1;
+ *isCritical = false;
+
+ ifstream elfFile(elfPath, ios::in | ios::binary);
+ if (!elfFile.is_open()) return -1;
+
+ ret = readSectionByName("critical", elfFile, critical);
+ *isCritical = !ret;
+
+ ret = readSectionByName("license", elfFile, license);
+ if (ret) {
+ ALOGE("Couldn't find license in %s", elfPath);
+ return ret;
+ } else {
+ ALOGD("Loading %s%s ELF object %s with license %s",
+ *isCritical ? "critical for " : "optional", *isCritical ? (char*)critical.data() : "",
+ elfPath, (char*)license.data());
+ }
+
+ // the following default values are for bpfloader V0.0 format which does not include them
+ unsigned int bpfLoaderMinVer =
+ readSectionUint("bpfloader_min_ver", elfFile, DEFAULT_BPFLOADER_MIN_VER);
+ unsigned int bpfLoaderMaxVer =
+ readSectionUint("bpfloader_max_ver", elfFile, DEFAULT_BPFLOADER_MAX_VER);
+ unsigned int bpfLoaderMinRequiredVer =
+ readSectionUint("bpfloader_min_required_ver", elfFile, 0);
+ size_t sizeOfBpfMapDef =
+ readSectionUint("size_of_bpf_map_def", elfFile, DEFAULT_SIZEOF_BPF_MAP_DEF);
+ size_t sizeOfBpfProgDef =
+ readSectionUint("size_of_bpf_prog_def", elfFile, DEFAULT_SIZEOF_BPF_PROG_DEF);
+
+ // inclusive lower bound check
+ if (BPFLOADER_VERSION < bpfLoaderMinVer) {
+ ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with min ver 0x%05x",
+ BPFLOADER_VERSION, elfPath, bpfLoaderMinVer);
+ return 0;
+ }
+
+ // exclusive upper bound check
+ if (BPFLOADER_VERSION >= bpfLoaderMaxVer) {
+ ALOGI("BpfLoader version 0x%05x ignoring ELF object %s with max ver 0x%05x",
+ BPFLOADER_VERSION, elfPath, bpfLoaderMaxVer);
+ return 0;
+ }
+
+ if (BPFLOADER_VERSION < bpfLoaderMinRequiredVer) {
+ ALOGI("BpfLoader version 0x%05x failing due to ELF object %s with required min ver 0x%05x",
+ BPFLOADER_VERSION, elfPath, bpfLoaderMinRequiredVer);
+ return -1;
+ }
+
+ ALOGI("BpfLoader version 0x%05x processing ELF object %s with ver [0x%05x,0x%05x)",
+ BPFLOADER_VERSION, elfPath, bpfLoaderMinVer, bpfLoaderMaxVer);
+
+ if (sizeOfBpfMapDef < DEFAULT_SIZEOF_BPF_MAP_DEF) {
+ ALOGE("sizeof(bpf_map_def) of %zu is too small (< %d)", sizeOfBpfMapDef,
+ DEFAULT_SIZEOF_BPF_MAP_DEF);
+ return -1;
+ }
+
+ if (sizeOfBpfProgDef < DEFAULT_SIZEOF_BPF_PROG_DEF) {
+ ALOGE("sizeof(bpf_prog_def) of %zu is too small (< %d)", sizeOfBpfProgDef,
+ DEFAULT_SIZEOF_BPF_PROG_DEF);
+ return -1;
+ }
+
+ ret = readCodeSections(elfFile, cs, sizeOfBpfProgDef);
+ if (ret) {
+ ALOGE("Couldn't read all code sections in %s", elfPath);
+ return ret;
+ }
+
+ /* Just for future debugging */
+ if (0) dumpAllCs(cs);
+
+ ret = createMaps(elfPath, elfFile, mapFds, location.prefix, sizeOfBpfMapDef);
+ if (ret) {
+ ALOGE("Failed to create maps: (ret=%d) in %s", ret, elfPath);
+ return ret;
+ }
+
+ for (int i = 0; i < (int)mapFds.size(); i++)
+ ALOGD("map_fd found at %d is %d in %s", i, mapFds[i].get(), elfPath);
+
+ applyMapRelo(elfFile, mapFds, cs);
+
+ ret = loadCodeSections(elfPath, cs, string(license.data()), location.prefix);
+ if (ret) ALOGE("Failed to load programs, loadCodeSections ret=%d", ret);
+
+ return ret;
+}
+
+} // namespace bpf
+} // namespace android
diff --git a/netbpfload/loader.h b/netbpfload/loader.h
new file mode 100644
index 0000000..b884637
--- /dev/null
+++ b/netbpfload/loader.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018-2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <linux/bpf.h>
+
+#include <fstream>
+
+namespace android {
+namespace bpf {
+
+// Bpf programs may specify per-program & per-map selinux_context and pin_subdir.
+//
+// The BpfLoader needs to convert these bpf.o specified strings into an enum
+// for internal use (to check that valid values were specified for the specific
+// location of the bpf.o file).
+//
+// It also needs to map selinux_context's into pin_subdir's.
+// This is because of how selinux_context is actually implemented via pin+rename.
+//
+// Thus 'domain' enumerates all selinux_context's/pin_subdir's that the BpfLoader
+// is aware of. Thus there currently needs to be a 1:1 mapping between the two.
+//
+enum class domain : int {
+ unrecognized = -1, // invalid for this version of the bpfloader
+ unspecified = 0, // means just use the default for that specific pin location
+ tethering, // (S+) fs_bpf_tethering /sys/fs/bpf/tethering
+ net_private, // (T+) fs_bpf_net_private /sys/fs/bpf/net_private
+ net_shared, // (T+) fs_bpf_net_shared /sys/fs/bpf/net_shared
+ netd_readonly, // (T+) fs_bpf_netd_readonly /sys/fs/bpf/netd_readonly
+ netd_shared, // (T+) fs_bpf_netd_shared /sys/fs/bpf/netd_shared
+};
+
+// Note: this does not include domain::unrecognized, but does include domain::unspecified
+static constexpr domain AllDomains[] = {
+ domain::unspecified,
+ domain::tethering,
+ domain::net_private,
+ domain::net_shared,
+ domain::netd_readonly,
+ domain::netd_shared,
+};
+
+static constexpr bool unrecognized(domain d) {
+ return d == domain::unrecognized;
+}
+
+// Note: this doesn't handle unrecognized, handle it first.
+static constexpr bool specified(domain d) {
+ return d != domain::unspecified;
+}
+
+struct Location {
+ const char* const dir = "";
+ const char* const prefix = "";
+};
+
+// BPF loader implementation. Loads an eBPF ELF object
+int loadProg(const char* elfPath, bool* isCritical, const Location &location = {});
+
+// Exposed for testing
+unsigned int readSectionUint(const char* name, std::ifstream& elfFile, unsigned int defVal);
+
+// Returns the build type string (from ro.build.type).
+const std::string& getBuildType();
+
+// The following functions classify the 3 Android build types.
+inline bool isEng() {
+ return getBuildType() == "eng";
+}
+inline bool isUser() {
+ return getBuildType() == "user";
+}
+inline bool isUserdebug() {
+ return getBuildType() == "userdebug";
+}
+
+} // namespace bpf
+} // namespace android
diff --git a/netbpfload/netbpfload.rc b/netbpfload/netbpfload.rc
new file mode 100644
index 0000000..14181dc
--- /dev/null
+++ b/netbpfload/netbpfload.rc
@@ -0,0 +1,86 @@
+# zygote-start is what officially starts netd (see //system/core/rootdir/init.rc)
+# However, on some hardware it's started from post-fs-data as well, which is just
+# a tad earlier. There's no benefit to that though, since on 4.9+ P+ devices netd
+# will just block until bpfloader finishes and sets the bpf.progs_loaded property.
+#
+# It is important that we start bpfloader after:
+# - /sys/fs/bpf is already mounted,
+# - apex (incl. rollback) is initialized (so that in the future we can load bpf
+# programs shipped as part of apex mainline modules)
+# - logd is ready for us to log stuff
+#
+# At the same time we want to be as early as possible to reduce races and thus
+# failures (before memory is fragmented, and cpu is busy running tons of other
+# stuff) and we absolutely want to be before netd and the system boot slot is
+# considered to have booted successfully.
+#
+on load_bpf_programs
+ exec_start bpfloader
+
+service bpfloader /system/bin/netbpfload
+ # netbpfload will do network bpf loading, then execute /system/bin/bpfloader
+ capabilities CHOWN SYS_ADMIN NET_ADMIN
+ # The following group memberships are a workaround for lack of DAC_OVERRIDE
+ # and allow us to open (among other things) files that we created and are
+ # no longer root owned (due to CHOWN) but still have group read access to
+ # one of the following groups. This is not perfect, but a more correct
+ # solution requires significantly more effort to implement.
+ group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
+ user root
+ #
+ # Set RLIMIT_MEMLOCK to 1GiB for bpfloader
+ #
+ # Actually only 8MiB would be needed if bpfloader ran as its own uid.
+ #
+ # However, while the rlimit is per-thread, the accounting is system wide.
+ # So, for example, if the graphics stack has already allocated 10MiB of
+ # memlock data before bpfloader even gets a chance to run, it would fail
+ # if its memlock rlimit is only 8MiB - since there would be none left for it.
+ #
+ # bpfloader succeeding is critical to system health, since a failure will
+ # cause netd crashloop and thus system server crashloop... and the only
+ # recovery is a full kernel reboot.
+ #
+ # We've had issues where devices would sometimes (rarely) boot into
+ # a crashloop because bpfloader would occasionally lose a boot time
+ # race against the graphics stack's boot time locked memory allocation.
+ #
+ # Thus bpfloader's memlock has to be 8MB higher then the locked memory
+ # consumption of the root uid anywhere else in the system...
+ # But we don't know what that is for all possible devices...
+ #
+ # Ideally, we'd simply grant bpfloader the IPC_LOCK capability and it
+ # would simply ignore it's memlock rlimit... but it turns that this
+ # capability is not even checked by the kernel's bpf system call.
+ #
+ # As such we simply use 1GiB as a reasonable approximation of infinity.
+ #
+ rlimit memlock 1073741824 1073741824
+ oneshot
+ #
+ # How to debug bootloops caused by 'bpfloader-failed'.
+ #
+ # 1. On some lower RAM devices (like wembley) you may need to first enable developer mode
+ # (from the Settings app UI), and change the developer option "Logger buffer sizes"
+ # from the default (wembley: 64kB) to the maximum (1M) per log buffer.
+ # Otherwise buffer will overflow before you manage to dump it and you'll get useless logs.
+ #
+ # 2. comment out 'reboot_on_failure reboot,bpfloader-failed' below
+ # 3. rebuild/reflash/reboot
+ # 4. as the device is booting up capture bpfloader logs via:
+ # adb logcat -s 'bpfloader:*' 'LibBpfLoader:*' 'NetBpfLoad:*' 'NetBpfLoader:*'
+ #
+ # something like:
+ # $ adb reboot; sleep 1; adb wait-for-device; adb root; sleep 1; adb wait-for-device; adb logcat -s 'bpfloader:*' 'LibBpfLoader:*' 'NetBpfLoad:*' 'NetBpfLoader:*'
+ # will take care of capturing logs as early as possible
+ #
+ # 5. look through the logs from the kernel's bpf verifier that bpfloader dumps out,
+ # it usually makes sense to search back from the end and find the particular
+ # bpf verifier failure that caused bpfloader to terminate early with an error code.
+ # This will probably be something along the lines of 'too many jumps' or
+ # 'cannot prove return value is 0 or 1' or 'unsupported / unknown operation / helper',
+ # 'invalid bpf_context access', etc.
+ #
+ reboot_on_failure reboot,bpfloader-failed
+ # we're not really updatable, but want to be able to load bpf programs shipped in apexes
+ updatable
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index a090a54..a7a4059 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -129,6 +129,24 @@
RETURN_IF_NOT_OK(
attachProgramToCgroup(CGROUP_SOCKET_PROG_PATH, cg_fd, BPF_CGROUP_INET_SOCK_CREATE));
}
+
+ if (bpf::isAtLeastKernelVersion(4, 19, 0)) {
+ RETURN_IF_NOT_OK(attachProgramToCgroup(
+ "/sys/fs/bpf/netd_readonly/prog_block_bind4_block_port",
+ cg_fd, BPF_CGROUP_INET4_BIND));
+ RETURN_IF_NOT_OK(attachProgramToCgroup(
+ "/sys/fs/bpf/netd_readonly/prog_block_bind6_block_port",
+ cg_fd, BPF_CGROUP_INET6_BIND));
+
+ // This should trivially pass, since we just attached up above,
+ // but BPF_PROG_QUERY is only implemented on 4.19+ kernels.
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_EGRESS) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_INGRESS) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET_SOCK_CREATE) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET4_BIND) <= 0) abort();
+ if (bpf::queryProgram(cg_fd, BPF_CGROUP_INET6_BIND) <= 0) abort();
+ }
+
return netdutils::status::ok;
}
diff --git a/remoteauth/tests/unit/Android.bp b/remoteauth/tests/unit/Android.bp
index 16a8242..a21c033 100644
--- a/remoteauth/tests/unit/Android.bp
+++ b/remoteauth/tests/unit/Android.bp
@@ -20,7 +20,7 @@
name: "RemoteAuthUnitTests",
defaults: [
"enable-remoteauth-targets",
- "mts-target-sdk-version-current"
+ "mts-target-sdk-version-current",
],
sdk_version: "test_current",
min_sdk_version: "31",
@@ -45,7 +45,7 @@
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"service-remoteauth-pre-jarjar",
- "truth-prebuilt",
+ "truth",
],
// these are needed for Extended Mockito
jni_libs: [
diff --git a/service-t/Android.bp b/service-t/Android.bp
index 7e2d2f4..bc49f0e 100644
--- a/service-t/Android.bp
+++ b/service-t/Android.bp
@@ -102,7 +102,12 @@
],
exclude_srcs: [
"src/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitor.java",
- "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java"
+ "src/com/android/server/connectivity/mdns/SocketNetLinkMonitorFactory.java",
+ "src/com/android/server/connectivity/mdns/MdnsAdvertiser.java",
+ "src/com/android/server/connectivity/mdns/MdnsAnnouncer.java",
+ "src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java",
+ "src/com/android/server/connectivity/mdns/MdnsProber.java",
+ "src/com/android/server/connectivity/mdns/MdnsRecordRepository.java",
],
static_libs: [
"net-utils-device-common-mdns-standalone-build-test",
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 187eadf..fbe02a5 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -20,7 +20,6 @@
srcs: [
"jni/com_android_server_net_NetworkStatsFactory.cpp",
],
- path: "jni",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
],
@@ -32,7 +31,6 @@
"jni/com_android_server_net_NetworkStatsFactory.cpp",
"jni/com_android_server_net_NetworkStatsService.cpp",
],
- path: "jni",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
],
diff --git a/service-t/lint-baseline.xml b/service-t/lint-baseline.xml
new file mode 100644
index 0000000..38d3ab0
--- /dev/null
+++ b/service-t/lint-baseline.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier#getInterfaceName`"
+ errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="224"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getInterface`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="276"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getOwnerUid`"
+ errorLine1=" delta.migrateTun(info.getOwnerUid(), info.getInterface(),"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="276"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.UnderlyingNetworkInfo#getUnderlyingInterfaces`"
+ errorLine1=" info.getUnderlyingInterfaces());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsFactory.java"
+ line="277"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" dnsAddresses.add(InetAddress.parseNumericAddress(address));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="875"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=" staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetTracker.java"
+ line="870"
+ column="66"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(os);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/NetworkStatsRecorder.java"
+ line="556"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(sockFd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1309"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mSocket);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/IpSecService.java"
+ line="1034"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+ line="156"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.EthernetNetworkSpecifier`"
+ errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="218"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.util.AtomicFile`"
+ errorLine1=" mFile = new AtomicFile(new File(path), logger);"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/net/PersistentInt.java"
+ line="53"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new java.net.InetSocketAddress`"
+ errorLine1=" super(handler, new RecvBuffer(buffer, new InetSocketAddress()));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java"
+ line="66"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java"
+ line="156"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Cast from `EthernetNetworkSpecifier` to `NetworkSpecifier` requires API level 31 (current min is 30)"
+ errorLine1=" nc.setNetworkSpecifier(new EthernetNetworkSpecifier(iface));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="218"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+ errorLine1=" if (!((EthernetNetworkSpecifier) spec).getInterfaceName().matches(iface)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="224"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.EthernetNetworkSpecifier`"
+ errorLine1=" if (!(spec instanceof EthernetNetworkSpecifier)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java"
+ line="221"
+ column="31"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 80c315a..450f380 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -25,9 +25,15 @@
#include <perfetto/tracing/platform.h>
#include <perfetto/tracing/tracing.h>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "netdbpf/BpfNetworkStats.h"
+
namespace android {
namespace bpf {
namespace internal {
+using ::android::base::StringPrintf;
void NetworkTracePoller::PollAndSchedule(perfetto::base::TaskRunner* runner,
uint32_t poll_ms) {
@@ -116,6 +122,28 @@
return res.ok();
}
+void NetworkTracePoller::TraceIfaces(const std::vector<PacketTrace>& packets) {
+ if (packets.empty()) return;
+
+ std::unordered_set<uint32_t> uniqueIfindex;
+ for (const PacketTrace& pkt : packets) {
+ uniqueIfindex.insert(pkt.ifindex);
+ }
+
+ for (uint32_t ifindex : uniqueIfindex) {
+ char ifname[IF_NAMESIZE] = {};
+ if (if_indextoname(ifindex, ifname) != ifname) continue;
+
+ StatsValue stats = {};
+ if (bpfGetIfIndexStats(ifindex, &stats) != 0) continue;
+
+ std::string rxTrack = StringPrintf("%s [%d] Rx Bytes", ifname, ifindex);
+ std::string txTrack = StringPrintf("%s [%d] Tx Bytes", ifname, ifindex);
+ ATRACE_INT64(rxTrack.c_str(), stats.rxBytes);
+ ATRACE_INT64(txTrack.c_str(), stats.txBytes);
+ }
+}
+
bool NetworkTracePoller::ConsumeAll() {
std::scoped_lock<std::mutex> lock(mMutex);
return ConsumeAllLocked();
@@ -137,6 +165,7 @@
ATRACE_INT("NetworkTracePackets", packets.size());
+ TraceIfaces(packets);
mCallback(packets);
return true;
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
index 8433934..092ab64 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTracePoller.h
@@ -61,6 +61,11 @@
void PollAndSchedule(perfetto::base::TaskRunner* runner, uint32_t poll_ms);
bool ConsumeAllLocked() REQUIRES(mMutex);
+ // Record sparse iface stats via atrace. This queries the per-iface stats maps
+ // for any iface present in the vector of packets. This is inexact, but should
+ // have sufficient coverage given these are cumulative counters.
+ void TraceIfaces(const std::vector<PacketTrace>& packets) REQUIRES(mMutex);
+
std::mutex mMutex;
// Records the number of successfully started active sessions so that only the
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 93ccb85..ee5f25b 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -519,9 +519,9 @@
}
}
+ // TODO: Use a Handler instead of a StateMachine since there are no state changes.
private class NsdStateMachine extends StateMachine {
- private final DefaultState mDefaultState = new DefaultState();
private final EnabledState mEnabledState = new EnabledState();
@Override
@@ -591,124 +591,12 @@
NsdStateMachine(String name, Handler handler) {
super(name, handler);
- addState(mDefaultState);
- addState(mEnabledState, mDefaultState);
+ addState(mEnabledState);
State initialState = mEnabledState;
setInitialState(initialState);
setLogRecSize(25);
}
- class DefaultState extends State {
- @Override
- public boolean processMessage(Message msg) {
- final ClientInfo cInfo;
- final int clientRequestId = msg.arg2;
- switch (msg.what) {
- case NsdManager.REGISTER_CLIENT:
- final ConnectorArgs arg = (ConnectorArgs) msg.obj;
- final INsdManagerCallback cb = arg.callback;
- try {
- cb.asBinder().linkToDeath(arg.connector, 0);
- final String tag = "Client" + arg.uid + "-" + mClientNumberId++;
- final NetworkNsdReportedMetrics metrics =
- mDeps.makeNetworkNsdReportedMetrics(
- (int) mClock.elapsedRealtime());
- cInfo = new ClientInfo(cb, arg.uid, arg.useJavaBackend,
- mServiceLogs.forSubComponent(tag), metrics);
- mClients.put(arg.connector, cInfo);
- } catch (RemoteException e) {
- Log.w(TAG, "Client request id " + clientRequestId
- + " has already died");
- }
- break;
- case NsdManager.UNREGISTER_CLIENT:
- final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
- cInfo = mClients.remove(connector);
- if (cInfo != null) {
- cInfo.expungeAllRequests();
- if (cInfo.isPreSClient()) {
- mLegacyClientCount -= 1;
- }
- }
- maybeStopMonitoringSocketsIfNoActiveRequest();
- maybeScheduleStop();
- break;
- case NsdManager.DISCOVER_SERVICES:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onDiscoverServicesFailedImmediately(clientRequestId,
- NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
- }
- break;
- case NsdManager.STOP_DISCOVERY:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onStopDiscoveryFailed(
- clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
- }
- break;
- case NsdManager.REGISTER_SERVICE:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onRegisterServiceFailedImmediately(clientRequestId,
- NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
- }
- break;
- case NsdManager.UNREGISTER_SERVICE:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onUnregisterServiceFailed(
- clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
- }
- break;
- case NsdManager.RESOLVE_SERVICE:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onResolveServiceFailedImmediately(clientRequestId,
- NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
- }
- break;
- case NsdManager.STOP_RESOLUTION:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onStopResolutionFailed(
- clientRequestId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
- }
- break;
- case NsdManager.REGISTER_SERVICE_CALLBACK:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cInfo.onServiceInfoCallbackRegistrationFailed(
- clientRequestId, NsdManager.FAILURE_BAD_PARAMETERS);
- }
- break;
- case NsdManager.DAEMON_CLEANUP:
- maybeStopDaemon();
- break;
- // This event should be only sent by the legacy (target SDK < S) clients.
- // Mark the sending client as legacy.
- case NsdManager.DAEMON_STARTUP:
- cInfo = getClientInfoForReply(msg);
- if (cInfo != null) {
- cancelStop();
- cInfo.setPreSClient();
- mLegacyClientCount += 1;
- maybeStartDaemon();
- }
- break;
- default:
- Log.e(TAG, "Unhandled " + msg);
- return NOT_HANDLED;
- }
- return HANDLED;
- }
-
- private ClientInfo getClientInfoForReply(Message msg) {
- final ListenerArgs args = (ListenerArgs) msg.obj;
- return mClients.get(args.connector);
- }
- }
-
class EnabledState extends State {
@Override
public void enter() {
@@ -793,6 +681,11 @@
removeRequestMap(clientRequestId, transactionId, clientInfo);
}
+ private ClientInfo getClientInfoForReply(Message msg) {
+ final ListenerArgs args = (ListenerArgs) msg.obj;
+ return mClients.get(args.connector);
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
@@ -1214,7 +1107,51 @@
case NsdManager.UNREGISTER_OFFLOAD_ENGINE:
mOffloadEngines.unregister((IOffloadEngine) msg.obj);
break;
+ case NsdManager.REGISTER_CLIENT:
+ final ConnectorArgs arg = (ConnectorArgs) msg.obj;
+ final INsdManagerCallback cb = arg.callback;
+ try {
+ cb.asBinder().linkToDeath(arg.connector, 0);
+ final String tag = "Client" + arg.uid + "-" + mClientNumberId++;
+ final NetworkNsdReportedMetrics metrics =
+ mDeps.makeNetworkNsdReportedMetrics(
+ (int) mClock.elapsedRealtime());
+ clientInfo = new ClientInfo(cb, arg.uid, arg.useJavaBackend,
+ mServiceLogs.forSubComponent(tag), metrics);
+ mClients.put(arg.connector, clientInfo);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Client request id " + clientRequestId
+ + " has already died");
+ }
+ break;
+ case NsdManager.UNREGISTER_CLIENT:
+ final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
+ clientInfo = mClients.remove(connector);
+ if (clientInfo != null) {
+ clientInfo.expungeAllRequests();
+ if (clientInfo.isPreSClient()) {
+ mLegacyClientCount -= 1;
+ }
+ }
+ maybeStopMonitoringSocketsIfNoActiveRequest();
+ maybeScheduleStop();
+ break;
+ case NsdManager.DAEMON_CLEANUP:
+ maybeStopDaemon();
+ break;
+ // This event should be only sent by the legacy (target SDK < S) clients.
+ // Mark the sending client as legacy.
+ case NsdManager.DAEMON_STARTUP:
+ clientInfo = getClientInfoForReply(msg);
+ if (clientInfo != null) {
+ cancelStop();
+ clientInfo.setPreSClient();
+ mLegacyClientCount += 1;
+ maybeStartDaemon();
+ }
+ break;
default:
+ Log.wtf(TAG, "Unhandled " + msg);
return NOT_HANDLED;
}
return HANDLED;
@@ -1703,15 +1640,22 @@
am.addOnUidImportanceListener(new UidImportanceListener(handler),
mRunningAppActiveImportanceCutoff);
+ final MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder()
+ .setIsMdnsOffloadFeatureEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD))
+ .setIncludeInetAddressRecordsInProbing(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING))
+ .setIsExpiredServicesRemovalEnabled(mDeps.isFeatureEnabled(
+ mContext, MdnsFeatureFlags.NSD_EXPIRED_SERVICES_REMOVAL))
+ .setIsLabelCountLimitEnabled(mDeps.isTetheringFeatureNotChickenedOut(
+ mContext, MdnsFeatureFlags.NSD_LIMIT_LABEL_COUNT))
+ .build();
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider,
- LOGGER.forSubComponent("MdnsMultinetworkSocketClient"));
+ LOGGER.forSubComponent("MdnsMultinetworkSocketClient"), flags);
mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
- mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
+ mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"), flags);
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
- MdnsFeatureFlags flags = new MdnsFeatureFlags.Builder().setIsMdnsOffloadFeatureEnabled(
- mDeps.isTetheringFeatureNotChickenedOut(mContext,
- MdnsFeatureFlags.NSD_FORCE_DISABLE_MDNS_OFFLOAD)).build();
mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"), flags);
mClock = deps.makeClock();
@@ -1769,12 +1713,21 @@
}
/**
+ * @see DeviceConfigUtils#isTrunkStableFeatureEnabled
+ */
+ public boolean isTrunkStableFeatureEnabled(String feature) {
+ return DeviceConfigUtils.isTrunkStableFeatureEnabled(feature);
+ }
+
+ /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog) {
- return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
+ @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags featureFlags) {
+ return new MdnsDiscoveryManager(
+ executorProvider, socketClient, sharedLog, featureFlags);
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index a946bca..28e3924 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -96,10 +96,11 @@
@NonNull Looper looper, @NonNull byte[] packetCreationBuffer,
@NonNull MdnsInterfaceAdvertiser.Callback cb,
@NonNull String[] deviceHostName,
- @NonNull SharedLog sharedLog) {
+ @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
// Note NetworkInterface is final and not mockable
return new MdnsInterfaceAdvertiser(socket, initialAddresses, looper,
- packetCreationBuffer, cb, deviceHostName, sharedLog);
+ packetCreationBuffer, cb, deviceHostName, sharedLog, mdnsFeatureFlags);
}
/**
@@ -394,7 +395,8 @@
if (advertiser == null) {
advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer,
mInterfaceAdvertiserCb, mDeviceHostName,
- mSharedLog.forSubComponent(socket.getInterface().getName()));
+ mSharedLog.forSubComponent(socket.getInterface().getName()),
+ mMdnsFeatureFlags);
mAllAdvertisers.put(socket, advertiser);
advertiser.start();
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
index d9bc643..5812797 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAnnouncer.java
@@ -110,7 +110,7 @@
@NonNull MdnsReplySender replySender,
@Nullable PacketRepeaterCallback<BaseAnnouncementInfo> cb,
@NonNull SharedLog sharedLog) {
- super(looper, replySender, cb, sharedLog);
+ super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
}
// TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
index 1251170..b83a6a0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConstants.java
@@ -19,6 +19,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
@@ -42,6 +43,10 @@
public static final String SUBTYPE_PREFIX = "_";
private static final String MDNS_IPV4_HOST_ADDRESS = "224.0.0.251";
private static final String MDNS_IPV6_HOST_ADDRESS = "FF02::FB";
+ public static final InetSocketAddress IPV6_SOCKET_ADDR = new InetSocketAddress(
+ getMdnsIPv6Address(), MDNS_PORT);
+ public static final InetSocketAddress IPV4_SOCKET_ADDR = new InetSocketAddress(
+ getMdnsIPv4Address(), MDNS_PORT);
private static InetAddress mdnsAddress;
private MdnsConstants() {
}
@@ -75,4 +80,4 @@
public static Charset getUtf8Charset() {
return UTF_8;
}
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 24e9fa8..766f999 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -53,6 +53,7 @@
@NonNull private final Handler handler;
@Nullable private final HandlerThread handlerThread;
@NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
private static class PerSocketServiceTypeClients {
private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
@@ -117,20 +118,22 @@
}
public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
+ @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
this.executorProvider = executorProvider;
this.socketClient = socketClient;
this.sharedLog = sharedLog;
this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
+ this.mdnsFeatureFlags = mdnsFeatureFlags;
if (socketClient.getLooper() != null) {
this.handlerThread = null;
this.handler = new Handler(socketClient.getLooper());
- this.serviceCache = new MdnsServiceCache(socketClient.getLooper());
+ this.serviceCache = new MdnsServiceCache(socketClient.getLooper(), mdnsFeatureFlags);
} else {
this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
this.handlerThread.start();
this.handler = new Handler(handlerThread.getLooper());
- this.serviceCache = new MdnsServiceCache(handlerThread.getLooper());
+ this.serviceCache = new MdnsServiceCache(handlerThread.getLooper(), mdnsFeatureFlags);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
index 9840409..0a6d8c1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsFeatureFlags.java
@@ -20,18 +20,50 @@
*/
public class MdnsFeatureFlags {
/**
- * The feature flag for control whether the mDNS offload is enabled or not.
+ * A feature flag to control whether the mDNS offload is enabled or not.
*/
public static final String NSD_FORCE_DISABLE_MDNS_OFFLOAD = "nsd_force_disable_mdns_offload";
+ /**
+ * A feature flag to control whether the probing question should include
+ * InetAddressRecords or not.
+ */
+ public static final String INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING =
+ "include_inet_address_records_in_probing";
+ /**
+ * A feature flag to control whether expired services removal should be enabled.
+ */
+ public static final String NSD_EXPIRED_SERVICES_REMOVAL =
+ "nsd_expired_services_removal";
+
+ /**
+ * A feature flag to control whether the label count limit should be enabled.
+ */
+ public static final String NSD_LIMIT_LABEL_COUNT = "nsd_limit_label_count";
+
// Flag for offload feature
public final boolean mIsMdnsOffloadFeatureEnabled;
+ // Flag for including InetAddressRecords in probing questions.
+ public final boolean mIncludeInetAddressRecordsInProbing;
+
+ // Flag for expired services removal
+ public final boolean mIsExpiredServicesRemovalEnabled;
+
+ // Flag for label count limit
+ public final boolean mIsLabelCountLimitEnabled;
+
/**
* The constructor for {@link MdnsFeatureFlags}.
*/
- public MdnsFeatureFlags(boolean isOffloadFeatureEnabled) {
+ public MdnsFeatureFlags(boolean isOffloadFeatureEnabled,
+ boolean includeInetAddressRecordsInProbing,
+ boolean isExpiredServicesRemovalEnabled,
+ boolean isLabelCountLimitEnabled) {
mIsMdnsOffloadFeatureEnabled = isOffloadFeatureEnabled;
+ mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
+ mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
+ mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
}
@@ -44,16 +76,24 @@
public static final class Builder {
private boolean mIsMdnsOffloadFeatureEnabled;
+ private boolean mIncludeInetAddressRecordsInProbing;
+ private boolean mIsExpiredServicesRemovalEnabled;
+ private boolean mIsLabelCountLimitEnabled;
/**
* The constructor for {@link Builder}.
*/
public Builder() {
mIsMdnsOffloadFeatureEnabled = false;
+ mIncludeInetAddressRecordsInProbing = false;
+ mIsExpiredServicesRemovalEnabled = false;
+ mIsLabelCountLimitEnabled = true; // Default enabled.
}
/**
- * Set if the mDNS offload feature is enabled.
+ * Set whether the mDNS offload feature is enabled.
+ *
+ * @see #NSD_FORCE_DISABLE_MDNS_OFFLOAD
*/
public Builder setIsMdnsOffloadFeatureEnabled(boolean isMdnsOffloadFeatureEnabled) {
mIsMdnsOffloadFeatureEnabled = isMdnsOffloadFeatureEnabled;
@@ -61,11 +101,44 @@
}
/**
+ * Set whether the probing question should include InetAddressRecords.
+ *
+ * @see #INCLUDE_INET_ADDRESS_RECORDS_IN_PROBING
+ */
+ public Builder setIncludeInetAddressRecordsInProbing(
+ boolean includeInetAddressRecordsInProbing) {
+ mIncludeInetAddressRecordsInProbing = includeInetAddressRecordsInProbing;
+ return this;
+ }
+
+ /**
+ * Set whether the expired services removal is enabled.
+ *
+ * @see #NSD_EXPIRED_SERVICES_REMOVAL
+ */
+ public Builder setIsExpiredServicesRemovalEnabled(boolean isExpiredServicesRemovalEnabled) {
+ mIsExpiredServicesRemovalEnabled = isExpiredServicesRemovalEnabled;
+ return this;
+ }
+
+ /**
+ * Set whether the label count limit is enabled.
+ *
+ * @see #NSD_LIMIT_LABEL_COUNT
+ */
+ public Builder setIsLabelCountLimitEnabled(boolean isLabelCountLimitEnabled) {
+ mIsLabelCountLimitEnabled = isLabelCountLimitEnabled;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsFeatureFlags} with the arguments supplied to this builder.
*/
public MdnsFeatureFlags build() {
- return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled);
+ return new MdnsFeatureFlags(mIsMdnsOffloadFeatureEnabled,
+ mIncludeInetAddressRecordsInProbing,
+ mIsExpiredServicesRemovalEnabled,
+ mIsLabelCountLimitEnabled);
}
-
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
index dd8a526..973fd96 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInetAddressRecord.java
@@ -18,7 +18,7 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
import java.io.IOException;
import java.net.Inet4Address;
@@ -29,7 +29,7 @@
import java.util.Objects;
/** An mDNS "AAAA" or "A" record, which holds an IPv6 or IPv4 address. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsInetAddressRecord extends MdnsRecord {
@Nullable private Inet6Address inet6Address;
@Nullable private Inet4Address inet4Address;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 40dfd57..62c37ad 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -65,9 +65,12 @@
private final MdnsProber mProber;
@NonNull
private final MdnsReplySender mReplySender;
-
@NonNull
private final SharedLog mSharedLog;
+ @NonNull
+ private final byte[] mPacketCreationBuffer;
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
/**
* Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates.
@@ -150,8 +153,8 @@
/** @see MdnsRecordRepository */
@NonNull
public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper,
- @NonNull String[] deviceHostName) {
- return new MdnsRecordRepository(looper, deviceHostName);
+ @NonNull String[] deviceHostName, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ return new MdnsRecordRepository(looper, deviceHostName, mdnsFeatureFlags);
}
/** @see MdnsReplySender */
@@ -161,7 +164,7 @@
@NonNull SharedLog sharedLog) {
return new MdnsReplySender(looper, socket, packetCreationBuffer,
sharedLog.forSubComponent(
- MdnsReplySender.class.getSimpleName() + "/" + interfaceTag));
+ MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG);
}
/** @see MdnsAnnouncer */
@@ -187,27 +190,31 @@
public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
@NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
@NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
- @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
+ @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
this(socket, initialAddresses, looper, packetCreationBuffer, cb,
- new Dependencies(), deviceHostName, sharedLog);
+ new Dependencies(), deviceHostName, sharedLog, mdnsFeatureFlags);
}
public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
@NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
@NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull Dependencies deps,
- @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
- mRecordRepository = deps.makeRecordRepository(looper, deviceHostName);
+ @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ mRecordRepository = deps.makeRecordRepository(looper, deviceHostName, mdnsFeatureFlags);
mRecordRepository.updateAddresses(initialAddresses);
mSocket = socket;
mCb = cb;
mCbHandler = new Handler(looper);
mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
packetCreationBuffer, sharedLog);
+ mPacketCreationBuffer = packetCreationBuffer;
mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
mAnnouncingCallback, sharedLog);
mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback,
sharedLog);
mSharedLog = sharedLog;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
@@ -346,7 +353,7 @@
public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) {
final MdnsPacket packet;
try {
- packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length));
+ packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length, mMdnsFeatureFlags));
} catch (MdnsPacket.ParseException e) {
mSharedLog.e("Error parsing mDNS packet", e);
if (DBG) {
@@ -370,7 +377,7 @@
// happen when the incoming packet has answer records (not a question), so there will be no
// answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
// conflicting service is still probing and won't reply either.
- final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
+ final MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
if (answers == null) return;
mReplySender.queueReply(answers);
@@ -388,12 +395,13 @@
* @param serviceId The serviceId.
* @return the raw offload payload
*/
+ @NonNull
public byte[] getRawOffloadPayload(int serviceId) {
try {
- return MdnsUtils.createRawDnsPacket(mReplySender.getPacketCreationBuffer(),
+ return MdnsUtils.createRawDnsPacket(mPacketCreationBuffer,
mRecordRepository.getOffloadPacket(serviceId));
} catch (IOException | IllegalArgumentException e) {
- mSharedLog.wtf("Cannot create rawOffloadPacket: " + e.getMessage());
+ mSharedLog.wtf("Cannot create rawOffloadPacket: ", e);
return new byte[0];
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
index 4ba6912..e7b0eaa 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -50,6 +50,7 @@
@NonNull private final Handler mHandler;
@NonNull private final MdnsSocketProvider mSocketProvider;
@NonNull private final SharedLog mSharedLog;
+ @NonNull private final MdnsFeatureFlags mMdnsFeatureFlags;
private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mSocketRequests =
new ArrayMap<>();
@@ -58,11 +59,12 @@
private int mReceivedPacketNumber = 0;
public MdnsMultinetworkSocketClient(@NonNull Looper looper,
- @NonNull MdnsSocketProvider provider,
- @NonNull SharedLog sharedLog) {
+ @NonNull MdnsSocketProvider provider, @NonNull SharedLog sharedLog,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mHandler = new Handler(looper);
mSocketProvider = provider;
mSharedLog = sharedLog;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
private class InterfaceSocketCallback implements MdnsSocketProvider.SocketCallback {
@@ -239,7 +241,7 @@
final MdnsPacket response;
try {
- response = MdnsResponseDecoder.parseResponse(recvbuf, length);
+ response = MdnsResponseDecoder.parseResponse(recvbuf, length, mMdnsFeatureFlags);
} catch (MdnsPacket.ParseException e) {
if (e.code != MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE) {
mSharedLog.e(e.getMessage(), e);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
index 7fa3f84..1fabd49 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacket.java
@@ -31,6 +31,7 @@
public class MdnsPacket {
private static final String TAG = MdnsPacket.class.getSimpleName();
+ public final int transactionId;
public final int flags;
@NonNull
public final List<MdnsRecord> questions;
@@ -46,6 +47,15 @@
@NonNull List<MdnsRecord> answers,
@NonNull List<MdnsRecord> authorityRecords,
@NonNull List<MdnsRecord> additionalRecords) {
+ this(0, flags, questions, answers, authorityRecords, additionalRecords);
+ }
+
+ MdnsPacket(int transactionId, int flags,
+ @NonNull List<MdnsRecord> questions,
+ @NonNull List<MdnsRecord> answers,
+ @NonNull List<MdnsRecord> authorityRecords,
+ @NonNull List<MdnsRecord> additionalRecords) {
+ this.transactionId = transactionId;
this.flags = flags;
this.questions = Collections.unmodifiableList(questions);
this.answers = Collections.unmodifiableList(answers);
@@ -70,15 +80,16 @@
*/
@NonNull
public static MdnsPacket parse(@NonNull MdnsPacketReader reader) throws ParseException {
+ final int transactionId;
final int flags;
try {
- reader.readUInt16(); // transaction ID (not used)
+ transactionId = reader.readUInt16();
flags = reader.readUInt16();
} catch (EOFException e) {
throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
"Reached the end of the mDNS response unexpectedly.", e);
}
- return parseRecordsSection(reader, flags);
+ return parseRecordsSection(reader, flags, transactionId);
}
/**
@@ -86,8 +97,8 @@
*
* The records section starts with the questions count, just after the packet flags.
*/
- public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags)
- throws ParseException {
+ public static MdnsPacket parseRecordsSection(@NonNull MdnsPacketReader reader, int flags,
+ int transactionId) throws ParseException {
try {
final int numQuestions = reader.readUInt16();
final int numAnswers = reader.readUInt16();
@@ -99,7 +110,7 @@
final ArrayList<MdnsRecord> authority = parseRecords(reader, numAuthority, false);
final ArrayList<MdnsRecord> additional = parseRecords(reader, numAdditional, false);
- return new MdnsPacket(flags, questions, answers, authority, additional);
+ return new MdnsPacket(transactionId, flags, questions, answers, authority, additional);
} catch (EOFException e) {
throw new ParseException(MdnsResponseErrorCode.ERROR_END_OF_FILE,
"Reached the end of the mDNS response unexpectedly.", e);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
index aa38844..4917188 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.SparseArray;
@@ -33,21 +34,23 @@
private final byte[] buf;
private final int count;
private final SparseArray<LabelEntry> labelDictionary;
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
private int pos;
private int limit;
/** Constructs a reader for the given packet. */
public MdnsPacketReader(DatagramPacket packet) {
- this(packet.getData(), packet.getLength());
+ this(packet.getData(), packet.getLength(), MdnsFeatureFlags.newBuilder().build());
}
/** Constructs a reader for the given packet. */
- public MdnsPacketReader(byte[] buffer, int length) {
+ public MdnsPacketReader(byte[] buffer, int length, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
buf = buffer;
count = length;
pos = 0;
limit = -1;
labelDictionary = new SparseArray<>(16);
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
@@ -269,4 +272,4 @@
this.label = label;
}
}
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index fd0f5c9..e84cead 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -16,8 +16,8 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV4_ADDR;
-import static com.android.server.connectivity.mdns.MdnsRecordRepository.IPV6_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,9 +38,8 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public abstract class MdnsPacketRepeater<T extends MdnsPacketRepeater.Request> {
- private static final boolean DBG = MdnsAdvertiser.DBG;
private static final InetSocketAddress[] ALL_ADDRS = new InetSocketAddress[] {
- IPV4_ADDR, IPV6_ADDR
+ IPV4_SOCKET_ADDR, IPV6_SOCKET_ADDR
};
@NonNull
@@ -51,6 +50,7 @@
private final PacketRepeaterCallback<T> mCb;
@NonNull
private final SharedLog mSharedLog;
+ private final boolean mEnableDebugLog;
/**
* Status callback from {@link MdnsPacketRepeater}.
@@ -111,7 +111,7 @@
}
final MdnsPacket packet = request.getPacket(index);
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Sending packets for iteration " + index + " out of "
+ request.getNumSends() + " for ID " + msg.what);
}
@@ -134,7 +134,7 @@
// likely not to be available since the device is in deep sleep anyway.
final long delay = request.getDelayMs(nextIndex);
sendMessageDelayed(obtainMessage(msg.what, nextIndex, 0, request), delay);
- if (DBG) mSharedLog.v("Scheduled next packet in " + delay + "ms");
+ if (mEnableDebugLog) mSharedLog.v("Scheduled next packet in " + delay + "ms");
}
// Call onSent after scheduling the next run, to allow the callback to cancel it
@@ -145,15 +145,17 @@
}
protected MdnsPacketRepeater(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
- @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog) {
+ @Nullable PacketRepeaterCallback<T> cb, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog) {
mHandler = new ProbeHandler(looper);
mReplySender = replySender;
mCb = cb;
mSharedLog = sharedLog;
+ mEnableDebugLog = enableDebugLog;
}
protected void startSending(int id, @NonNull T request, long initialDelayMs) {
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Starting send with id " + id + ", request "
+ request.getClass().getSimpleName() + ", delay " + initialDelayMs);
}
@@ -172,7 +174,7 @@
// all in the handler queue; unless this method is called from a message, but the current
// message cannot be cancelled.
if (mHandler.hasMessages(id)) {
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Stopping send on id " + id);
}
mHandler.removeMessages(id);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
index c88ead0..41cc380 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPointerRecord.java
@@ -18,14 +18,15 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.Arrays;
/** An mDNS "PTR" record, which holds a name (the "pointer"). */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsPointerRecord extends MdnsRecord {
private String[] pointer;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
index f2b562a..e88947a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsProber.java
@@ -40,9 +40,8 @@
private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
public MdnsProber(@NonNull Looper looper, @NonNull MdnsReplySender replySender,
- @NonNull PacketRepeaterCallback<ProbingInfo> cb,
- @NonNull SharedLog sharedLog) {
- super(looper, replySender, cb, sharedLog);
+ @NonNull PacketRepeaterCallback<ProbingInfo> cb, @NonNull SharedLog sharedLog) {
+ super(looper, replySender, cb, sharedLog, MdnsAdvertiser.DBG);
}
/** Probing request to send with {@link MdnsProber}. */
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index f532372..e34778f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV4_SOCKET_ADDR;
+import static com.android.server.connectivity.mdns.MdnsConstants.IPV6_SOCKET_ADDR;
import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET;
import android.annotation.NonNull;
@@ -79,11 +81,6 @@
private static final String[] DNS_SD_SERVICE_TYPE =
new String[] { "_services", "_dns-sd", "_udp", LOCAL_TLD };
- public static final InetSocketAddress IPV6_ADDR = new InetSocketAddress(
- MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT);
- public static final InetSocketAddress IPV4_ADDR = new InetSocketAddress(
- MdnsConstants.getMdnsIPv4Address(), MdnsConstants.MDNS_PORT);
-
@NonNull
private final Random mDelayGenerator = new Random();
// Map of service unique ID -> records for service
@@ -95,16 +92,19 @@
private final Looper mLooper;
@NonNull
private final String[] mDeviceHostname;
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
- public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname) {
- this(looper, new Dependencies(), deviceHostname);
+ public MdnsRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostname,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, new Dependencies(), deviceHostname, mdnsFeatureFlags);
}
@VisibleForTesting
public MdnsRecordRepository(@NonNull Looper looper, @NonNull Dependencies deps,
- @NonNull String[] deviceHostname) {
+ @NonNull String[] deviceHostname, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
mDeviceHostname = deviceHostname;
mLooper = looper;
+ mMdnsFeatureFlags = mdnsFeatureFlags;
}
/**
@@ -133,11 +133,6 @@
public final boolean isSharedName;
/**
- * Whether probing is still in progress for the record.
- */
- public boolean isProbing;
-
- /**
* Last time (as per SystemClock.elapsedRealtime) when advertised via multicast, 0 if never
*/
public long lastAdvertisedTimeMs;
@@ -148,12 +143,10 @@
*/
public long lastSentTimeMs;
- RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName,
- boolean probing) {
+ RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
this.serviceInfo = serviceInfo;
this.record = record;
this.isSharedName = sharedName;
- this.isProbing = probing;
}
}
@@ -187,6 +180,11 @@
public int sentPacketCount = NO_PACKET;
/**
+ * Whether probing is still in progress.
+ */
+ private boolean isProbing;
+
+ /**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*
* @param deviceHostname Hostname of the device (for the interface used)
@@ -209,7 +207,7 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
- true /* sharedName */, true /* probing */);
+ true /* sharedName */);
if (subtype == null) {
this.ptrRecords = Collections.singletonList(ptrRecord);
@@ -226,7 +224,7 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceName),
- true /* sharedName */, true /* probing */);
+ true /* sharedName */);
this.ptrRecords = List.of(ptrRecord, subtypeRecord);
}
@@ -239,7 +237,7 @@
NAME_RECORDS_TTL_MILLIS, 0 /* servicePriority */, 0 /* serviceWeight */,
serviceInfo.getPort(),
deviceHostname),
- false /* sharedName */, true /* probing */);
+ false /* sharedName */);
txtRecord = new RecordInfo<>(
serviceInfo,
@@ -248,7 +246,7 @@
true /* cacheFlush */, // Service name is verified unique after probing
NON_NAME_RECORDS_TTL_MILLIS,
attrsToTextEntries(serviceInfo.getAttributes())),
- false /* sharedName */, true /* probing */);
+ false /* sharedName */);
final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
allRecords.addAll(ptrRecords);
@@ -263,18 +261,18 @@
false /* cacheFlush */,
NON_NAME_RECORDS_TTL_MILLIS,
serviceType),
- true /* sharedName */, true /* probing */));
+ true /* sharedName */));
this.allRecords = Collections.unmodifiableList(allRecords);
this.repliedServiceCount = repliedServiceCount;
this.sentPacketCount = sentPacketCount;
+ this.isProbing = true;
}
void setProbing(boolean probing) {
- for (RecordInfo<?> info : allRecords) {
- info.isProbing = probing;
- }
+ this.isProbing = probing;
}
+
}
/**
@@ -292,7 +290,7 @@
true /* cacheFlush */,
NAME_RECORDS_TTL_MILLIS,
mDeviceHostname),
- false /* sharedName */, false /* probing */));
+ false /* sharedName */));
mGeneralRecords.add(new RecordInfo<>(
null /* serviceInfo */,
@@ -302,7 +300,7 @@
true /* cacheFlush */,
NAME_RECORDS_TTL_MILLIS,
addr.getAddress()),
- false /* sharedName */, false /* probing */));
+ false /* sharedName */));
}
}
@@ -354,7 +352,8 @@
}
private MdnsProber.ProbingInfo makeProbingInfo(int serviceId,
- @NonNull MdnsServiceRecord srvRecord) {
+ @NonNull MdnsServiceRecord srvRecord,
+ @NonNull List<MdnsInetAddressRecord> inetAddressRecords) {
final List<MdnsRecord> probingRecords = new ArrayList<>();
// Probe with cacheFlush cleared; it is set when announcing, as it was verified unique:
// RFC6762 10.2
@@ -366,6 +365,15 @@
srvRecord.getServicePort(),
srvRecord.getServiceHost()));
+ for (MdnsInetAddressRecord inetAddressRecord : inetAddressRecords) {
+ probingRecords.add(new MdnsInetAddressRecord(inetAddressRecord.getName(),
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ inetAddressRecord.getTtl(),
+ inetAddressRecord.getInet4Address() == null
+ ? inetAddressRecord.getInet6Address()
+ : inetAddressRecord.getInet4Address()));
+ }
return new MdnsProber.ProbingInfo(serviceId, probingRecords);
}
@@ -455,44 +463,13 @@
}
/**
- * Info about a reply to be sent.
- */
- public static class ReplyInfo {
- @NonNull
- public final List<MdnsRecord> answers;
- @NonNull
- public final List<MdnsRecord> additionalAnswers;
- public final long sendDelayMs;
- @NonNull
- public final InetSocketAddress destination;
-
- public ReplyInfo(
- @NonNull List<MdnsRecord> answers,
- @NonNull List<MdnsRecord> additionalAnswers,
- long sendDelayMs,
- @NonNull InetSocketAddress destination) {
- this.answers = answers;
- this.additionalAnswers = additionalAnswers;
- this.sendDelayMs = sendDelayMs;
- this.destination = destination;
- }
-
- @Override
- public String toString() {
- return "{ReplyInfo to " + destination + ", answers: " + answers.size()
- + ", additionalAnswers: " + additionalAnswers.size()
- + ", sendDelayMs " + sendDelayMs + "}";
- }
- }
-
- /**
* Get the reply to send to an incoming packet.
*
* @param packet The incoming packet.
* @param src The source address of the incoming packet.
*/
@Nullable
- public ReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
+ public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
@@ -506,7 +483,7 @@
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
- if (registration.exiting) continue;
+ if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
answerInfo, additionalAnswerRecords)) {
@@ -543,9 +520,9 @@
if (replyUnicast) {
dest = src;
} else if (src.getAddress() instanceof Inet4Address) {
- dest = IPV4_ADDR;
+ dest = IPV4_SOCKET_ADDR;
} else {
- dest = IPV6_ADDR;
+ dest = IPV6_SOCKET_ADDR;
}
// Build the list of answer records from their RecordInfo
@@ -559,7 +536,7 @@
answerRecords.add(info.record);
}
- return new ReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
+ return new MdnsReplyInfo(answerRecords, additionalAnswerRecords, delayMs, dest);
}
/**
@@ -579,7 +556,6 @@
final int answersStartIndex = answerInfo.size();
for (RecordInfo<?> info : serviceRecords) {
- if (info.isProbing) continue;
/* RFC6762 6.: the record name must match the question name, the record rrtype
must match the question qtype unless the qtype is "ANY" (255) or the rrtype is
@@ -858,6 +834,18 @@
return conflicting;
}
+ private List<MdnsInetAddressRecord> makeProbingInetAddressRecords() {
+ final List<MdnsInetAddressRecord> records = new ArrayList<>();
+ if (mMdnsFeatureFlags.mIncludeInetAddressRecordsInProbing) {
+ for (RecordInfo<?> record : mGeneralRecords) {
+ if (record.record instanceof MdnsInetAddressRecord) {
+ records.add((MdnsInetAddressRecord) record.record);
+ }
+ }
+ }
+ return records;
+ }
+
/**
* (Re)set a service to the probing state.
* @return The {@link MdnsProber.ProbingInfo} to send for probing.
@@ -868,7 +856,8 @@
if (registration == null) return null;
registration.setProbing(true);
- return makeProbingInfo(serviceId, registration.srvRecord.record);
+ return makeProbingInfo(
+ serviceId, registration.srvRecord.record, makeProbingInetAddressRecords());
}
/**
@@ -878,7 +867,7 @@
final ServiceRegistration registration = mServices.get(serviceId);
if (registration == null) return false;
- return registration.srvRecord.isProbing;
+ return registration.isProbing;
}
/**
@@ -904,7 +893,8 @@
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
mServices.put(serviceId, newService);
- return makeProbingInfo(serviceId, newService.srvRecord.record);
+ return makeProbingInfo(
+ serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
}
/**
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
new file mode 100644
index 0000000..ce61b54
--- /dev/null
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplyInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.mdns;
+
+import android.annotation.NonNull;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+/**
+ * Info about a mDNS reply to be sent.
+ */
+public final class MdnsReplyInfo {
+ @NonNull
+ public final List<MdnsRecord> answers;
+ @NonNull
+ public final List<MdnsRecord> additionalAnswers;
+ public final long sendDelayMs;
+ @NonNull
+ public final InetSocketAddress destination;
+
+ public MdnsReplyInfo(
+ @NonNull List<MdnsRecord> answers,
+ @NonNull List<MdnsRecord> additionalAnswers,
+ long sendDelayMs,
+ @NonNull InetSocketAddress destination) {
+ this.answers = answers;
+ this.additionalAnswers = additionalAnswers;
+ this.sendDelayMs = sendDelayMs;
+ this.destination = destination;
+ }
+
+ @Override
+ public String toString() {
+ return "{MdnsReplyInfo to " + destination + ", answers: " + answers.size()
+ + ", additionalAnswers: " + additionalAnswers.size()
+ + ", sendDelayMs " + sendDelayMs + "}";
+ }
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index 3d64b5a..ea3af5e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -26,7 +26,6 @@
import android.os.Message;
import com.android.net.module.util.SharedLog;
-import com.android.server.connectivity.mdns.MdnsRecordRepository.ReplyInfo;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -45,7 +44,6 @@
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class MdnsReplySender {
- private static final boolean DBG = MdnsAdvertiser.DBG;
private static final int MSG_SEND = 1;
private static final int PACKET_NOT_SENT = 0;
private static final int PACKET_SENT = 1;
@@ -58,24 +56,27 @@
private final byte[] mPacketCreationBuffer;
@NonNull
private final SharedLog mSharedLog;
+ private final boolean mEnableDebugLog;
public MdnsReplySender(@NonNull Looper looper, @NonNull MdnsInterfaceSocket socket,
- @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog) {
+ @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog,
+ boolean enableDebugLog) {
mHandler = new SendHandler(looper);
mSocket = socket;
mPacketCreationBuffer = packetCreationBuffer;
mSharedLog = sharedLog;
+ mEnableDebugLog = enableDebugLog;
}
/**
* Queue a reply to be sent when its send delay expires.
*/
- public void queueReply(@NonNull ReplyInfo reply) {
+ public void queueReply(@NonNull MdnsReplyInfo reply) {
ensureRunningOnHandlerThread(mHandler);
// TODO: implement response aggregation (RFC 6762 6.4)
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SEND, reply), reply.sendDelayMs);
- if (DBG) {
+ if (mEnableDebugLog) {
mSharedLog.v("Scheduling " + reply);
}
}
@@ -98,11 +99,6 @@
return PACKET_SENT;
}
- /** Get the packetCreationBuffer */
- public byte[] getPacketCreationBuffer() {
- return mPacketCreationBuffer;
- }
-
/**
* Cancel all pending sends.
*/
@@ -118,8 +114,8 @@
@Override
public void handleMessage(@NonNull Message msg) {
- final ReplyInfo replyInfo = (ReplyInfo) msg.obj;
- if (DBG) mSharedLog.v("Sending " + replyInfo);
+ final MdnsReplyInfo replyInfo = (MdnsReplyInfo) msg.obj;
+ if (mEnableDebugLog) mSharedLog.v("Sending " + replyInfo);
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
final MdnsPacket packet = new MdnsPacket(flags,
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
index e2288c1..05ad1be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -33,6 +33,7 @@
/** An mDNS response. */
public class MdnsResponse {
+ public static final long EXPIRATION_NEVER = Long.MAX_VALUE;
private final List<MdnsRecord> records;
private final List<MdnsPointerRecord> pointerRecords;
private MdnsServiceRecord serviceRecord;
@@ -349,6 +350,21 @@
return serviceName;
}
+ /** Get the min remaining ttl time from received records */
+ public long getMinRemainingTtl(long now) {
+ long minRemainingTtl = EXPIRATION_NEVER;
+ // TODO: Check other records(A, AAAA, TXT) ttl time.
+ if (!hasServiceRecord()) {
+ return EXPIRATION_NEVER;
+ }
+ // Check ttl time.
+ long remainingTtl = serviceRecord.getRemainingTTL(now);
+ if (remainingTtl < minRemainingTtl) {
+ minRemainingTtl = remainingTtl;
+ }
+ return minRemainingTtl;
+ }
+
/**
* Tests if this response is a goodbye message. This will be true if a service record is present
* and any of the records have a TTL of 0.
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
index a3cc0eb..b812bb4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -84,20 +84,20 @@
* @throws MdnsPacket.ParseException if a response packet could not be parsed.
*/
@NonNull
- public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length)
- throws MdnsPacket.ParseException {
- MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length);
+ public static MdnsPacket parseResponse(@NonNull byte[] recvbuf, int length,
+ @NonNull MdnsFeatureFlags mdnsFeatureFlags) throws MdnsPacket.ParseException {
+ final MdnsPacketReader reader = new MdnsPacketReader(recvbuf, length, mdnsFeatureFlags);
final MdnsPacket mdnsPacket;
try {
- reader.readUInt16(); // transaction ID (not used)
+ final int transactionId = reader.readUInt16();
int flags = reader.readUInt16();
if ((flags & MdnsConstants.FLAGS_RESPONSE_MASK) != MdnsConstants.FLAGS_RESPONSE) {
throw new MdnsPacket.ParseException(
MdnsResponseErrorCode.ERROR_NOT_RESPONSE_MESSAGE, "Not a response", null);
}
- mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags);
+ mdnsPacket = MdnsPacket.parseRecordsSection(reader, flags, transactionId);
if (mdnsPacket.answers.size() < 1) {
throw new MdnsPacket.ParseException(
MdnsResponseErrorCode.ERROR_NO_ANSWERS, "Response has no answers",
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
index ec6af9b..e9a41d1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceCache.java
@@ -16,16 +16,22 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsResponse.EXPIRATION_NEVER;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.mdns.util.MdnsUtils.equalsIgnoreDnsCase;
import static com.android.server.connectivity.mdns.util.MdnsUtils.toDnsLowerCase;
+import static java.lang.Math.min;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -42,7 +48,7 @@
* to their default value (0, false or null).
*/
public class MdnsServiceCache {
- private static class CacheKey {
+ static class CacheKey {
@NonNull final String mLowercaseServiceType;
@NonNull final SocketKey mSocketKey;
@@ -67,32 +73,54 @@
}
}
/**
- * A map of cached services. Key is composed of service name, type and socket. Value is the
- * service which use the service type to discover from each socket.
+ * A map of cached services. Key is composed of service type and socket. Value is the list of
+ * services which are discovered from the given CacheKey.
+ * When the MdnsFeatureFlags#NSD_EXPIRED_SERVICES_REMOVAL flag is enabled, the lists are sorted
+ * by expiration time, with the earliest entries appearing first. This sorting allows the
+ * removal process to progress through the expiration check efficiently.
*/
@NonNull
private final ArrayMap<CacheKey, List<MdnsResponse>> mCachedServices = new ArrayMap<>();
+ /**
+ * A map of service expire callbacks. Key is composed of service type and socket and value is
+ * the callback listener.
+ */
+ @NonNull
+ private final ArrayMap<CacheKey, ServiceExpiredCallback> mCallbacks = new ArrayMap<>();
@NonNull
private final Handler mHandler;
+ @NonNull
+ private final MdnsFeatureFlags mMdnsFeatureFlags;
+ @NonNull
+ private final MdnsUtils.Clock mClock;
+ private long mNextExpirationTime = EXPIRATION_NEVER;
- public MdnsServiceCache(@NonNull Looper looper) {
+ public MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
+ this(looper, mdnsFeatureFlags, new MdnsUtils.Clock());
+ }
+
+ @VisibleForTesting
+ MdnsServiceCache(@NonNull Looper looper, @NonNull MdnsFeatureFlags mdnsFeatureFlags,
+ @NonNull MdnsUtils.Clock clock) {
mHandler = new Handler(looper);
+ mMdnsFeatureFlags = mdnsFeatureFlags;
+ mClock = clock;
}
/**
* Get the cache services which are queried from given service type and socket.
*
- * @param serviceType the target service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @return the set of services which matches the given service type.
*/
@NonNull
- public List<MdnsResponse> getCachedServices(@NonNull String serviceType,
- @NonNull SocketKey socketKey) {
+ public List<MdnsResponse> getCachedServices(@NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final CacheKey key = new CacheKey(serviceType, socketKey);
- return mCachedServices.containsKey(key)
- ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(key)))
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+ }
+ return mCachedServices.containsKey(cacheKey)
+ ? Collections.unmodifiableList(new ArrayList<>(mCachedServices.get(cacheKey)))
: Collections.emptyList();
}
@@ -117,16 +145,16 @@
* Get the cache service.
*
* @param serviceName the target service name.
- * @param serviceType the target service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @return the service which matches given conditions.
*/
@Nullable
- public MdnsResponse getCachedService(@NonNull String serviceName,
- @NonNull String serviceType, @NonNull SocketKey socketKey) {
+ public MdnsResponse getCachedService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses =
- mCachedServices.get(new CacheKey(serviceType, socketKey));
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ maybeRemoveExpiredServices(cacheKey, mClock.elapsedRealtime());
+ }
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
}
@@ -134,51 +162,188 @@
return response != null ? new MdnsResponse(response) : null;
}
+ static void insertResponseAndSortList(
+ List<MdnsResponse> responses, MdnsResponse response, long now) {
+ // binarySearch returns "the index of the search key, if it is contained in the list;
+ // otherwise, (-(insertion point) - 1)"
+ final int searchRes = Collections.binarySearch(responses, response,
+ // Sort the list by ttl.
+ (o1, o2) -> Long.compare(o1.getMinRemainingTtl(now), o2.getMinRemainingTtl(now)));
+ responses.add(searchRes >= 0 ? searchRes : (-searchRes - 1), response);
+ }
+
/**
* Add or update a service.
*
- * @param serviceType the service type.
- * @param socketKey the target socket
+ * @param cacheKey the target CacheKey.
* @param response the response of the discovered service.
*/
- public void addOrUpdateService(@NonNull String serviceType, @NonNull SocketKey socketKey,
- @NonNull MdnsResponse response) {
+ public void addOrUpdateService(@NonNull CacheKey cacheKey, @NonNull MdnsResponse response) {
ensureRunningOnHandlerThread(mHandler);
final List<MdnsResponse> responses = mCachedServices.computeIfAbsent(
- new CacheKey(serviceType, socketKey), key -> new ArrayList<>());
+ cacheKey, key -> new ArrayList<>());
// Remove existing service if present.
final MdnsResponse existing =
findMatchedResponse(responses, response.getServiceInstanceName());
responses.remove(existing);
- responses.add(response);
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ final long now = mClock.elapsedRealtime();
+ // Insert and sort service
+ insertResponseAndSortList(responses, response, now);
+ // Update the next expiration check time when a new service is added.
+ mNextExpirationTime = getNextExpirationTime(now);
+ } else {
+ responses.add(response);
+ }
}
/**
* Remove a service which matches the given service name, type and socket.
*
* @param serviceName the target service name.
- * @param serviceType the target service type.
- * @param socketKey the target socket.
+ * @param cacheKey the target CacheKey.
*/
@Nullable
- public MdnsResponse removeService(@NonNull String serviceName, @NonNull String serviceType,
- @NonNull SocketKey socketKey) {
+ public MdnsResponse removeService(@NonNull String serviceName, @NonNull CacheKey cacheKey) {
ensureRunningOnHandlerThread(mHandler);
- final List<MdnsResponse> responses =
- mCachedServices.get(new CacheKey(serviceType, socketKey));
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
if (responses == null) {
return null;
}
final Iterator<MdnsResponse> iterator = responses.iterator();
+ MdnsResponse removedResponse = null;
while (iterator.hasNext()) {
final MdnsResponse response = iterator.next();
if (equalsIgnoreDnsCase(serviceName, response.getServiceInstanceName())) {
iterator.remove();
- return response;
+ removedResponse = response;
+ break;
}
}
- return null;
+
+ if (mMdnsFeatureFlags.mIsExpiredServicesRemovalEnabled) {
+ // Remove the serviceType if no response.
+ if (responses.isEmpty()) {
+ mCachedServices.remove(cacheKey);
+ }
+ // Update the next expiration check time when a service is removed.
+ mNextExpirationTime = getNextExpirationTime(mClock.elapsedRealtime());
+ }
+ return removedResponse;
}
- // TODO: check ttl expiration for each service and notify to the clients.
+ /**
+ * Register a callback to listen to service expiration.
+ *
+ * <p> Registering the same callback instance twice is a no-op, since MdnsServiceTypeClient
+ * relies on this.
+ *
+ * @param cacheKey the target CacheKey.
+ * @param callback the callback that notify the service is expired.
+ */
+ public void registerServiceExpiredCallback(@NonNull CacheKey cacheKey,
+ @NonNull ServiceExpiredCallback callback) {
+ ensureRunningOnHandlerThread(mHandler);
+ mCallbacks.put(cacheKey, callback);
+ }
+
+ /**
+ * Unregister the service expired callback.
+ *
+ * @param cacheKey the CacheKey that is registered to listen service expiration before.
+ */
+ public void unregisterServiceExpiredCallback(@NonNull CacheKey cacheKey) {
+ ensureRunningOnHandlerThread(mHandler);
+ mCallbacks.remove(cacheKey);
+ }
+
+ private void notifyServiceExpired(@NonNull CacheKey cacheKey,
+ @NonNull MdnsResponse previousResponse, @Nullable MdnsResponse newResponse) {
+ final ServiceExpiredCallback callback = mCallbacks.get(cacheKey);
+ if (callback == null) {
+ // The cached service is no listener.
+ return;
+ }
+ mHandler.post(()-> callback.onServiceRecordExpired(previousResponse, newResponse));
+ }
+
+ static List<MdnsResponse> removeExpiredServices(@NonNull List<MdnsResponse> responses,
+ long now) {
+ final List<MdnsResponse> removedResponses = new ArrayList<>();
+ final Iterator<MdnsResponse> iterator = responses.iterator();
+ while (iterator.hasNext()) {
+ final MdnsResponse response = iterator.next();
+ // TODO: Check other records (A, AAAA, TXT) ttl time and remove the record if it's
+ // expired. Then send service update notification.
+ if (!response.hasServiceRecord() || response.getMinRemainingTtl(now) > 0) {
+ // The responses are sorted by the service record ttl time. Break out of loop
+ // early if service is not expired or no service record.
+ break;
+ }
+ // Remove the ttl expired service.
+ iterator.remove();
+ removedResponses.add(response);
+ }
+ return removedResponses;
+ }
+
+ private long getNextExpirationTime(long now) {
+ if (mCachedServices.isEmpty()) {
+ return EXPIRATION_NEVER;
+ }
+
+ long minRemainingTtl = EXPIRATION_NEVER;
+ for (int i = 0; i < mCachedServices.size(); i++) {
+ minRemainingTtl = min(minRemainingTtl,
+ // The empty lists are not kept in the map, so there's always at least one
+ // element in the list. Therefore, it's fine to get the first element without a
+ // null check.
+ mCachedServices.valueAt(i).get(0).getMinRemainingTtl(now));
+ }
+ return minRemainingTtl == EXPIRATION_NEVER ? EXPIRATION_NEVER : now + minRemainingTtl;
+ }
+
+ /**
+ * Check whether the ttl time is expired on each service and notify to the listeners
+ */
+ private void maybeRemoveExpiredServices(CacheKey cacheKey, long now) {
+ ensureRunningOnHandlerThread(mHandler);
+ if (now < mNextExpirationTime) {
+ // Skip the check if ttl time is not expired.
+ return;
+ }
+
+ final List<MdnsResponse> responses = mCachedServices.get(cacheKey);
+ if (responses == null) {
+ // No such services.
+ return;
+ }
+
+ final List<MdnsResponse> removedResponses = removeExpiredServices(responses, now);
+ if (removedResponses.isEmpty()) {
+ // No expired services.
+ return;
+ }
+
+ for (MdnsResponse previousResponse : removedResponses) {
+ notifyServiceExpired(cacheKey, previousResponse, null /* newResponse */);
+ }
+
+ // Remove the serviceType if no response.
+ if (responses.isEmpty()) {
+ mCachedServices.remove(cacheKey);
+ }
+
+ // Update next expiration time.
+ mNextExpirationTime = getNextExpirationTime(now);
+ }
+
+ /*** Callbacks for listening service expiration */
+ public interface ServiceExpiredCallback {
+ /*** Notify the service is expired */
+ void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+ @Nullable MdnsResponse newResponse);
+ }
+
+ // TODO: Schedule a job to check ttl expiration for all services and notify to the clients.
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
index f851b35..4d407be 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceRecord.java
@@ -18,7 +18,8 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
@@ -27,7 +28,7 @@
import java.util.Objects;
/** An mDNS "SRV" record, which contains service information. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsServiceRecord extends MdnsRecord {
public static final int PROTO_NONE = 0;
public static final int PROTO_TCP = 1;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index bbe8f4c..32f604e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -16,6 +16,7 @@
package com.android.server.connectivity.mdns;
+import static com.android.server.connectivity.mdns.MdnsServiceCache.ServiceExpiredCallback;
import static com.android.server.connectivity.mdns.MdnsServiceCache.findMatchedResponse;
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
@@ -71,6 +72,15 @@
* The service caches for each socket. It should be accessed from looper thread only.
*/
@NonNull private final MdnsServiceCache serviceCache;
+ @NonNull private final MdnsServiceCache.CacheKey cacheKey;
+ @NonNull private final ServiceExpiredCallback serviceExpiredCallback =
+ new ServiceExpiredCallback() {
+ @Override
+ public void onServiceRecordExpired(@NonNull MdnsResponse previousResponse,
+ @Nullable MdnsResponse newResponse) {
+ notifyRemovedServiceToListeners(previousResponse, "Service record expired");
+ }
+ };
private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
new ArrayMap<>();
private final boolean removeServiceAfterTtlExpires =
@@ -225,6 +235,16 @@
this.dependencies = dependencies;
this.serviceCache = serviceCache;
this.mdnsQueryScheduler = new MdnsQueryScheduler();
+ this.cacheKey = new MdnsServiceCache.CacheKey(serviceType, socketKey);
+ }
+
+ /**
+ * Do the cleanup of the MdnsServiceTypeClient
+ */
+ private void shutDown() {
+ removeScheduledTask();
+ mdnsQueryScheduler.cancelScheduledRun();
+ serviceCache.unregisterServiceExpiredCallback(cacheKey);
}
private static MdnsServiceInfo buildMdnsServiceInfoFromResponse(
@@ -292,8 +312,7 @@
this.searchOptions = searchOptions;
boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
- for (MdnsResponse existingResponse :
- serviceCache.getCachedServices(serviceType, socketKey)) {
+ for (MdnsResponse existingResponse : serviceCache.getCachedServices(cacheKey)) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
final MdnsServiceInfo info =
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
@@ -341,6 +360,8 @@
servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
executor.submit(queryTask);
}
+
+ serviceCache.registerServiceExpiredCallback(cacheKey, serviceExpiredCallback);
}
/**
@@ -390,8 +411,7 @@
return listeners.isEmpty();
}
if (listeners.isEmpty()) {
- removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
+ shutDown();
}
return listeners.isEmpty();
}
@@ -404,8 +424,7 @@
ensureRunningOnHandlerThread(handler);
// Augment the list of current known responses, and generated responses for resolve
// requests if there is no known response
- final List<MdnsResponse> cachedList =
- serviceCache.getCachedServices(serviceType, socketKey);
+ final List<MdnsResponse> cachedList = serviceCache.getCachedServices(cacheKey);
final List<MdnsResponse> currentList = new ArrayList<>(cachedList);
List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey);
for (MdnsResponse additionalResponse : additionalResponses) {
@@ -432,7 +451,7 @@
} else if (findMatchedResponse(cachedList, serviceInstanceName) != null) {
// If the response is not modified and already in the cache. The cache will
// need to be updated to refresh the last receipt time.
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
}
}
if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)) {
@@ -458,44 +477,50 @@
}
}
- /** Notify all services are removed because the socket is destroyed. */
- public void notifySocketDestroyed() {
- ensureRunningOnHandlerThread(handler);
- for (MdnsResponse response : serviceCache.getCachedServices(serviceType, socketKey)) {
- final String name = response.getServiceInstanceName();
- if (name == null) continue;
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- final MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
+ private void notifyRemovedServiceToListeners(@NonNull MdnsResponse response,
+ @NonNull String message) {
+ for (int i = 0; i < listeners.size(); i++) {
+ if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
+ final MdnsServiceBrowserListener listener = listeners.keyAt(i);
+ if (response.getServiceInstanceName() != null) {
+ final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
+ response, serviceTypeLabels);
if (response.isComplete()) {
- sharedLog.log("Socket destroyed. onServiceRemoved: " + name);
+ sharedLog.log(message + ". onServiceRemoved: " + serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
- sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
+ sharedLog.log(message + ". onServiceNameRemoved: " + serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
- removeScheduledTask();
- mdnsQueryScheduler.cancelScheduledRun();
+ }
+
+ /** Notify all services are removed because the socket is destroyed. */
+ public void notifySocketDestroyed() {
+ ensureRunningOnHandlerThread(handler);
+ for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
+ final String name = response.getServiceInstanceName();
+ if (name == null) continue;
+ notifyRemovedServiceToListeners(response, "Socket destroyed");
+ }
+ shutDown();
}
private void onResponseModified(@NonNull MdnsResponse response) {
final String serviceInstanceName = response.getServiceInstanceName();
final MdnsResponse currentResponse =
- serviceCache.getCachedService(serviceInstanceName, serviceType, socketKey);
+ serviceCache.getCachedService(serviceInstanceName, cacheKey);
boolean newServiceFound = false;
boolean serviceBecomesComplete = false;
if (currentResponse == null) {
newServiceFound = true;
if (serviceInstanceName != null) {
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
}
} else {
boolean before = currentResponse.isComplete();
- serviceCache.addOrUpdateService(serviceType, socketKey, response);
+ serviceCache.addOrUpdateService(cacheKey, response);
boolean after = response.isComplete();
serviceBecomesComplete = !before && after;
}
@@ -529,22 +554,11 @@
private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
final MdnsResponse response =
- serviceCache.removeService(serviceInstanceName, serviceType, socketKey);
+ serviceCache.removeService(serviceInstanceName, cacheKey);
if (response == null) {
return;
}
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- final MdnsServiceInfo serviceInfo =
- buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
- if (response.isComplete()) {
- sharedLog.log("onServiceRemoved: " + serviceInfo);
- listener.onServiceRemoved(serviceInfo);
- }
- sharedLog.log("onServiceNameRemoved: " + serviceInfo);
- listener.onServiceNameRemoved(serviceInfo);
- }
+ notifyRemovedServiceToListeners(response, "Goodbye received");
}
private boolean shouldRemoveServiceAfterTtlExpires() {
@@ -567,7 +581,7 @@
continue;
}
MdnsResponse knownResponse =
- serviceCache.getCachedService(resolveName, serviceType, socketKey);
+ serviceCache.getCachedService(resolveName, cacheKey);
if (knownResponse == null) {
final ArrayList<String> instanceFullName = new ArrayList<>(
serviceTypeLabels.length + 1);
@@ -585,36 +599,18 @@
private void tryRemoveServiceAfterTtlExpires() {
if (!shouldRemoveServiceAfterTtlExpires()) return;
- Iterator<MdnsResponse> iter =
- serviceCache.getCachedServices(serviceType, socketKey).iterator();
+ final Iterator<MdnsResponse> iter = serviceCache.getCachedServices(cacheKey).iterator();
while (iter.hasNext()) {
MdnsResponse existingResponse = iter.next();
- final String serviceInstanceName = existingResponse.getServiceInstanceName();
if (existingResponse.hasServiceRecord()
&& existingResponse.getServiceRecord()
.getRemainingTTL(clock.elapsedRealtime()) == 0) {
- serviceCache.removeService(serviceInstanceName, serviceType, socketKey);
- for (int i = 0; i < listeners.size(); i++) {
- if (!responseMatchesOptions(existingResponse, listeners.valueAt(i))) {
- continue;
- }
- final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- if (serviceInstanceName != null) {
- final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
- existingResponse, serviceTypeLabels);
- if (existingResponse.isComplete()) {
- sharedLog.log("TTL expired. onServiceRemoved: " + serviceInfo);
- listener.onServiceRemoved(serviceInfo);
- }
- sharedLog.log("TTL expired. onServiceNameRemoved: " + serviceInfo);
- listener.onServiceNameRemoved(serviceInfo);
- }
- }
+ serviceCache.removeService(existingResponse.getServiceInstanceName(), cacheKey);
+ notifyRemovedServiceToListeners(existingResponse, "TTL expired");
}
}
}
-
private static class QuerySentArguments {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
@@ -672,7 +668,7 @@
private long getMinRemainingTtl(long now) {
long minRemainingTtl = Long.MAX_VALUE;
- for (MdnsResponse response : serviceCache.getCachedServices(serviceType, socketKey)) {
+ for (MdnsResponse response : serviceCache.getCachedServices(cacheKey)) {
if (!response.isComplete()) {
continue;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
index d18a19b..82c8c5b 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -105,9 +105,10 @@
private AtomicInteger packetsCount;
@Nullable private Timer checkMulticastResponseTimer;
private final SharedLog sharedLog;
+ @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
public MdnsSocketClient(@NonNull Context context, @NonNull MulticastLock multicastLock,
- SharedLog sharedLog) {
+ SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
this.sharedLog = sharedLog;
this.context = context;
this.multicastLock = multicastLock;
@@ -116,6 +117,7 @@
} else {
unicastReceiverBuffer = null;
}
+ this.mdnsFeatureFlags = mdnsFeatureFlags;
}
@Override
@@ -454,7 +456,8 @@
final MdnsPacket response;
try {
- response = MdnsResponseDecoder.parseResponse(packet.getData(), packet.getLength());
+ response = MdnsResponseDecoder.parseResponse(
+ packet.getData(), packet.getLength(), mdnsFeatureFlags);
} catch (MdnsPacket.ParseException e) {
sharedLog.w(String.format("Error while decoding %s packet (%d): %d",
responseType, packetNumber, e.code));
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
index 4149dbe..cf6c8ac 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -18,7 +18,8 @@
import android.annotation.Nullable;
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import java.io.IOException;
@@ -28,7 +29,7 @@
import java.util.Objects;
/** An mDNS "TXT" record, which contains a list of {@link TextEntry}. */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class MdnsTextRecord extends MdnsRecord {
private List<TextEntry> entries;
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 0dcc560..4d79f9d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -35,6 +35,7 @@
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -189,7 +190,7 @@
// TODO: support packets over size (send in multiple packets with TC bit set)
final MdnsPacketWriter writer = new MdnsPacketWriter(packetCreationBuffer);
- writer.writeUInt16(0); // Transaction ID (advertisement: 0)
+ writer.writeUInt16(packet.transactionId); // Transaction ID (advertisement: 0)
writer.writeUInt16(packet.flags); // Response, authoritative (rfc6762 18.4)
writer.writeUInt16(packet.questions.size()); // questions count
writer.writeUInt16(packet.answers.size()); // answers count
@@ -211,9 +212,7 @@
}
final int len = writer.getWritePosition();
- final byte[] outBuffer = new byte[len];
- System.arraycopy(packetCreationBuffer, 0, outBuffer, 0, len);
- return outBuffer;
+ return Arrays.copyOfRange(packetCreationBuffer, 0, len);
}
/**
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 48e86d8..01b8de7 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -48,6 +48,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -237,7 +238,18 @@
mDeps = deps;
// Interface match regex.
- mIfaceMatch = mDeps.getInterfaceRegexFromResource(mContext);
+ String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
+ // "*" is a magic string to indicate "pick the default".
+ if (ifaceMatchRegex.equals("*")) {
+ if (SdkLevel.isAtLeastU()) {
+ // On U+, include both usb%d and eth%d interfaces.
+ ifaceMatchRegex = "(usb|eth)\\d+";
+ } else {
+ // On T, include only eth%d interfaces.
+ ifaceMatchRegex = "eth\\d+";
+ }
+ }
+ mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
diff --git a/service-t/src/com/android/server/net/NetworkStatsEventLogger.java b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
new file mode 100644
index 0000000..679837a
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsEventLogger.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class for NetworkStatsService to log events.
+ *
+ * @hide
+ */
+public class NetworkStatsEventLogger {
+ static final int POLL_REASON_DUMPSYS = 0;
+ static final int POLL_REASON_FORCE_UPDATE = 1;
+ static final int POLL_REASON_GLOBAL_ALERT = 2;
+ static final int POLL_REASON_NETWORK_STATUS_CHANGED = 3;
+ static final int POLL_REASON_OPEN_SESSION = 4;
+ static final int POLL_REASON_PERIODIC = 5;
+ static final int POLL_REASON_RAT_CHANGED = 6;
+ static final int POLL_REASON_REG_CALLBACK = 7;
+ static final int POLL_REASON_REMOVE_UIDS = 8;
+ static final int POLL_REASON_UPSTREAM_CHANGED = 9;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "POLL_REASON_" }, value = {
+ POLL_REASON_DUMPSYS,
+ POLL_REASON_FORCE_UPDATE,
+ POLL_REASON_GLOBAL_ALERT,
+ POLL_REASON_NETWORK_STATUS_CHANGED,
+ POLL_REASON_OPEN_SESSION,
+ POLL_REASON_PERIODIC,
+ POLL_REASON_RAT_CHANGED,
+ POLL_REASON_REMOVE_UIDS,
+ POLL_REASON_REG_CALLBACK,
+ POLL_REASON_UPSTREAM_CHANGED
+ })
+ public @interface PollReason {
+ }
+ static final int MAX_POLL_REASON = POLL_REASON_UPSTREAM_CHANGED;
+
+ @VisibleForTesting(visibility = PRIVATE)
+ public static final int MAX_EVENTS_LOGS = 50;
+ private final LocalLog mEventChanges = new LocalLog(MAX_EVENTS_LOGS);
+ private final int[] mPollEventCounts = new int[MAX_POLL_REASON + 1];
+
+ /**
+ * Log a poll event.
+ *
+ * @param flags Flags used when polling. See NetworkStatsService#FLAG_PERSIST_*.
+ * @param event The event of polling to be logged.
+ */
+ public void logPollEvent(int flags, @NonNull PollEvent event) {
+ mEventChanges.log("Poll(flags=" + flags + ", " + event + ")");
+ mPollEventCounts[event.reason]++;
+ }
+
+ /**
+ * Print poll counts per reason into the given stream.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public void dumpPollCountsPerReason(@NonNull IndentingPrintWriter pw) {
+ pw.println("Poll counts per reason:");
+ pw.increaseIndent();
+ for (int i = 0; i <= MAX_POLL_REASON; i++) {
+ pw.println(PollEvent.pollReasonNameOf(i) + ": " + mPollEventCounts[i]);
+ }
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ /**
+ * Print recent poll events into the given stream.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public void dumpRecentPollEvents(@NonNull IndentingPrintWriter pw) {
+ pw.println("Recent poll events:");
+ pw.increaseIndent();
+ mEventChanges.reverseDump(pw);
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ /**
+ * Print the object's state into the given stream.
+ */
+ public void dump(@NonNull IndentingPrintWriter pw) {
+ dumpPollCountsPerReason(pw);
+ dumpRecentPollEvents(pw);
+ }
+
+ public static class PollEvent {
+ public final int reason;
+
+ public PollEvent(@PollReason int reason) {
+ if (reason < 0 || reason > MAX_POLL_REASON) {
+ throw new IllegalArgumentException("Unsupported poll reason: " + reason);
+ }
+ this.reason = reason;
+ }
+
+ @Override
+ public String toString() {
+ return "PollEvent{" + "reason=" + pollReasonNameOf(reason) + "}";
+ }
+
+ /**
+ * Get the name of the given reason.
+ *
+ * If the reason does not have a String representation, returns its integer representation.
+ */
+ @NonNull
+ public static String pollReasonNameOf(@PollReason int reason) {
+ switch (reason) {
+ case POLL_REASON_DUMPSYS: return "DUMPSYS";
+ case POLL_REASON_FORCE_UPDATE: return "FORCE_UPDATE";
+ case POLL_REASON_GLOBAL_ALERT: return "GLOBAL_ALERT";
+ case POLL_REASON_NETWORK_STATUS_CHANGED: return "NETWORK_STATUS_CHANGED";
+ case POLL_REASON_OPEN_SESSION: return "OPEN_SESSION";
+ case POLL_REASON_PERIODIC: return "PERIODIC";
+ case POLL_REASON_RAT_CHANGED: return "RAT_CHANGED";
+ case POLL_REASON_REMOVE_UIDS: return "REMOVE_UIDS";
+ case POLL_REASON_REG_CALLBACK: return "REG_CALLBACK";
+ case POLL_REASON_UPSTREAM_CHANGED: return "UPSTREAM_CHANGED";
+ default: return Integer.toString(reason);
+ }
+ }
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index 1cd670a..21cf351 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -142,6 +142,11 @@
@VisibleForTesting
protected Looper getHandlerLooperLocked() {
+ // TODO: Currently, callbacks are dispatched on this thread if the caller register
+ // callback without supplying a Handler. To ensure that the service handler thread
+ // is not blocked by client code, the observers must create their own thread. Once
+ // all callbacks are dispatched outside of the handler thread, the service handler
+ // thread can be used here.
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
return handlerThread.getLooper();
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 25e59d5..804e709 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -66,6 +66,17 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
import static com.android.net.module.util.NetworkStatsUtils.LIMIT_GLOBAL_ALERT;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REMOVE_UIDS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_UPSTREAM_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -281,6 +292,8 @@
static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
static final String NETSTATS_IMPORT_SUCCESSES_COUNTER_NAME = "import.successes";
static final String NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME = "import.fallbacks";
+ static final String CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER =
+ "enable_network_stats_event_logger";
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
@@ -441,6 +454,7 @@
* Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
* the caller of open session and it is only for debugging.
*/
+ // TODO: Move to NetworkStatsEventLogger to centralize event logging.
@GuardedBy("mOpenSessionCallsLock")
private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
@@ -513,19 +527,21 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PERFORM_POLL: {
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent((int) msg.obj));
break;
}
case MSG_NOTIFY_NETWORK_STATUS: {
- // If no cached states, ignore.
- if (mLastNetworkStateSnapshots == null) break;
- // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing.
- handleNotifyNetworkStatus(
- mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+ synchronized (mStatsLock) {
+ // If no cached states, ignore.
+ if (mLastNetworkStateSnapshots == null) break;
+ handleNotifyNetworkStatus(mDefaultNetworks, mLastNetworkStateSnapshots,
+ mActiveIface, maybeCreatePollEvent((int) msg.obj));
+ }
break;
}
case MSG_PERFORM_POLL_REGISTER_ALERT: {
- performPoll(FLAG_PERSIST_NETWORK);
+ performPoll(FLAG_PERSIST_NETWORK,
+ maybeCreatePollEvent(POLL_REASON_GLOBAL_ALERT));
registerGlobalAlert();
break;
}
@@ -611,18 +627,21 @@
mStatsMapB = mDeps.getStatsMapB();
mAppUidStatsMap = mDeps.getAppUidStatsMap();
mIfaceStatsMap = mDeps.getIfaceStatsMap();
+ // To prevent any possible races, the flag is not allowed to change until rebooting.
+ mSupportEventLogger = mDeps.supportEventLogger(mContext);
+ if (mSupportEventLogger) {
+ mEventLogger = new NetworkStatsEventLogger();
+ } else {
+ mEventLogger = null;
+ }
// TODO: Remove bpfNetMaps creation and always start SkDestroyListener
// Following code is for the experiment to verify the SkDestroyListener refactoring. Based
// on the experiment flag, BpfNetMaps starts C SkDestroyListener (existing code) or
// NetworkStatsService starts Java SkDestroyListener (new code).
final BpfNetMaps bpfNetMaps = mDeps.makeBpfNetMaps(mContext);
- if (bpfNetMaps.isSkDestroyListenerRunning()) {
- mSkDestroyListener = null;
- } else {
- mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
- mHandler.post(mSkDestroyListener::start);
- }
+ mSkDestroyListener = mDeps.makeSkDestroyListener(mCookieTagMap, mHandler);
+ mHandler.post(mSkDestroyListener::start);
}
/**
@@ -839,6 +858,14 @@
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
return new SkDestroyListener(cookieTagMap, handler, new SharedLog(TAG));
}
+
+ /**
+ * Get whether event logger feature is supported.
+ */
+ public boolean supportEventLogger(Context ctx) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(
+ ctx, CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER);
+ }
}
/**
@@ -1431,7 +1458,7 @@
| NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
final long ident = Binder.clearCallingIdentity();
try {
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_OPEN_SESSION));
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1827,7 +1854,8 @@
final long token = Binder.clearCallingIdentity();
try {
- handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface);
+ handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface,
+ maybeCreatePollEvent(POLL_REASON_NETWORK_STATUS_CHANGED));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1844,7 +1872,8 @@
final long token = Binder.clearCallingIdentity();
try {
- performPoll(FLAG_PERSIST_ALL);
+ // TODO: Log callstack for system server callers.
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_FORCE_UPDATE));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1901,7 +1930,8 @@
}
// Create baseline stats
- mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL,
+ POLL_REASON_REG_CALLBACK));
return normalizedRequest;
}
@@ -1998,7 +2028,8 @@
new TetheringManager.TetheringEventCallback() {
@Override
public void onUpstreamChanged(@Nullable Network network) {
- performPoll(FLAG_PERSIST_NETWORK);
+ performPoll(FLAG_PERSIST_NETWORK,
+ maybeCreatePollEvent(POLL_REASON_UPSTREAM_CHANGED));
}
};
@@ -2007,7 +2038,7 @@
public void onReceive(Context context, Intent intent) {
// on background handler thread, and verified UPDATE_DEVICE_STATS
// permission above.
- performPoll(FLAG_PERSIST_ALL);
+ performPoll(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_PERIODIC));
// verify that we're watching global alert
registerGlobalAlert();
@@ -2071,19 +2102,20 @@
public void handleOnCollapsedRatTypeChanged() {
// Protect service from frequently updating. Remove pending messages if any.
mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS);
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay());
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS,
+ POLL_REASON_RAT_CHANGED), mSettings.getPollDelay());
}
private void handleNotifyNetworkStatus(
Network[] defaultNetworks,
NetworkStateSnapshot[] snapshots,
- String activeIface) {
+ String activeIface,
+ @Nullable PollEvent event) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
mActiveIface = activeIface;
- handleNotifyNetworkStatusLocked(defaultNetworks, snapshots);
+ handleNotifyNetworkStatusLocked(defaultNetworks, snapshots, event);
} finally {
mWakeLock.release();
}
@@ -2097,7 +2129,7 @@
*/
@GuardedBy("mStatsLock")
private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks,
- @NonNull NetworkStateSnapshot[] snapshots) {
+ @NonNull NetworkStateSnapshot[] snapshots, @Nullable PollEvent event) {
if (!mSystemReady) return;
if (LOGV) Log.v(TAG, "handleNotifyNetworkStatusLocked()");
@@ -2107,7 +2139,7 @@
// poll, but only persist network stats to keep codepath fast. UID stats
// will be persisted during next alarm poll event.
- performPollLocked(FLAG_PERSIST_NETWORK);
+ performPollLocked(FLAG_PERSIST_NETWORK, event);
// Rebuild active interfaces based on connected networks
mActiveIfaces.clear();
@@ -2324,12 +2356,12 @@
}
}
- private void performPoll(int flags) {
+ private void performPoll(int flags, @Nullable PollEvent event) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
- performPollLocked(flags);
+ performPollLocked(flags, event);
} finally {
mWakeLock.release();
}
@@ -2341,11 +2373,15 @@
* {@link NetworkStatsHistory}.
*/
@GuardedBy("mStatsLock")
- private void performPollLocked(int flags) {
+ private void performPollLocked(int flags, @Nullable PollEvent event) {
if (!mSystemReady) return;
if (LOGV) Log.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");
+ if (mSupportEventLogger) {
+ mEventLogger.logPollEvent(flags, event);
+ }
+
final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;
@@ -2545,7 +2581,7 @@
if (LOGV) Log.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids));
// Perform one last poll before removing
- performPollLocked(FLAG_PERSIST_ALL);
+ performPollLocked(FLAG_PERSIST_ALL, maybeCreatePollEvent(POLL_REASON_REMOVE_UIDS));
mUidRecorder.removeUidsLocked(uids);
mUidTagRecorder.removeUidsLocked(uids);
@@ -2628,7 +2664,8 @@
}
if (poll) {
- performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
+ performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE,
+ maybeCreatePollEvent(POLL_REASON_DUMPSYS));
pw.println("Forced poll");
return;
}
@@ -2688,6 +2725,7 @@
pw.println("(failed to dump platform legacy stats import counters)");
}
}
+ pw.println(CONFIG_ENABLE_NETWORK_STATS_EVENT_LOGGER + ": " + mSupportEventLogger);
pw.decreaseIndent();
@@ -2745,6 +2783,10 @@
pw.decreaseIndent();
pw.println();
+ if (mSupportEventLogger) {
+ mEventLogger.dump(pw);
+ }
+
pw.println("Stats Providers:");
pw.increaseIndent();
invokeForAllStatsProviderCallbacks((cb) -> {
@@ -3214,6 +3256,22 @@
}
}
+ private final boolean mSupportEventLogger;
+ @GuardedBy("mStatsLock")
+ @Nullable
+ private final NetworkStatsEventLogger mEventLogger;
+
+ /**
+ * Create a PollEvent instance if the feature is enabled.
+ */
+ @Nullable
+ public PollEvent maybeCreatePollEvent(@NetworkStatsEventLogger.PollReason int reason) {
+ if (mSupportEventLogger) {
+ return new PollEvent(reason);
+ }
+ return null;
+ }
+
private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
@Override
public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
diff --git a/service/Android.bp b/service/Android.bp
index 8e59e86..e2dab9e 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -110,8 +110,8 @@
],
srcs: [
":services.connectivity-netstats-jni-sources",
- "jni/com_android_server_BpfNetMaps.cpp",
"jni/com_android_server_connectivity_ClatCoordinator.cpp",
+ "jni/com_android_server_ServiceManagerWrapper.cpp",
"jni/com_android_server_TestNetworkService.cpp",
"jni/onload.cpp",
],
@@ -124,11 +124,11 @@
"libmodules-utils-build",
"libnetjniutils",
"libnet_utils_device_common_bpfjni",
- "libtraffic_controller",
"netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
"libbase",
+ "libbinder_ndk",
"libcutils",
"libnetdutils",
"liblog",
@@ -185,10 +185,9 @@
"androidx.annotation_annotation",
"connectivity-net-module-utils-bpf",
"connectivity_native_aidl_interface-lateststable-java",
- "dnsresolver_aidl_interface-V11-java",
+ "dnsresolver_aidl_interface-V13-java",
"modules-utils-shell-command-handler",
"net-utils-device-common",
- "net-utils-device-common-bpf",
"net-utils-device-common-ip",
"net-utils-device-common-netlink",
"net-utils-services-common",
@@ -205,6 +204,8 @@
visibility: [
"//packages/modules/Connectivity/service-t",
"//packages/modules/Connectivity/tests:__subpackages__",
+ "//packages/modules/Connectivity/thread/service:__subpackages__",
+ "//packages/modules/Connectivity/thread/tests:__subpackages__",
],
}
diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
index e753cb3..7e4dd42 100644
--- a/service/ServiceConnectivityResources/res/values-as/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -18,7 +18,7 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ছিষ্টেম সংযোগৰ উৎস"</string>
- <string name="wifi_available_sign_in" msgid="8041178343789805553">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
+ <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi নেটৱৰ্কত ছাইন ইন কৰক"</string>
<string name="network_available_sign_in" msgid="2622520134876355561">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
<!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
<skip />
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index f30abc6..045d707f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -194,8 +194,11 @@
-->
</string-array>
- <!-- Regex of wired ethernet ifaces -->
- <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
+ <!-- Regex of wired ethernet ifaces. Network interfaces that match this regex will be tracked
+ by ethernet service.
+ If set to "*", ethernet service uses "(eth|usb)\\d+" on Android U+ and eth\\d+ on
+ Android T. -->
+ <string translatable="false" name="config_ethernet_iface_regex">*</string>
<!-- Ignores Wi-Fi validation failures after roam.
If validation fails on a Wi-Fi network after a roam to a new BSSID,
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
deleted file mode 100644
index 50a0635..0000000
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "TrafficControllerJni"
-
-#include "TrafficController.h"
-
-#include "netd.h"
-
-#include <jni.h>
-#include <log/log.h>
-#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedUtfChars.h>
-#include <nativehelper/ScopedPrimitiveArray.h>
-#include <netjniutils/netjniutils.h>
-#include <net/if.h>
-#include <private/android_filesystem_config.h>
-#include <unistd.h>
-#include <vector>
-
-
-using android::net::TrafficController;
-using android::netdutils::Status;
-
-using UidOwnerMatchType::PENALTY_BOX_MATCH;
-using UidOwnerMatchType::HAPPY_BOX_MATCH;
-
-static android::net::TrafficController mTc;
-
-namespace android {
-
-#define CHECK_LOG(status) \
- do { \
- if (!isOk(status)) \
- ALOGE("%s failed, error code = %d", __func__, status.code()); \
- } while (0)
-
-static void native_init(JNIEnv* env, jclass clazz, jboolean startSkDestroyListener) {
- Status status = mTc.start(startSkDestroyListener);
- CHECK_LOG(status);
- if (!isOk(status)) {
- uid_t uid = getuid();
- ALOGE("BpfNetMaps jni init failure as uid=%d", uid);
- // We probably only ever get called from system_server (ie. AID_SYSTEM)
- // or from tests, and never from network_stack (ie. AID_NETWORK_STACK).
- // However, if we ever do add calls from production network_stack code
- // we do want to make sure this initializes correctly.
- // TODO: Fix tests to not use this jni lib, so we can unconditionally abort()
- if (uid == AID_SYSTEM || uid == AID_NETWORK_STACK) abort();
- }
-}
-
-static jint native_addNaughtyApp(JNIEnv* env, jobject self, jint uid) {
- const uint32_t appUids = static_cast<uint32_t>(abs(uid));
- Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOp::IptOpInsert);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_removeNaughtyApp(JNIEnv* env, jobject self, jint uid) {
- const uint32_t appUids = static_cast<uint32_t>(abs(uid));
- Status status = mTc.updateUidOwnerMap(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOp::IptOpDelete);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_addNiceApp(JNIEnv* env, jobject self, jint uid) {
- const uint32_t appUids = static_cast<uint32_t>(abs(uid));
- Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
- TrafficController::IptOp::IptOpInsert);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_removeNiceApp(JNIEnv* env, jobject self, jint uid) {
- const uint32_t appUids = static_cast<uint32_t>(abs(uid));
- Status status = mTc.updateUidOwnerMap(appUids, HAPPY_BOX_MATCH,
- TrafficController::IptOp::IptOpDelete);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_setChildChain(JNIEnv* env, jobject self, jint childChain, jboolean enable) {
- auto chain = static_cast<ChildChain>(childChain);
- int res = mTc.toggleUidOwnerMap(chain, enable);
- if (res) ALOGE("%s failed, error code = %d", __func__, res);
- return (jint)res;
-}
-
-static jint native_replaceUidChain(JNIEnv* env, jobject self, jstring name, jboolean isAllowlist,
- jintArray jUids) {
- const ScopedUtfChars chainNameUtf8(env, name);
- if (chainNameUtf8.c_str() == nullptr) return -EINVAL;
- const std::string chainName(chainNameUtf8.c_str());
-
- ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) return -EINVAL;
-
- size_t size = uids.size();
- static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
- std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
- int res = mTc.replaceUidOwnerMap(chainName, isAllowlist, data);
- if (res) ALOGE("%s failed, error code = %d", __func__, res);
- return (jint)res;
-}
-
-static jint native_setUidRule(JNIEnv* env, jobject self, jint childChain, jint uid,
- jint firewallRule) {
- auto chain = static_cast<ChildChain>(childChain);
- auto rule = static_cast<FirewallRule>(firewallRule);
- FirewallType fType = mTc.getFirewallType(chain);
-
- int res = mTc.changeUidOwnerRule(chain, uid, rule, fType);
- if (res) ALOGE("%s failed, error code = %d", __func__, res);
- return (jint)res;
-}
-
-static jint native_addUidInterfaceRules(JNIEnv* env, jobject self, jstring ifName,
- jintArray jUids) {
- // Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
- // set to 0.
- int ifIndex = 0;
- if (ifName != nullptr) {
- const ScopedUtfChars ifNameUtf8(env, ifName);
- const std::string interfaceName(ifNameUtf8.c_str());
- ifIndex = if_nametoindex(interfaceName.c_str());
- }
-
- ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) return -EINVAL;
-
- size_t size = uids.size();
- static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
- std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
- Status status = mTc.addUidInterfaceRules(ifIndex, data);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_removeUidInterfaceRules(JNIEnv* env, jobject self, jintArray jUids) {
- ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) return -EINVAL;
-
- size_t size = uids.size();
- static_assert(sizeof(*(uids.get())) == sizeof(int32_t));
- std::vector<int32_t> data ((int32_t *)&uids[0], (int32_t*)&uids[size]);
- Status status = mTc.removeUidInterfaceRules(data);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_updateUidLockdownRule(JNIEnv* env, jobject self, jint uid, jboolean add) {
- Status status = mTc.updateUidLockdownRule(uid, add);
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
- Status status = mTc.swapActiveStatsMap();
- CHECK_LOG(status);
- return (jint)status.code();
-}
-
-static void native_setPermissionForUids(JNIEnv* env, jobject self, jint permission,
- jintArray jUids) {
- ScopedIntArrayRO uids(env, jUids);
- if (uids.get() == nullptr) return;
-
- size_t size = uids.size();
- static_assert(sizeof(*(uids.get())) == sizeof(uid_t));
- std::vector<uid_t> data ((uid_t *)&uids[0], (uid_t*)&uids[size]);
- mTc.setPermissionForUids(permission, data);
-}
-
-static jint native_synchronizeKernelRCU(JNIEnv* env, jobject self) {
- return -bpf::synchronizeKernelRCU();
-}
-
-/*
- * JNI registration.
- */
-// clang-format off
-static const JNINativeMethod gMethods[] = {
- /* name, signature, funcPtr */
- {"native_init", "(Z)V",
- (void*)native_init},
- {"native_addNaughtyApp", "(I)I",
- (void*)native_addNaughtyApp},
- {"native_removeNaughtyApp", "(I)I",
- (void*)native_removeNaughtyApp},
- {"native_addNiceApp", "(I)I",
- (void*)native_addNiceApp},
- {"native_removeNiceApp", "(I)I",
- (void*)native_removeNiceApp},
- {"native_setChildChain", "(IZ)I",
- (void*)native_setChildChain},
- {"native_replaceUidChain", "(Ljava/lang/String;Z[I)I",
- (void*)native_replaceUidChain},
- {"native_setUidRule", "(III)I",
- (void*)native_setUidRule},
- {"native_addUidInterfaceRules", "(Ljava/lang/String;[I)I",
- (void*)native_addUidInterfaceRules},
- {"native_removeUidInterfaceRules", "([I)I",
- (void*)native_removeUidInterfaceRules},
- {"native_updateUidLockdownRule", "(IZ)I",
- (void*)native_updateUidLockdownRule},
- {"native_swapActiveStatsMap", "()I",
- (void*)native_swapActiveStatsMap},
- {"native_setPermissionForUids", "(I[I)V",
- (void*)native_setPermissionForUids},
- {"native_synchronizeKernelRCU", "()I",
- (void*)native_synchronizeKernelRCU},
-};
-// clang-format on
-
-int register_com_android_server_BpfNetMaps(JNIEnv* env) {
- return jniRegisterNativeMethods(env, "android/net/connectivity/com/android/server/BpfNetMaps",
- gMethods, NELEM(gMethods));
-}
-
-}; // namespace android
diff --git a/service/jni/com_android_server_ServiceManagerWrapper.cpp b/service/jni/com_android_server_ServiceManagerWrapper.cpp
new file mode 100644
index 0000000..0e32726
--- /dev/null
+++ b/service/jni/com_android_server_ServiceManagerWrapper.cpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android/binder_ibinder_jni.h>
+#include <android/binder_manager.h>
+#include <jni.h>
+#include "nativehelper/JNIHelp.h"
+#include <nativehelper/ScopedUtfChars.h>
+#include <private/android_filesystem_config.h>
+
+namespace android {
+static jobject com_android_server_ServiceManagerWrapper_waitForService(
+ JNIEnv* env, jobject clazz, jstring serviceName) {
+ ScopedUtfChars name(env, serviceName);
+
+// AServiceManager_waitForService is available on only 31+, but it's still safe for Thread
+// service because it's enabled on only 34+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+ return AIBinder_toJavaBinder(env, AServiceManager_waitForService(name.c_str()));
+#pragma clang diagnostic pop
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeWaitForService",
+ "(Ljava/lang/String;)Landroid/os/IBinder;",
+ (void*)com_android_server_ServiceManagerWrapper_waitForService},
+};
+
+int register_com_android_server_ServiceManagerWrapper(JNIEnv* env) {
+ return jniRegisterNativeMethods(env,
+ "android/net/connectivity/com/android/server/ServiceManagerWrapper",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/service/jni/onload.cpp b/service/jni/onload.cpp
index ed74430..bb70d4f 100644
--- a/service/jni/onload.cpp
+++ b/service/jni/onload.cpp
@@ -22,10 +22,10 @@
namespace android {
int register_com_android_server_TestNetworkService(JNIEnv* env);
-int register_com_android_server_BpfNetMaps(JNIEnv* env);
int register_com_android_server_connectivity_ClatCoordinator(JNIEnv* env);
int register_android_server_net_NetworkStatsFactory(JNIEnv* env);
int register_android_server_net_NetworkStatsService(JNIEnv* env);
+int register_com_android_server_ServiceManagerWrapper(JNIEnv* env);
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
JNIEnv *env;
@@ -38,11 +38,11 @@
return JNI_ERR;
}
- if (android::modules::sdklevel::IsAtLeastT()) {
- if (register_com_android_server_BpfNetMaps(env) < 0) {
- return JNI_ERR;
- }
+ if (register_com_android_server_ServiceManagerWrapper(env) < 0) {
+ return JNI_ERR;
+ }
+ if (android::modules::sdklevel::IsAtLeastT()) {
if (register_com_android_server_connectivity_ClatCoordinator(env) < 0) {
return JNI_ERR;
}
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
new file mode 100644
index 0000000..5149e6d
--- /dev/null
+++ b/service/lint-baseline.xml
@@ -0,0 +1,510 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.app.PendingIntent#intentFilterEquals`"
+ errorLine1=" return a.intentFilterEquals(b);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1358"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.app.usage.NetworkStatsManager#notifyNetworkStatus`"
+ errorLine1=" mStatsManager.notifyNetworkStatus(getDefaultNetworks(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9938"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isOem`"
+ errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="481"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isProduct`"
+ errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="481"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.content.pm.ApplicationInfo#isVendor`"
+ errorLine1=" return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="481"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getMultipathPreference`"
+ errorLine1=" networkPreference = netPolicyManager.getMultipathPreference(network);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5498"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#getRestrictBackgroundStatus`"
+ errorLine1=" return mPolicyManager.getRestrictBackgroundStatus(callerUid);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2565"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidNetworkingBlocked`"
+ errorLine1=" return mPolicyManager.isUidNetworkingBlocked(uid, metered);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1914"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#isUidRestrictedOnMeteredNetworks`"
+ errorLine1=" if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="7094"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkPolicyManager#registerNetworkPolicyCallback`"
+ errorLine1=" mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1567"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getLinkProperties`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2584"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetworkCapabilities`"
+ errorLine1=" snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2584"
+ column="64"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2585"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getNetwork`"
+ errorLine1=" final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2581"
+ column="81"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkStateSnapshot#getSubscriberId`"
+ errorLine1=" snapshot.getNetwork(), snapshot.getSubscriberId()));"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2585"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager#getWatchlistConfigHash`"
+ errorLine1=" return nwm.getWatchlistConfigHash();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10060"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#addPacProxyInstalledListener`"
+ errorLine1=" mPacProxyManager.addPacProxyInstalledListener("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="111"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="208"
+ column="48"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.net.PacProxyManager#setCurrentProxyScriptUrl`"
+ errorLine1=" mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="252"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportMobileRadioPowerState`"
+ errorLine1=" bs.reportMobileRadioPowerState(isActive, NO_UID);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11006"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportNetworkInterfaceForTransports`"
+ errorLine1=" batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1347"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.BatteryStatsManager#reportWifiRadioPowerState`"
+ errorLine1=" bs.reportWifiRadioPowerState(isActive, NO_UID);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="11009"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9074"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.Build#isDebuggable`"
+ errorLine1=" if (!Build.isDebuggable()) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5039"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(INTERNET)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="396"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.SystemConfigManager#getSystemPermissionUids`"
+ errorLine1=" for (final int uid : mSystemConfigManager.getSystemPermissionUids(UPDATE_DEVICE_STATS)) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="404"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.os.UserHandle#getUid`"
+ errorLine1=" final int uid = handle.getUid(appId);"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java"
+ line="1069"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="285"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="287"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="265"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#getsockoptInt`"
+ errorLine1=" tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="262"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCINQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="392"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `android.system.Os#ioctlInt`"
+ errorLine1=" final int result = Os.ioctlInt(fd, SIOCOUTQ);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java"
+ line="402"
+ column="31"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=' InetAddress.parseNumericAddress("::").getAddress();'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/DscpPolicyValue.java"
+ line="99"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `java.net.InetAddress#parseNumericAddress`"
+ errorLine1=' private static final InetAddress GOOGLE_DNS_4 = InetAddress.parseNumericAddress("8.8.8.8");'
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ClatCoordinator.java"
+ line="89"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="9991"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(pfd);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10008"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `libcore.io.IoUtils#closeQuietly`"
+ errorLine1=" IoUtils.closeQuietly(mFileDescriptor);"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java"
+ line="481"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.NetworkStateSnapshot`"
+ errorLine1=" return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java"
+ line="1269"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Call requires API level 31 (current min is 30): `new android.net.UnderlyingNetworkInfo`"
+ errorLine1=" return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="6123"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager.NetworkPolicyCallback`"
+ errorLine1=" private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="2827"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="5493"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkPolicyManager`"
+ errorLine1=" mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="1554"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.NetworkWatchlistManager`"
+ errorLine1=" NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java"
+ line="10054"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager.PacProxyInstalledListener`"
+ errorLine1=" private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="90"
+ column="56"/>
+ </issue>
+
+ <issue
+ id="NewApi"
+ message="Class requires API level 31 (current min is 30): `android.net.PacProxyManager`"
+ errorLine1=" mPacProxyManager = context.getSystemService(PacProxyManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java"
+ line="108"
+ column="53"/>
+ </issue>
+
+</issues>
\ No newline at end of file
diff --git a/service/native/Android.bp b/service/native/Android.bp
deleted file mode 100644
index 697fcbd..0000000
--- a/service/native/Android.bp
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_library {
- name: "libtraffic_controller",
- defaults: ["netd_defaults"],
- srcs: [
- "TrafficController.cpp",
- ],
- header_libs: [
- "bpf_connectivity_headers",
- ],
- static_libs: [
- // TrafficController would use the constants of INetd so that add
- // netd_aidl_interface-lateststable-ndk.
- "netd_aidl_interface-lateststable-ndk",
- ],
- shared_libs: [
- // TODO: Find a good way to remove libbase.
- "libbase",
- "libcutils",
- "libnetdutils",
- "libutils",
- "liblog",
- ],
- export_include_dirs: ["include"],
- sanitize: {
- cfi: true,
- },
- apex_available: [
- "com.android.tethering",
- ],
- min_sdk_version: "30",
-}
-
-cc_test {
- name: "traffic_controller_unit_test",
- test_suites: ["general-tests", "mts-tethering"],
- test_config_template: ":net_native_test_config_template",
- require_root: true,
- local_include_dirs: ["include"],
- header_libs: [
- "bpf_connectivity_headers",
- ],
- srcs: [
- "TrafficControllerTest.cpp",
- ],
- static_libs: [
- "libbase",
- "libgmock",
- "liblog",
- "libnetdutils",
- "libtraffic_controller",
- "libutils",
- "libnetd_updatable",
- "netd_aidl_interface-lateststable-ndk",
- ],
- compile_multilib: "both",
- multilib: {
- lib32: {
- suffix: "32",
- },
- lib64: {
- suffix: "64",
- },
- },
-}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
deleted file mode 100644
index 8cd698e..0000000
--- a/service/native/TrafficController.cpp
+++ /dev/null
@@ -1,577 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#define LOG_TAG "TrafficController"
-#include <inttypes.h>
-#include <linux/if_ether.h>
-#include <linux/in.h>
-#include <linux/inet_diag.h>
-#include <linux/netlink.h>
-#include <linux/sock_diag.h>
-#include <linux/unistd.h>
-#include <net/if.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/utsname.h>
-#include <sys/wait.h>
-#include <map>
-#include <mutex>
-#include <unordered_set>
-#include <vector>
-
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <android-base/unique_fd.h>
-#include <netdutils/StatusOr.h>
-#include <netdutils/Syscalls.h>
-#include <netdutils/UidConstants.h>
-#include <netdutils/Utils.h>
-#include <private/android_filesystem_config.h>
-
-#include "TrafficController.h"
-#include "bpf/BpfMap.h"
-
-namespace android {
-namespace net {
-
-using base::StringPrintf;
-using base::unique_fd;
-using bpf::BpfMap;
-using bpf::synchronizeKernelRCU;
-using netdutils::NetlinkListener;
-using netdutils::NetlinkListenerInterface;
-using netdutils::Slice;
-using netdutils::sSyscalls;
-using netdutils::Status;
-using netdutils::statusFromErrno;
-using netdutils::StatusOr;
-
-constexpr int kSockDiagMsgType = SOCK_DIAG_BY_FAMILY;
-constexpr int kSockDiagDoneMsgType = NLMSG_DONE;
-
-const char* TrafficController::LOCAL_DOZABLE = "fw_dozable";
-const char* TrafficController::LOCAL_STANDBY = "fw_standby";
-const char* TrafficController::LOCAL_POWERSAVE = "fw_powersave";
-const char* TrafficController::LOCAL_RESTRICTED = "fw_restricted";
-const char* TrafficController::LOCAL_LOW_POWER_STANDBY = "fw_low_power_standby";
-const char* TrafficController::LOCAL_OEM_DENY_1 = "fw_oem_deny_1";
-const char* TrafficController::LOCAL_OEM_DENY_2 = "fw_oem_deny_2";
-const char* TrafficController::LOCAL_OEM_DENY_3 = "fw_oem_deny_3";
-
-static_assert(BPF_PERMISSION_INTERNET == INetd::PERMISSION_INTERNET,
- "Mismatch between BPF and AIDL permissions: PERMISSION_INTERNET");
-static_assert(BPF_PERMISSION_UPDATE_DEVICE_STATS == INetd::PERMISSION_UPDATE_DEVICE_STATS,
- "Mismatch between BPF and AIDL permissions: PERMISSION_UPDATE_DEVICE_STATS");
-
-#define FLAG_MSG_TRANS(result, flag, value) \
- do { \
- if ((value) & (flag)) { \
- (result).append(" " #flag); \
- (value) &= ~(flag); \
- } \
- } while (0)
-
-const std::string uidMatchTypeToString(uint32_t match) {
- std::string matchType;
- FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
- FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
- FLAG_MSG_TRANS(matchType, DOZABLE_MATCH, match);
- FLAG_MSG_TRANS(matchType, STANDBY_MATCH, match);
- FLAG_MSG_TRANS(matchType, POWERSAVE_MATCH, match);
- FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
- FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
- FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
- FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
- FLAG_MSG_TRANS(matchType, OEM_DENY_1_MATCH, match);
- FLAG_MSG_TRANS(matchType, OEM_DENY_2_MATCH, match);
- FLAG_MSG_TRANS(matchType, OEM_DENY_3_MATCH, match);
- if (match) {
- return StringPrintf("Unknown match: %u", match);
- }
- return matchType;
-}
-
-const std::string UidPermissionTypeToString(int permission) {
- if (permission == INetd::PERMISSION_NONE) {
- return "PERMISSION_NONE";
- }
- if (permission == INetd::PERMISSION_UNINSTALLED) {
- // This should never appear in the map, complain loudly if it does.
- return "PERMISSION_UNINSTALLED error!";
- }
- std::string permissionType;
- FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_INTERNET, permission);
- FLAG_MSG_TRANS(permissionType, BPF_PERMISSION_UPDATE_DEVICE_STATS, permission);
- if (permission) {
- return StringPrintf("Unknown permission: %u", permission);
- }
- return permissionType;
-}
-
-StatusOr<std::unique_ptr<NetlinkListenerInterface>> TrafficController::makeSkDestroyListener() {
- const auto& sys = sSyscalls.get();
- ASSIGN_OR_RETURN(auto event, sys.eventfd(0, EFD_CLOEXEC));
- const int domain = AF_NETLINK;
- const int type = SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK;
- const int protocol = NETLINK_INET_DIAG;
- ASSIGN_OR_RETURN(auto sock, sys.socket(domain, type, protocol));
-
- // TODO: if too many sockets are closed too quickly, we can overflow the socket buffer, and
- // some entries in mCookieTagMap will not be freed. In order to fix this we would need to
- // periodically dump all sockets and remove the tag entries for sockets that have been closed.
- // For now, set a large-enough buffer that we can close hundreds of sockets without getting
- // ENOBUFS and leaking mCookieTagMap entries.
- int rcvbuf = 512 * 1024;
- auto ret = sys.setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
- if (!ret.ok()) {
- ALOGW("Failed to set SkDestroyListener buffer size to %d: %s", rcvbuf, ret.msg().c_str());
- }
-
- sockaddr_nl addr = {
- .nl_family = AF_NETLINK,
- .nl_groups = 1 << (SKNLGRP_INET_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET_UDP_DESTROY - 1) |
- 1 << (SKNLGRP_INET6_TCP_DESTROY - 1) | 1 << (SKNLGRP_INET6_UDP_DESTROY - 1)};
- RETURN_IF_NOT_OK(sys.bind(sock, addr));
-
- const sockaddr_nl kernel = {.nl_family = AF_NETLINK};
- RETURN_IF_NOT_OK(sys.connect(sock, kernel));
-
- std::unique_ptr<NetlinkListenerInterface> listener =
- std::make_unique<NetlinkListener>(std::move(event), std::move(sock), "SkDestroyListen");
-
- return listener;
-}
-
-Status TrafficController::initMaps() {
- std::lock_guard guard(mMutex);
-
- RETURN_IF_NOT_OK(mCookieTagMap.init(COOKIE_TAG_MAP_PATH));
- RETURN_IF_NOT_OK(mUidCounterSetMap.init(UID_COUNTERSET_MAP_PATH));
- RETURN_IF_NOT_OK(mAppUidStatsMap.init(APP_UID_STATS_MAP_PATH));
- RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
- RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
- RETURN_IF_NOT_OK(mIfaceIndexNameMap.init(IFACE_INDEX_NAME_MAP_PATH));
- RETURN_IF_NOT_OK(mIfaceStatsMap.init(IFACE_STATS_MAP_PATH));
-
- RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
-
- RETURN_IF_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
- RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
- ALOGI("%s successfully", __func__);
-
- return netdutils::status::ok;
-}
-
-Status TrafficController::start(bool startSkDestroyListener) {
- RETURN_IF_NOT_OK(initMaps());
-
- if (!startSkDestroyListener) {
- return netdutils::status::ok;
- }
-
- auto result = makeSkDestroyListener();
- if (!isOk(result)) {
- ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
- } else {
- mSkDestroyListener = std::move(result.value());
- }
- // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function.
- const auto rxHandler = [this](const nlmsghdr&, const Slice msg) {
- std::lock_guard guard(mMutex);
- inet_diag_msg diagmsg = {};
- if (extract(msg, diagmsg) < sizeof(inet_diag_msg)) {
- ALOGE("Unrecognized netlink message: %s", toString(msg).c_str());
- return;
- }
- uint64_t sock_cookie = static_cast<uint64_t>(diagmsg.id.idiag_cookie[0]) |
- (static_cast<uint64_t>(diagmsg.id.idiag_cookie[1]) << 32);
-
- Status s = mCookieTagMap.deleteValue(sock_cookie);
- if (!isOk(s) && s.code() != ENOENT) {
- ALOGE("Failed to delete cookie %" PRIx64 ": %s", sock_cookie, toString(s).c_str());
- return;
- }
- };
- expectOk(mSkDestroyListener->subscribe(kSockDiagMsgType, rxHandler));
-
- // In case multiple netlink message comes in as a stream, we need to handle the rxDone message
- // properly.
- const auto rxDoneHandler = [](const nlmsghdr&, const Slice msg) {
- // Ignore NLMSG_DONE messages
- inet_diag_msg diagmsg = {};
- extract(msg, diagmsg);
- };
- expectOk(mSkDestroyListener->subscribe(kSockDiagDoneMsgType, rxDoneHandler));
-
- return netdutils::status::ok;
-}
-
-Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
- FirewallType type) {
- std::lock_guard guard(mMutex);
- if ((rule == ALLOW && type == ALLOWLIST) || (rule == DENY && type == DENYLIST)) {
- RETURN_IF_NOT_OK(addRule(uid, match));
- } else if ((rule == ALLOW && type == DENYLIST) || (rule == DENY && type == ALLOWLIST)) {
- RETURN_IF_NOT_OK(removeRule(uid, match));
- } else {
- //Cannot happen.
- return statusFromErrno(EINVAL, "");
- }
- return netdutils::status::ok;
-}
-
-Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) {
- auto oldMatch = mUidOwnerMap.readValue(uid);
- if (oldMatch.ok()) {
- UidOwnerValue newMatch = {
- .iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
- .rule = oldMatch.value().rule & ~match,
- };
- if (newMatch.rule == 0) {
- RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
- } else {
- RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
- }
- } else {
- return statusFromErrno(ENOENT, StringPrintf("uid: %u does not exist in map", uid));
- }
- return netdutils::status::ok;
-}
-
-Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
- if (match != IIF_MATCH && iif != 0) {
- return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
- }
- auto oldMatch = mUidOwnerMap.readValue(uid);
- if (oldMatch.ok()) {
- UidOwnerValue newMatch = {
- .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
- .rule = oldMatch.value().rule | match,
- };
- RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
- } else {
- UidOwnerValue newMatch = {
- .iif = iif,
- .rule = match,
- };
- RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
- }
- return netdutils::status::ok;
-}
-
-Status TrafficController::updateUidOwnerMap(const uint32_t uid,
- UidOwnerMatchType matchType, IptOp op) {
- std::lock_guard guard(mMutex);
- if (op == IptOpDelete) {
- RETURN_IF_NOT_OK(removeRule(uid, matchType));
- } else if (op == IptOpInsert) {
- RETURN_IF_NOT_OK(addRule(uid, matchType));
- } else {
- // Cannot happen.
- return statusFromErrno(EINVAL, StringPrintf("invalid IptOp: %d, %d", op, matchType));
- }
- return netdutils::status::ok;
-}
-
-FirewallType TrafficController::getFirewallType(ChildChain chain) {
- switch (chain) {
- case DOZABLE:
- return ALLOWLIST;
- case STANDBY:
- return DENYLIST;
- case POWERSAVE:
- return ALLOWLIST;
- case RESTRICTED:
- return ALLOWLIST;
- case LOW_POWER_STANDBY:
- return ALLOWLIST;
- case OEM_DENY_1:
- return DENYLIST;
- case OEM_DENY_2:
- return DENYLIST;
- case OEM_DENY_3:
- return DENYLIST;
- case NONE:
- default:
- return DENYLIST;
- }
-}
-
-int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallRule rule,
- FirewallType type) {
- Status res;
- switch (chain) {
- case DOZABLE:
- res = updateOwnerMapEntry(DOZABLE_MATCH, uid, rule, type);
- break;
- case STANDBY:
- res = updateOwnerMapEntry(STANDBY_MATCH, uid, rule, type);
- break;
- case POWERSAVE:
- res = updateOwnerMapEntry(POWERSAVE_MATCH, uid, rule, type);
- break;
- case RESTRICTED:
- res = updateOwnerMapEntry(RESTRICTED_MATCH, uid, rule, type);
- break;
- case LOW_POWER_STANDBY:
- res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
- break;
- case OEM_DENY_1:
- res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
- break;
- case OEM_DENY_2:
- res = updateOwnerMapEntry(OEM_DENY_2_MATCH, uid, rule, type);
- break;
- case OEM_DENY_3:
- res = updateOwnerMapEntry(OEM_DENY_3_MATCH, uid, rule, type);
- break;
- case NONE:
- default:
- ALOGW("Unknown child chain: %d", chain);
- return -EINVAL;
- }
- if (!isOk(res)) {
- ALOGE("change uid(%u) rule of %d failed: %s, rule: %d, type: %d", uid, chain,
- res.msg().c_str(), rule, type);
- return -res.code();
- }
- return 0;
-}
-
-Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match,
- const std::vector<int32_t>& uids) {
- std::lock_guard guard(mMutex);
- std::set<int32_t> uidSet(uids.begin(), uids.end());
- std::vector<uint32_t> uidsToDelete;
- auto getUidsToDelete = [&uidsToDelete, &uidSet](const uint32_t& key,
- const BpfMap<uint32_t, UidOwnerValue>&) {
- if (uidSet.find((int32_t) key) == uidSet.end()) {
- uidsToDelete.push_back(key);
- }
- return base::Result<void>();
- };
- RETURN_IF_NOT_OK(mUidOwnerMap.iterate(getUidsToDelete));
-
- for(auto uid : uidsToDelete) {
- RETURN_IF_NOT_OK(removeRule(uid, match));
- }
-
- for (auto uid : uids) {
- RETURN_IF_NOT_OK(addRule(uid, match));
- }
- return netdutils::status::ok;
-}
-
-Status TrafficController::addUidInterfaceRules(const int iif,
- const std::vector<int32_t>& uidsToAdd) {
- std::lock_guard guard(mMutex);
-
- for (auto uid : uidsToAdd) {
- netdutils::Status result = addRule(uid, IIF_MATCH, iif);
- if (!isOk(result)) {
- ALOGW("addRule failed(%d): uid=%d iif=%d", result.code(), uid, iif);
- }
- }
- return netdutils::status::ok;
-}
-
-Status TrafficController::removeUidInterfaceRules(const std::vector<int32_t>& uidsToDelete) {
- std::lock_guard guard(mMutex);
-
- for (auto uid : uidsToDelete) {
- netdutils::Status result = removeRule(uid, IIF_MATCH);
- if (!isOk(result)) {
- ALOGW("removeRule failed(%d): uid=%d", result.code(), uid);
- }
- }
- return netdutils::status::ok;
-}
-
-Status TrafficController::updateUidLockdownRule(const uid_t uid, const bool add) {
- std::lock_guard guard(mMutex);
-
- netdutils::Status result = add ? addRule(uid, LOCKDOWN_VPN_MATCH)
- : removeRule(uid, LOCKDOWN_VPN_MATCH);
- if (!isOk(result)) {
- ALOGW("%s Lockdown rule failed(%d): uid=%d",
- (add ? "add": "remove"), result.code(), uid);
- }
- return result;
-}
-
-int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
- const std::vector<int32_t>& uids) {
- // FirewallRule rule = isAllowlist ? ALLOW : DENY;
- // FirewallType type = isAllowlist ? ALLOWLIST : DENYLIST;
- Status res;
- if (!name.compare(LOCAL_DOZABLE)) {
- res = replaceRulesInMap(DOZABLE_MATCH, uids);
- } else if (!name.compare(LOCAL_STANDBY)) {
- res = replaceRulesInMap(STANDBY_MATCH, uids);
- } else if (!name.compare(LOCAL_POWERSAVE)) {
- res = replaceRulesInMap(POWERSAVE_MATCH, uids);
- } else if (!name.compare(LOCAL_RESTRICTED)) {
- res = replaceRulesInMap(RESTRICTED_MATCH, uids);
- } else if (!name.compare(LOCAL_LOW_POWER_STANDBY)) {
- res = replaceRulesInMap(LOW_POWER_STANDBY_MATCH, uids);
- } else if (!name.compare(LOCAL_OEM_DENY_1)) {
- res = replaceRulesInMap(OEM_DENY_1_MATCH, uids);
- } else if (!name.compare(LOCAL_OEM_DENY_2)) {
- res = replaceRulesInMap(OEM_DENY_2_MATCH, uids);
- } else if (!name.compare(LOCAL_OEM_DENY_3)) {
- res = replaceRulesInMap(OEM_DENY_3_MATCH, uids);
- } else {
- ALOGE("unknown chain name: %s", name.c_str());
- return -EINVAL;
- }
- if (!isOk(res)) {
- ALOGE("Failed to clean up chain: %s: %s", name.c_str(), res.msg().c_str());
- return -res.code();
- }
- return 0;
-}
-
-int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
- std::lock_guard guard(mMutex);
- uint32_t key = UID_RULES_CONFIGURATION_KEY;
- auto oldConfigure = mConfigurationMap.readValue(key);
- if (!oldConfigure.ok()) {
- ALOGE("Cannot read the old configuration from map: %s",
- oldConfigure.error().message().c_str());
- return -oldConfigure.error().code();
- }
- uint32_t match;
- switch (chain) {
- case DOZABLE:
- match = DOZABLE_MATCH;
- break;
- case STANDBY:
- match = STANDBY_MATCH;
- break;
- case POWERSAVE:
- match = POWERSAVE_MATCH;
- break;
- case RESTRICTED:
- match = RESTRICTED_MATCH;
- break;
- case LOW_POWER_STANDBY:
- match = LOW_POWER_STANDBY_MATCH;
- break;
- case OEM_DENY_1:
- match = OEM_DENY_1_MATCH;
- break;
- case OEM_DENY_2:
- match = OEM_DENY_2_MATCH;
- break;
- case OEM_DENY_3:
- match = OEM_DENY_3_MATCH;
- break;
- default:
- return -EINVAL;
- }
- BpfConfig newConfiguration =
- enable ? (oldConfigure.value() | match) : (oldConfigure.value() & ~match);
- Status res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
- if (!isOk(res)) {
- ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
- }
- return -res.code();
-}
-
-Status TrafficController::swapActiveStatsMap() {
- std::lock_guard guard(mMutex);
-
- uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
- auto oldConfigure = mConfigurationMap.readValue(key);
- if (!oldConfigure.ok()) {
- ALOGE("Cannot read the old configuration from map: %s",
- oldConfigure.error().message().c_str());
- return Status(oldConfigure.error().code(), oldConfigure.error().message());
- }
-
- // Write to the configuration map to inform the kernel eBPF program to switch
- // from using one map to the other. Use flag BPF_EXIST here since the map should
- // be already populated in initMaps.
- uint32_t newConfigure = (oldConfigure.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
- auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
- BPF_EXIST);
- if (!res.ok()) {
- ALOGE("Failed to toggle the stats map: %s", strerror(res.error().code()));
- return res;
- }
- // After changing the config, we need to make sure all the current running
- // eBPF programs are finished and all the CPUs are aware of this config change
- // before we modify the old map. So we do a special hack here to wait for
- // the kernel to do a synchronize_rcu(). Once the kernel called
- // synchronize_rcu(), the config we just updated will be available to all cores
- // and the next eBPF programs triggered inside the kernel will use the new
- // map configuration. So once this function returns we can safely modify the
- // old stats map without concerning about race between the kernel and
- // userspace.
- int ret = synchronizeKernelRCU();
- if (ret) {
- ALOGE("map swap synchronize_rcu() ended with failure: %s", strerror(-ret));
- return statusFromErrno(-ret, "map swap synchronize_rcu() failed");
- }
- return netdutils::status::ok;
-}
-
-void TrafficController::setPermissionForUids(int permission, const std::vector<uid_t>& uids) {
- std::lock_guard guard(mMutex);
- if (permission == INetd::PERMISSION_UNINSTALLED) {
- for (uid_t uid : uids) {
- // Clean up all permission information for the related uid if all the
- // packages related to it are uninstalled.
- mPrivilegedUser.erase(uid);
- Status ret = mUidPermissionMap.deleteValue(uid);
- if (!isOk(ret) && ret.code() != ENOENT) {
- ALOGE("Failed to clean up the permission for %u: %s", uid, strerror(ret.code()));
- }
- }
- return;
- }
-
- bool privileged = (permission & INetd::PERMISSION_UPDATE_DEVICE_STATS);
-
- for (uid_t uid : uids) {
- if (privileged) {
- mPrivilegedUser.insert(uid);
- } else {
- mPrivilegedUser.erase(uid);
- }
-
- // The map stores all the permissions that the UID has, except if the only permission
- // the UID has is the INTERNET permission, then the UID should not appear in the map.
- if (permission != INetd::PERMISSION_INTERNET) {
- Status ret = mUidPermissionMap.writeValue(uid, permission, BPF_ANY);
- if (!isOk(ret)) {
- ALOGE("Failed to set permission: %s of uid(%u) to permission map: %s",
- UidPermissionTypeToString(permission).c_str(), uid, strerror(ret.code()));
- }
- } else {
- Status ret = mUidPermissionMap.deleteValue(uid);
- if (!isOk(ret) && ret.code() != ENOENT) {
- ALOGE("Failed to remove uid %u from permission map: %s", uid, strerror(ret.code()));
- }
- }
- }
-}
-
-} // namespace net
-} // namespace android
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
deleted file mode 100644
index 99e9831..0000000
--- a/service/native/TrafficControllerTest.cpp
+++ /dev/null
@@ -1,822 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * TrafficControllerTest.cpp - unit tests for TrafficController.cpp
- */
-
-#include <cstdint>
-#include <string>
-#include <vector>
-
-#include <fcntl.h>
-#include <inttypes.h>
-#include <linux/inet_diag.h>
-#include <linux/sock_diag.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include <gtest/gtest.h>
-
-#include <android-base/file.h>
-#include <android-base/logging.h>
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <binder/Status.h>
-
-#include <netdutils/MockSyscalls.h>
-
-#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
-#include "TrafficController.h"
-#include "bpf/BpfUtils.h"
-#include "NetdUpdatablePublic.h"
-
-using namespace android::bpf; // NOLINT(google-build-using-namespace): grandfathered
-
-namespace android {
-namespace net {
-
-using android::netdutils::Status;
-using base::Result;
-using netdutils::isOk;
-using netdutils::statusFromErrno;
-
-constexpr int TEST_MAP_SIZE = 10;
-constexpr uid_t TEST_UID = 10086;
-constexpr uid_t TEST_UID2 = 54321;
-constexpr uid_t TEST_UID3 = 98765;
-constexpr uint32_t TEST_TAG = 42;
-constexpr uint32_t TEST_COUNTERSET = 1;
-constexpr int TEST_IFINDEX = 999;
-constexpr int RXPACKETS = 1;
-constexpr int RXBYTES = 100;
-constexpr int TXPACKETS = 0;
-constexpr int TXBYTES = 0;
-
-#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
-#define ASSERT_INVALID(x) ASSERT_FALSE((x).isValid())
-
-class TrafficControllerTest : public ::testing::Test {
- protected:
- TrafficControllerTest() {}
- TrafficController mTc;
- BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
- BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
- BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
- BpfMap<StatsKey, StatsValue> mFakeStatsMapB; // makeTrafficControllerMapsInvalid only
- BpfMap<uint32_t, StatsValue> mFakeIfaceStatsMap; ; // makeTrafficControllerMapsInvalid only
- BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
- BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
- BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
- BpfMap<uint32_t, uint8_t> mFakeUidCounterSetMap;
- BpfMap<uint32_t, IfaceValue> mFakeIfaceIndexNameMap;
-
- void SetUp() {
- std::lock_guard guard(mTc.mMutex);
- ASSERT_EQ(0, setrlimitForTest());
-
- mFakeCookieTagMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeCookieTagMap);
-
- mFakeAppUidStatsMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeAppUidStatsMap);
-
- mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeStatsMapA);
-
- mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
- ASSERT_VALID(mFakeConfigurationMap);
-
- mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeUidOwnerMap);
- mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeUidPermissionMap);
-
- mFakeUidCounterSetMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeUidCounterSetMap);
-
- mFakeIfaceIndexNameMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
- ASSERT_VALID(mFakeIfaceIndexNameMap);
-
- mTc.mCookieTagMap = mFakeCookieTagMap;
- ASSERT_VALID(mTc.mCookieTagMap);
- mTc.mAppUidStatsMap = mFakeAppUidStatsMap;
- ASSERT_VALID(mTc.mAppUidStatsMap);
- mTc.mStatsMapA = mFakeStatsMapA;
- ASSERT_VALID(mTc.mStatsMapA);
- mTc.mConfigurationMap = mFakeConfigurationMap;
- ASSERT_VALID(mTc.mConfigurationMap);
-
- // Always write to stats map A by default.
- static_assert(SELECT_MAP_A == 0);
-
- mTc.mUidOwnerMap = mFakeUidOwnerMap;
- ASSERT_VALID(mTc.mUidOwnerMap);
- mTc.mUidPermissionMap = mFakeUidPermissionMap;
- ASSERT_VALID(mTc.mUidPermissionMap);
- mTc.mPrivilegedUser.clear();
-
- mTc.mUidCounterSetMap = mFakeUidCounterSetMap;
- ASSERT_VALID(mTc.mUidCounterSetMap);
-
- mTc.mIfaceIndexNameMap = mFakeIfaceIndexNameMap;
- ASSERT_VALID(mTc.mIfaceIndexNameMap);
- }
-
- void populateFakeStats(uint64_t cookie, uint32_t uid, uint32_t tag, StatsKey* key) {
- UidTagValue cookieMapkey = {.uid = (uint32_t)uid, .tag = tag};
- EXPECT_RESULT_OK(mFakeCookieTagMap.writeValue(cookie, cookieMapkey, BPF_ANY));
- *key = {.uid = uid, .tag = tag, .counterSet = TEST_COUNTERSET, .ifaceIndex = TEST_IFINDEX};
- StatsValue statsMapValue = {.rxPackets = RXPACKETS, .rxBytes = RXBYTES,
- .txPackets = TXPACKETS, .txBytes = TXBYTES};
- EXPECT_RESULT_OK(mFakeStatsMapA.writeValue(*key, statsMapValue, BPF_ANY));
- EXPECT_RESULT_OK(mFakeAppUidStatsMap.writeValue(uid, statsMapValue, BPF_ANY));
- // put tag information back to statsKey
- key->tag = tag;
- }
-
- void populateFakeCounterSet(uint32_t uid, uint32_t counterSet) {
- EXPECT_RESULT_OK(mFakeUidCounterSetMap.writeValue(uid, counterSet, BPF_ANY));
- }
-
- void populateFakeIfaceIndexName(const char* name, uint32_t ifaceIndex) {
- if (name == nullptr || ifaceIndex <= 0) return;
-
- IfaceValue iface;
- strlcpy(iface.name, name, sizeof(IfaceValue));
- EXPECT_RESULT_OK(mFakeIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY));
- }
-
- void checkUidOwnerRuleForChain(ChildChain chain, UidOwnerMatchType match) {
- uint32_t uid = TEST_UID;
- EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, DENYLIST));
- Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_RESULT_OK(value);
- EXPECT_TRUE(value.value().rule & match);
-
- uid = TEST_UID2;
- EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, ALLOWLIST));
- value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_RESULT_OK(value);
- EXPECT_TRUE(value.value().rule & match);
-
- EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, DENY, ALLOWLIST));
- value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_FALSE(value.ok());
- EXPECT_EQ(ENOENT, value.error().code());
-
- uid = TEST_UID;
- EXPECT_EQ(0, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
- value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_FALSE(value.ok());
- EXPECT_EQ(ENOENT, value.error().code());
-
- uid = TEST_UID3;
- EXPECT_EQ(-ENOENT, mTc.changeUidOwnerRule(chain, uid, ALLOW, DENYLIST));
- value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_FALSE(value.ok());
- EXPECT_EQ(ENOENT, value.error().code());
- }
-
- void checkEachUidValue(const std::vector<int32_t>& uids, UidOwnerMatchType match) {
- for (uint32_t uid : uids) {
- Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_RESULT_OK(value);
- EXPECT_TRUE(value.value().rule & match);
- }
- std::set<uint32_t> uidSet(uids.begin(), uids.end());
- const auto checkNoOtherUid = [&uidSet](const int32_t& key,
- const BpfMap<uint32_t, UidOwnerValue>&) {
- EXPECT_NE(uidSet.end(), uidSet.find(key));
- return Result<void>();
- };
- EXPECT_RESULT_OK(mFakeUidOwnerMap.iterate(checkNoOtherUid));
- }
-
- void checkUidMapReplace(const std::string& name, const std::vector<int32_t>& uids,
- UidOwnerMatchType match) {
- bool isAllowlist = true;
- EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
- checkEachUidValue(uids, match);
-
- isAllowlist = false;
- EXPECT_EQ(0, mTc.replaceUidOwnerMap(name, isAllowlist, uids));
- checkEachUidValue(uids, match);
- }
-
- void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint32_t expectedRule,
- uint32_t expectedIif) {
- for (uint32_t uid : appUids) {
- Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
- EXPECT_RESULT_OK(value);
- EXPECT_EQ(expectedRule, value.value().rule)
- << "Expected rule for UID " << uid << " to be " << expectedRule << ", but was "
- << value.value().rule;
- EXPECT_EQ(expectedIif, value.value().iif)
- << "Expected iif for UID " << uid << " to be " << expectedIif << ", but was "
- << value.value().iif;
- }
- }
-
- template <class Key, class Value>
- void expectMapEmpty(BpfMap<Key, Value>& map) {
- auto isEmpty = map.isEmpty();
- EXPECT_RESULT_OK(isEmpty);
- EXPECT_TRUE(isEmpty.value());
- }
-
- void expectUidPermissionMapValues(const std::vector<uid_t>& appUids, uint8_t expectedValue) {
- for (uid_t uid : appUids) {
- Result<uint8_t> value = mFakeUidPermissionMap.readValue(uid);
- EXPECT_RESULT_OK(value);
- EXPECT_EQ(expectedValue, value.value())
- << "Expected value for UID " << uid << " to be " << expectedValue
- << ", but was " << value.value();
- }
- }
-
- void expectPrivilegedUserSet(const std::vector<uid_t>& appUids) {
- std::lock_guard guard(mTc.mMutex);
- EXPECT_EQ(appUids.size(), mTc.mPrivilegedUser.size());
- for (uid_t uid : appUids) {
- EXPECT_NE(mTc.mPrivilegedUser.end(), mTc.mPrivilegedUser.find(uid));
- }
- }
-
- void expectPrivilegedUserSetEmpty() {
- std::lock_guard guard(mTc.mMutex);
- EXPECT_TRUE(mTc.mPrivilegedUser.empty());
- }
-
- Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
- UidOwnerMatchType matchType, TrafficController::IptOp op) {
- Status ret(0);
- for (auto uid : appUids) {
- ret = mTc.updateUidOwnerMap(uid, matchType, op);
- if(!isOk(ret)) break;
- }
- return ret;
- }
-};
-
-TEST_F(TrafficControllerTest, TestUpdateOwnerMapEntry) {
- uint32_t uid = TEST_UID;
- ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, DENY, DENYLIST)));
- Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
- ASSERT_RESULT_OK(value);
- ASSERT_TRUE(value.value().rule & STANDBY_MATCH);
-
- ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, ALLOW, ALLOWLIST)));
- value = mFakeUidOwnerMap.readValue(uid);
- ASSERT_RESULT_OK(value);
- ASSERT_TRUE(value.value().rule & DOZABLE_MATCH);
-
- ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(DOZABLE_MATCH, uid, DENY, ALLOWLIST)));
- value = mFakeUidOwnerMap.readValue(uid);
- ASSERT_RESULT_OK(value);
- ASSERT_FALSE(value.value().rule & DOZABLE_MATCH);
-
- ASSERT_TRUE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
- ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
-
- uid = TEST_UID2;
- ASSERT_FALSE(isOk(mTc.updateOwnerMapEntry(STANDBY_MATCH, uid, ALLOW, DENYLIST)));
- ASSERT_FALSE(mFakeUidOwnerMap.readValue(uid).ok());
-}
-
-TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) {
- checkUidOwnerRuleForChain(DOZABLE, DOZABLE_MATCH);
- checkUidOwnerRuleForChain(STANDBY, STANDBY_MATCH);
- checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
- checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
- checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
- checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
- checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
- checkUidOwnerRuleForChain(OEM_DENY_3, OEM_DENY_3_MATCH);
- ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
- ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
-}
-
-TEST_F(TrafficControllerTest, TestReplaceUidOwnerMap) {
- std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
- checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
- checkUidMapReplace("fw_standby", uids, STANDBY_MATCH);
- checkUidMapReplace("fw_powersave", uids, POWERSAVE_MATCH);
- checkUidMapReplace("fw_restricted", uids, RESTRICTED_MATCH);
- checkUidMapReplace("fw_low_power_standby", uids, LOW_POWER_STANDBY_MATCH);
- checkUidMapReplace("fw_oem_deny_1", uids, OEM_DENY_1_MATCH);
- checkUidMapReplace("fw_oem_deny_2", uids, OEM_DENY_2_MATCH);
- checkUidMapReplace("fw_oem_deny_3", uids, OEM_DENY_3_MATCH);
- ASSERT_EQ(-EINVAL, mTc.replaceUidOwnerMap("unknow", true, uids));
-}
-
-TEST_F(TrafficControllerTest, TestReplaceSameChain) {
- std::vector<int32_t> uids = {TEST_UID, TEST_UID2, TEST_UID3};
- checkUidMapReplace("fw_dozable", uids, DOZABLE_MATCH);
- std::vector<int32_t> newUids = {TEST_UID2, TEST_UID3};
- checkUidMapReplace("fw_dozable", newUids, DOZABLE_MATCH);
-}
-
-TEST_F(TrafficControllerTest, TestDenylistUidMatch) {
- std::vector<uint32_t> appUids = {1000, 1001, 10012};
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOpInsert)));
- expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOpDelete)));
- expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestAllowlistUidMatch) {
- std::vector<uint32_t> appUids = {1000, 1001, 10012};
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
- expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
- expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestReplaceMatchUid) {
- std::vector<uint32_t> appUids = {1000, 1001, 10012};
- // Add appUids to the denylist and expect that their values are all PENALTY_BOX_MATCH.
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOpInsert)));
- expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
-
- // Add the same UIDs to the allowlist and expect that we get PENALTY_BOX_MATCH |
- // HAPPY_BOX_MATCH.
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpInsert)));
- expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH | PENALTY_BOX_MATCH, 0);
-
- // Remove the same UIDs from the allowlist and check the PENALTY_BOX_MATCH is still there.
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH, TrafficController::IptOpDelete)));
- expectUidOwnerMapValues(appUids, PENALTY_BOX_MATCH, 0);
-
- // Remove the same UIDs from the denylist and check the map is empty.
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOpDelete)));
- ASSERT_FALSE(mFakeUidOwnerMap.getFirstKey().ok());
-}
-
-TEST_F(TrafficControllerTest, TestDeleteWrongMatchSilentlyFails) {
- std::vector<uint32_t> appUids = {1000, 1001, 10012};
- // If the uid does not exist in the map, trying to delete a rule about it will fail.
- ASSERT_FALSE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
- TrafficController::IptOpDelete)));
- expectMapEmpty(mFakeUidOwnerMap);
-
- // Add denylist rules for appUids.
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, HAPPY_BOX_MATCH,
- TrafficController::IptOpInsert)));
- expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
-
- // Delete (non-existent) denylist rules for appUids, and check that this silently does
- // nothing if the uid is in the map but does not have denylist match. This is required because
- // NetworkManagementService will try to remove a uid from denylist after adding it to the
- // allowlist and if the remove fails it will not update the uid status.
- ASSERT_TRUE(isOk(updateUidOwnerMaps(appUids, PENALTY_BOX_MATCH,
- TrafficController::IptOpDelete)));
- expectUidOwnerMapValues(appUids, HAPPY_BOX_MATCH, 0);
-}
-
-TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRules) {
- int iif0 = 15;
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
- expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
-
- // Add some non-overlapping new uids. They should coexist with existing rules
- int iif1 = 16;
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
- expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
- expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
-
- // Overwrite some existing uids
- int iif2 = 17;
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif2, {1000, 2000})));
- expectUidOwnerMapValues({1001}, IIF_MATCH, iif0);
- expectUidOwnerMapValues({2001}, IIF_MATCH, iif1);
- expectUidOwnerMapValues({1000, 2000}, IIF_MATCH, iif2);
-}
-
-TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRules) {
- int iif0 = 15;
- int iif1 = 16;
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif0, {1000, 1001})));
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {2000, 2001})));
- expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif0);
- expectUidOwnerMapValues({2000, 2001}, IIF_MATCH, iif1);
-
- // Rmove some uids
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001, 2001})));
- expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
- expectUidOwnerMapValues({2000}, IIF_MATCH, iif1);
- checkEachUidValue({1000, 2000}, IIF_MATCH); // Make sure there are only two uids remaining
-
- // Remove non-existent uids shouldn't fail
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({2000, 3000})));
- expectUidOwnerMapValues({1000}, IIF_MATCH, iif0);
- checkEachUidValue({1000}, IIF_MATCH); // Make sure there are only one uid remaining
-
- // Remove everything
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
- expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestUpdateUidLockdownRule) {
- // Add Lockdown rules
- ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, true /* add */)));
- ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, true /* add */)));
- expectUidOwnerMapValues({1000, 1001}, LOCKDOWN_VPN_MATCH, 0);
-
- // Remove one of Lockdown rules
- ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, false /* add */)));
- expectUidOwnerMapValues({1001}, LOCKDOWN_VPN_MATCH, 0);
-
- // Remove remaining Lockdown rule
- ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, false /* add */)));
- expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
- // Set up existing PENALTY_BOX_MATCH rules
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
- TrafficController::IptOpInsert)));
- expectUidOwnerMapValues({1000, 1001, 10012}, PENALTY_BOX_MATCH, 0);
-
- // Add some partially-overlapping uid owner rules and check result
- int iif1 = 32;
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10012, 10013, 10014})));
- expectUidOwnerMapValues({1000, 1001}, PENALTY_BOX_MATCH, 0);
- expectUidOwnerMapValues({10012}, PENALTY_BOX_MATCH | IIF_MATCH, iif1);
- expectUidOwnerMapValues({10013, 10014}, IIF_MATCH, iif1);
-
- // Removing some PENALTY_BOX_MATCH rules should not change uid interface rule
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1001, 10012}, PENALTY_BOX_MATCH,
- TrafficController::IptOpDelete)));
- expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
- expectUidOwnerMapValues({10012, 10013, 10014}, IIF_MATCH, iif1);
-
- // Remove all uid interface rules
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({10012, 10013, 10014})));
- expectUidOwnerMapValues({1000}, PENALTY_BOX_MATCH, 0);
- // Make sure these are the only uids left
- checkEachUidValue({1000}, PENALTY_BOX_MATCH);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatches) {
- int iif1 = 56;
- // Set up existing uid interface rules
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif1, {10001, 10002})));
- expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
-
- // Add some partially-overlapping doze rules
- EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {10002, 10003}));
- expectUidOwnerMapValues({10001}, IIF_MATCH, iif1);
- expectUidOwnerMapValues({10002}, DOZABLE_MATCH | IIF_MATCH, iif1);
- expectUidOwnerMapValues({10003}, DOZABLE_MATCH, 0);
-
- // Introduce a third rule type (powersave) on various existing UIDs
- EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {10000, 10001, 10002, 10003}));
- expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
- expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
- expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif1);
- expectUidOwnerMapValues({10003}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
-
- // Remove all doze rules
- EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_dozable", true, {}));
- expectUidOwnerMapValues({10000}, POWERSAVE_MATCH, 0);
- expectUidOwnerMapValues({10001}, POWERSAVE_MATCH | IIF_MATCH, iif1);
- expectUidOwnerMapValues({10002}, POWERSAVE_MATCH | IIF_MATCH, iif1);
- expectUidOwnerMapValues({10003}, POWERSAVE_MATCH, 0);
-
- // Remove all powersave rules, expect ownerMap to only have uid interface rules left
- EXPECT_EQ(0, mTc.replaceUidOwnerMap("fw_powersave", true, {}));
- expectUidOwnerMapValues({10001, 10002}, IIF_MATCH, iif1);
- // Make sure these are the only uids left
- checkEachUidValue({10001, 10002}, IIF_MATCH);
-}
-
-TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
- // iif=0 is a wildcard
- int iif = 0;
- // Add interface rule with wildcard to uids
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
- expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
-}
-
-TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
- // iif=0 is a wildcard
- int iif = 0;
- // Add interface rule with wildcard to two uids
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
- expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
-
- // Remove interface rule from one of the uids
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
- expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
- checkEachUidValue({1001}, IIF_MATCH);
-
- // Remove interface rule from the remaining uid
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
- expectMapEmpty(mFakeUidOwnerMap);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
- // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
- TrafficController::IptOpInsert)));
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
- TrafficController::IptOpInsert)));
-
- // iif=0 is a wildcard
- int iif = 0;
- // Add interface rule with wildcard to the existing uid
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
- expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
-
- // Remove interface rule with wildcard from the existing uid
- ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
- expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
-}
-
-TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
- // iif=0 is a wildcard
- int iif = 0;
- // Set up existing interface rule with wildcard
- ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
-
- // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
- TrafficController::IptOpInsert)));
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
- TrafficController::IptOpInsert)));
- expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
-
- // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
- TrafficController::IptOpDelete)));
- ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
- TrafficController::IptOpDelete)));
- expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
-}
-
-TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
- expectMapEmpty(mFakeUidPermissionMap);
- expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, TestRevokeInternetPermission) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
- expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-}
-
-TEST_F(TrafficControllerTest, TestPermissionUninstalled) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
- expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
- expectPrivilegedUserSet(appUids);
-
- std::vector<uid_t> uidToRemove = {TEST_UID};
- mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidToRemove);
-
- std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
- expectUidPermissionMapValues(uidRemain, INetd::PERMISSION_UPDATE_DEVICE_STATS);
- expectPrivilegedUserSet(uidRemain);
-
- mTc.setPermissionForUids(INetd::PERMISSION_UNINSTALLED, uidRemain);
- expectMapEmpty(mFakeUidPermissionMap);
- expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, TestGrantUpdateStatsPermission) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
- expectUidPermissionMapValues(appUids, INetd::PERMISSION_UPDATE_DEVICE_STATS);
- expectPrivilegedUserSet(appUids);
-
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
- expectPrivilegedUserSetEmpty();
- expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-}
-
-TEST_F(TrafficControllerTest, TestRevokeUpdateStatsPermission) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
- expectPrivilegedUserSet(appUids);
-
- std::vector<uid_t> uidToRemove = {TEST_UID};
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidToRemove);
-
- std::vector<uid_t> uidRemain = {TEST_UID3, TEST_UID2};
- expectPrivilegedUserSet(uidRemain);
-
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, uidRemain);
- expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, TestGrantWrongPermission) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
- expectPrivilegedUserSetEmpty();
- expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-}
-
-TEST_F(TrafficControllerTest, TestGrantDuplicatePermissionSlientlyFail) {
- std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
-
- mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, appUids);
- expectMapEmpty(mFakeUidPermissionMap);
-
- std::vector<uid_t> uidToAdd = {TEST_UID};
- mTc.setPermissionForUids(INetd::PERMISSION_INTERNET, uidToAdd);
-
- expectPrivilegedUserSetEmpty();
-
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
- expectUidPermissionMapValues(appUids, INetd::PERMISSION_NONE);
-
- mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, appUids);
- expectPrivilegedUserSet(appUids);
-
- mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, uidToAdd);
- expectPrivilegedUserSet(appUids);
-
- mTc.setPermissionForUids(INetd::PERMISSION_NONE, appUids);
- expectPrivilegedUserSetEmpty();
-}
-
-TEST_F(TrafficControllerTest, getFirewallType) {
- static const struct TestConfig {
- ChildChain childChain;
- FirewallType firewallType;
- } testConfigs[] = {
- // clang-format off
- {NONE, DENYLIST},
- {DOZABLE, ALLOWLIST},
- {STANDBY, DENYLIST},
- {POWERSAVE, ALLOWLIST},
- {RESTRICTED, ALLOWLIST},
- {LOW_POWER_STANDBY, ALLOWLIST},
- {OEM_DENY_1, DENYLIST},
- {OEM_DENY_2, DENYLIST},
- {OEM_DENY_3, DENYLIST},
- {INVALID_CHAIN, DENYLIST},
- // clang-format on
- };
-
- for (const auto& config : testConfigs) {
- SCOPED_TRACE(fmt::format("testConfig: [{}, {}]", config.childChain, config.firewallType));
- EXPECT_EQ(config.firewallType, mTc.getFirewallType(config.childChain));
- }
-}
-
-constexpr uint32_t SOCK_CLOSE_WAIT_US = 30 * 1000;
-constexpr uint32_t ENOBUFS_POLL_WAIT_US = 10 * 1000;
-
-using android::base::Error;
-using android::base::Result;
-using android::bpf::BpfMap;
-
-// This test set up a SkDestroyListener that is running parallel with the production
-// SkDestroyListener. The test will create thousands of sockets and tag them on the
-// production cookieUidTagMap and close them in a short time. When the number of
-// sockets get closed exceeds the buffer size, it will start to return ENOBUFF
-// error. The error will be ignored by the production SkDestroyListener and the
-// test will clean up the tags in tearDown if there is any remains.
-
-// TODO: Instead of test the ENOBUFF error, we can test the production
-// SkDestroyListener to see if it failed to delete a tagged socket when ENOBUFF
-// triggered.
-class NetlinkListenerTest : public testing::Test {
- protected:
- NetlinkListenerTest() {}
- BpfMap<uint64_t, UidTagValue> mCookieTagMap;
-
- void SetUp() {
- mCookieTagMap.init(COOKIE_TAG_MAP_PATH);
- ASSERT_TRUE(mCookieTagMap.isValid());
- }
-
- void TearDown() {
- const auto deleteTestCookieEntries = [](const uint64_t& key, const UidTagValue& value,
- BpfMap<uint64_t, UidTagValue>& map) {
- if ((value.uid == TEST_UID) && (value.tag == TEST_TAG)) {
- Result<void> res = map.deleteValue(key);
- if (res.ok() || (res.error().code() == ENOENT)) {
- return Result<void>();
- }
- ALOGE("Failed to delete data(cookie = %" PRIu64 "): %s", key,
- strerror(res.error().code()));
- }
- // Move forward to next cookie in the map.
- return Result<void>();
- };
- EXPECT_RESULT_OK(mCookieTagMap.iterateWithValue(deleteTestCookieEntries));
- }
-
- Result<void> checkNoGarbageTagsExist() {
- const auto checkGarbageTags = [](const uint64_t&, const UidTagValue& value,
- const BpfMap<uint64_t, UidTagValue>&) -> Result<void> {
- if ((TEST_UID == value.uid) && (TEST_TAG == value.tag)) {
- return Error(EUCLEAN) << "Closed socket is not untagged";
- }
- return {};
- };
- return mCookieTagMap.iterateWithValue(checkGarbageTags);
- }
-
- bool checkMassiveSocketDestroy(int totalNumber, bool expectError) {
- std::unique_ptr<android::netdutils::NetlinkListenerInterface> skDestroyListener;
- auto result = android::net::TrafficController::makeSkDestroyListener();
- if (!isOk(result)) {
- ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
- } else {
- skDestroyListener = std::move(result.value());
- }
- int rxErrorCount = 0;
- // Rx handler extracts nfgenmsg looks up and invokes registered dispatch function.
- const auto rxErrorHandler = [&rxErrorCount](const int, const int) { rxErrorCount++; };
- skDestroyListener->registerSkErrorHandler(rxErrorHandler);
- int fds[totalNumber];
- for (int i = 0; i < totalNumber; i++) {
- fds[i] = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
- // The likely reason for a failure is running out of available file descriptors.
- EXPECT_LE(0, fds[i]) << i << " of " << totalNumber;
- if (fds[i] < 0) {
- // EXPECT_LE already failed above, so test case is a failure, but we don't
- // want potentially tens of thousands of extra failures creating and then
- // closing all these fds cluttering up the logs.
- totalNumber = i;
- break;
- };
- libnetd_updatable_tagSocket(fds[i], TEST_TAG, TEST_UID, 1000);
- }
-
- // TODO: Use a separate thread that has its own fd table so we can
- // close sockets even faster simply by terminating that thread.
- for (int i = 0; i < totalNumber; i++) {
- EXPECT_EQ(0, close(fds[i]));
- }
- // wait a bit for netlink listener to handle all the messages.
- usleep(SOCK_CLOSE_WAIT_US);
- if (expectError) {
- // If ENOBUFS triggered, check it only called into the handler once, ie.
- // that the netlink handler is not spinning.
- int currentErrorCount = rxErrorCount;
- // 0 error count is acceptable because the system has chances to close all sockets
- // normally.
- EXPECT_LE(0, rxErrorCount);
- if (!rxErrorCount) return true;
-
- usleep(ENOBUFS_POLL_WAIT_US);
- EXPECT_EQ(currentErrorCount, rxErrorCount);
- } else {
- EXPECT_RESULT_OK(checkNoGarbageTagsExist());
- EXPECT_EQ(0, rxErrorCount);
- }
- return false;
- }
-};
-
-TEST_F(NetlinkListenerTest, TestAllSocketUntagged) {
- checkMassiveSocketDestroy(10, false);
- checkMassiveSocketDestroy(100, false);
-}
-
-// Disabled because flaky on blueline-userdebug; this test relies on the main thread
-// winning a race against the NetlinkListener::run() thread. There's no way to ensure
-// things will be scheduled the same way across all architectures and test environments.
-TEST_F(NetlinkListenerTest, DISABLED_TestSkDestroyError) {
- bool needRetry = false;
- int retryCount = 0;
- do {
- needRetry = checkMassiveSocketDestroy(32500, true);
- if (needRetry) retryCount++;
- } while (needRetry && retryCount < 3);
- // Should review test if it can always close all sockets correctly.
- EXPECT_GT(3, retryCount);
-}
-
-
-} // namespace net
-} // namespace android
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
deleted file mode 100644
index 03f449a..0000000
--- a/service/native/include/Common.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-// TODO: deduplicate with the constants in NetdConstants.h.
-#include <aidl/android/net/INetd.h>
-#include "clat_mark.h"
-
-using aidl::android::net::INetd;
-
-static_assert(INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
-
-enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
-
-// ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
-// DENYLIST means the firewall allows all by default, uids must be explicitly DENYed
-
-enum FirewallType { ALLOWLIST = INetd::FIREWALL_ALLOWLIST, DENYLIST = INetd::FIREWALL_DENYLIST };
-
-// LINT.IfChange(firewall_chain)
-enum ChildChain {
- NONE = 0,
- DOZABLE = 1,
- STANDBY = 2,
- POWERSAVE = 3,
- RESTRICTED = 4,
- LOW_POWER_STANDBY = 5,
- OEM_DENY_1 = 7,
- OEM_DENY_2 = 8,
- OEM_DENY_3 = 9,
- INVALID_CHAIN
-};
-// LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
deleted file mode 100644
index 86cf50a..0000000
--- a/service/native/include/TrafficController.h
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <set>
-#include <Common.h>
-
-#include "android-base/thread_annotations.h"
-#include "bpf/BpfMap.h"
-#include "netd.h"
-#include "netdutils/NetlinkListener.h"
-#include "netdutils/StatusOr.h"
-
-namespace android {
-namespace net {
-
-using netdutils::StatusOr;
-
-class TrafficController {
- public:
- /*
- * Initialize the whole controller
- */
- netdutils::Status start(bool startSkDestroyListener);
-
- /*
- * Swap the stats map config from current active stats map to the idle one.
- */
- netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
-
- int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
-
- int removeUidOwnerRule(const uid_t uid);
-
- int replaceUidOwnerMap(const std::string& name, bool isAllowlist,
- const std::vector<int32_t>& uids);
-
- enum IptOp { IptOpInsert, IptOpDelete };
-
- netdutils::Status updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
- FirewallType type) EXCLUDES(mMutex);
-
- netdutils::Status replaceRulesInMap(UidOwnerMatchType match, const std::vector<int32_t>& uids)
- EXCLUDES(mMutex);
-
- netdutils::Status addUidInterfaceRules(const int ifIndex, const std::vector<int32_t>& uids)
- EXCLUDES(mMutex);
- netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
-
- netdutils::Status updateUidLockdownRule(const uid_t uid, const bool add) EXCLUDES(mMutex);
-
- netdutils::Status updateUidOwnerMap(const uint32_t uid,
- UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
-
- int toggleUidOwnerMap(ChildChain chain, bool enable) EXCLUDES(mMutex);
-
- static netdutils::StatusOr<std::unique_ptr<netdutils::NetlinkListenerInterface>>
- makeSkDestroyListener();
-
- void setPermissionForUids(int permission, const std::vector<uid_t>& uids) EXCLUDES(mMutex);
-
- FirewallType getFirewallType(ChildChain);
-
- static const char* LOCAL_DOZABLE;
- static const char* LOCAL_STANDBY;
- static const char* LOCAL_POWERSAVE;
- static const char* LOCAL_RESTRICTED;
- static const char* LOCAL_LOW_POWER_STANDBY;
- static const char* LOCAL_OEM_DENY_1;
- static const char* LOCAL_OEM_DENY_2;
- static const char* LOCAL_OEM_DENY_3;
-
- private:
- /*
- * mCookieTagMap: Store the corresponding tag and uid for a specific socket.
- * DO NOT hold any locks when modifying this map, otherwise when the untag
- * operation is waiting for a lock hold by other process and there are more
- * sockets being closed than can fit in the socket buffer of the netlink socket
- * that receives them, then the kernel will drop some of these sockets and we
- * won't delete their tags.
- * Map Key: uint64_t socket cookie
- * Map Value: UidTagValue, contains a uint32 uid and a uint32 tag.
- */
- bpf::BpfMap<uint64_t, UidTagValue> mCookieTagMap GUARDED_BY(mMutex);
-
- /*
- * mUidCounterSetMap: Store the counterSet of a specific uid.
- * Map Key: uint32 uid.
- * Map Value: uint32 counterSet specifies if the traffic is a background
- * or foreground traffic.
- */
- bpf::BpfMap<uint32_t, uint8_t> mUidCounterSetMap GUARDED_BY(mMutex);
-
- /*
- * mAppUidStatsMap: Store the total traffic stats for a uid regardless of
- * tag, counterSet and iface. The stats is used by TrafficStats.getUidStats
- * API to return persistent stats for a specific uid since device boot.
- */
- bpf::BpfMap<uint32_t, StatsValue> mAppUidStatsMap;
-
- /*
- * mStatsMapA/mStatsMapB: Store the traffic statistics for a specific
- * combination of uid, tag, iface and counterSet. These two maps contain
- * both tagged and untagged traffic.
- * Map Key: StatsKey contains the uid, tag, counterSet and ifaceIndex
- * information.
- * Map Value: Stats, contains packet count and byte count of each
- * transport protocol on egress and ingress direction.
- */
- bpf::BpfMap<StatsKey, StatsValue> mStatsMapA GUARDED_BY(mMutex);
-
- bpf::BpfMap<StatsKey, StatsValue> mStatsMapB GUARDED_BY(mMutex);
-
- /*
- * mIfaceIndexNameMap: Store the index name pair of each interface show up
- * on the device since boot. The interface index is used by the eBPF program
- * to correctly match the iface name when receiving a packet.
- */
- bpf::BpfMap<uint32_t, IfaceValue> mIfaceIndexNameMap;
-
- /*
- * mIfaceStataMap: Store per iface traffic stats gathered from xt_bpf
- * filter.
- */
- bpf::BpfMap<uint32_t, StatsValue> mIfaceStatsMap;
-
- /*
- * mConfigurationMap: Store the current network policy about uid filtering
- * and the current stats map in use. There are two configuration entries in
- * the map right now:
- * - Entry with UID_RULES_CONFIGURATION_KEY:
- * Store the configuration for the current uid rules. It indicates the device
- * is in doze/powersave/standby/restricted/low power standby/oem deny mode.
- * - Entry with CURRENT_STATS_MAP_CONFIGURATION_KEY:
- * Stores the current live stats map that kernel program is writing to.
- * Userspace can do scraping and cleaning job on the other one depending on the
- * current configs.
- */
- bpf::BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
-
- /*
- * mUidOwnerMap: Store uids that are used for bandwidth control uid match.
- */
- bpf::BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
-
- /*
- * mUidOwnerMap: Store uids that are used for INTERNET permission check.
- */
- bpf::BpfMap<uint32_t, uint8_t> mUidPermissionMap GUARDED_BY(mMutex);
-
- std::unique_ptr<netdutils::NetlinkListenerInterface> mSkDestroyListener;
-
- netdutils::Status removeRule(uint32_t uid, UidOwnerMatchType match) REQUIRES(mMutex);
-
- netdutils::Status addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif = 0)
- REQUIRES(mMutex);
-
- std::mutex mMutex;
-
- netdutils::Status initMaps() EXCLUDES(mMutex);
-
- // Keep track of uids that have permission UPDATE_DEVICE_STATS so we don't
- // need to call back to system server for permission check.
- std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
-
- // For testing
- friend class TrafficControllerTest;
-};
-
-} // namespace net
-} // namespace android
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 996706e..5c6b123 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -43,11 +43,15 @@
srcs: [
"clatutils_test.cpp",
],
+ header_libs: [
+ "bpf_connectivity_headers",
+ ],
static_libs: [
"libbase",
"libclat",
"libip_checksum",
"libnetd_test_tun_interface",
+ "netd_aidl_interface-lateststable-ndk",
],
shared_libs: [
"liblog",
diff --git a/service/native/libs/libclat/clatutils_test.cpp b/service/native/libs/libclat/clatutils_test.cpp
index f4f97db..cf6492f 100644
--- a/service/native/libs/libclat/clatutils_test.cpp
+++ b/service/native/libs/libclat/clatutils_test.cpp
@@ -26,6 +26,10 @@
#include "checksum.h"
}
+#include <aidl/android/net/INetd.h>
+#include "clat_mark.h"
+static_assert(aidl::android::net::INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
+
// Default translation parameters.
static const char kIPv4LocalAddr[] = "192.0.0.4";
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index 6a34a24..ad9cfbe 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -19,23 +19,22 @@
import static android.net.BpfNetMapsConstants.CONFIGURATION_MAP_PATH;
import static android.net.BpfNetMapsConstants.COOKIE_TAG_MAP_PATH;
import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_MAP_PATH;
import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
+import static android.net.BpfNetMapsConstants.INGRESS_DISCARD_MAP_PATH;
import static android.net.BpfNetMapsConstants.LOCKDOWN_VPN_MATCH;
import static android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.UID_OWNER_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_PERMISSION_MAP_PATH;
import static android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsUtils.PRE_T;
import static android.net.BpfNetMapsUtils.getMatchByFirewallChain;
+import static android.net.BpfNetMapsUtils.isFirewallAllowList;
import static android.net.BpfNetMapsUtils.matchToString;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.INetd.PERMISSION_INTERNET;
@@ -51,7 +50,9 @@
import android.app.StatsManager;
import android.content.Context;
+import android.net.BpfNetMapsReader;
import android.net.INetd;
+import android.net.UidOwnerValue;
import android.os.Build;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -67,10 +68,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.BackgroundThread;
-import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BpfDump;
import com.android.net.module.util.BpfMap;
-import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.S32;
@@ -78,9 +77,12 @@
import com.android.net.module.util.Struct.U8;
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.bpf.IngressDiscardKey;
+import com.android.net.module.util.bpf.IngressDiscardValue;
import java.io.FileDescriptor;
import java.io.IOException;
+import java.net.InetAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@@ -92,7 +94,6 @@
* {@hide}
*/
public class BpfNetMaps {
- private static final boolean PRE_T = !SdkLevel.isAtLeastT();
static {
if (!PRE_T) {
System.loadLibrary("service-connectivity");
@@ -105,10 +106,6 @@
// Use legacy netd for releases before T.
private static boolean sInitialized = false;
- private static Boolean sEnableJavaBpfMap = null;
- private static final String BPF_NET_MAPS_FORCE_DISABLE_JAVA_BPF_MAP =
- "bpf_net_maps_force_disable_java_bpf_map";
-
// Lock for sConfigurationMap entry for UID_RULES_CONFIGURATION_KEY.
// This entry is not accessed by others.
// BpfNetMaps acquires this lock while sequence of read, modify, and write.
@@ -128,6 +125,9 @@
private static IBpfMap<S32, UidOwnerValue> sUidOwnerMap = null;
private static IBpfMap<S32, U8> sUidPermissionMap = null;
private static IBpfMap<CookieTagMapKey, CookieTagMapValue> sCookieTagMap = null;
+ // TODO: Add BOOL class and replace U8?
+ private static IBpfMap<S32, U8> sDataSaverEnabledMap = null;
+ private static IBpfMap<IngressDiscardKey, IngressDiscardValue> sIngressDiscardMap = null;
private static final List<Pair<Integer, String>> PERMISSION_LIST = Arrays.asList(
Pair.create(PERMISSION_INTERNET, "PERMISSION_INTERNET"),
@@ -135,14 +135,6 @@
);
/**
- * Set sEnableJavaBpfMap for test.
- */
- @VisibleForTesting
- public static void setEnableJavaBpfMapForTest(boolean enable) {
- sEnableJavaBpfMap = enable;
- }
-
- /**
* Set configurationMap for test.
*/
@VisibleForTesting
@@ -175,6 +167,23 @@
sCookieTagMap = cookieTagMap;
}
+ /**
+ * Set dataSaverEnabledMap for test.
+ */
+ @VisibleForTesting
+ public static void setDataSaverEnabledMapForTest(IBpfMap<S32, U8> dataSaverEnabledMap) {
+ sDataSaverEnabledMap = dataSaverEnabledMap;
+ }
+
+ /**
+ * Set ingressDiscardMap for test.
+ */
+ @VisibleForTesting
+ public static void setIngressDiscardMapForTest(
+ IBpfMap<IngressDiscardKey, IngressDiscardValue> ingressDiscardMap) {
+ sIngressDiscardMap = ingressDiscardMap;
+ }
+
private static IBpfMap<S32, U32> getConfigurationMap() {
try {
return new BpfMap<>(
@@ -211,6 +220,24 @@
}
}
+ private static IBpfMap<S32, U8> getDataSaverEnabledMap() {
+ try {
+ return new BpfMap<>(
+ DATA_SAVER_ENABLED_MAP_PATH, BpfMap.BPF_F_RDWR, S32.class, U8.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open data saver enabled map", e);
+ }
+ }
+
+ private static IBpfMap<IngressDiscardKey, IngressDiscardValue> getIngressDiscardMap() {
+ try {
+ return new BpfMap<>(INGRESS_DISCARD_MAP_PATH, BpfMap.BPF_F_RDWR,
+ IngressDiscardKey.class, IngressDiscardValue.class);
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Cannot open ingress discard map", e);
+ }
+ }
+
private static void initBpfMaps() {
if (sConfigurationMap == null) {
sConfigurationMap = getConfigurationMap();
@@ -244,6 +271,24 @@
if (sCookieTagMap == null) {
sCookieTagMap = getCookieTagMap();
}
+
+ if (sDataSaverEnabledMap == null) {
+ sDataSaverEnabledMap = getDataSaverEnabledMap();
+ }
+ try {
+ sDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize data saver configuration", e);
+ }
+
+ if (sIngressDiscardMap == null) {
+ sIngressDiscardMap = getIngressDiscardMap();
+ }
+ try {
+ sIngressDiscardMap.clear();
+ } catch (ErrnoException e) {
+ throw new IllegalStateException("Failed to initialize ingress discard map", e);
+ }
}
/**
@@ -252,22 +297,10 @@
*/
private static synchronized void ensureInitialized(final Context context) {
if (sInitialized) return;
- if (sEnableJavaBpfMap == null) {
- sEnableJavaBpfMap = SdkLevel.isAtLeastU() ||
- DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context,
- BPF_NET_MAPS_FORCE_DISABLE_JAVA_BPF_MAP);
- }
- Log.d(TAG, "BpfNetMaps is initialized with sEnableJavaBpfMap=" + sEnableJavaBpfMap);
-
initBpfMaps();
- native_init(!sEnableJavaBpfMap /* startSkDestroyListener */);
sInitialized = true;
}
- public boolean isSkDestroyListenerRunning() {
- return !sEnableJavaBpfMap;
- }
-
/**
* Dependencies of BpfNetMaps, for injection in tests.
*/
@@ -281,10 +314,23 @@
}
/**
- * Call synchronize_rcu()
+ * Get interface name
*/
+ public String getIfName(final int ifIndex) {
+ return Os.if_indextoname(ifIndex);
+ }
+
+ /**
+ * Synchronously call in to kernel to synchronize_rcu()
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public int synchronizeKernelRCU() {
- return native_synchronizeKernelRCU();
+ try {
+ BpfMap.synchronizeKernelRCU();
+ } catch (ErrnoException e) {
+ return -e.errno;
+ }
+ return 0;
}
/**
@@ -298,6 +344,7 @@
}
/** Constructor used after T that doesn't need to use netd anymore. */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public BpfNetMaps(final Context context) {
this(context, null);
@@ -317,29 +364,6 @@
mDeps = deps;
}
- /**
- * Get if the chain is allow list or not.
- *
- * ALLOWLIST means the firewall denies all by default, uids must be explicitly allowed
- * DENYLIST means the firewall allows all by default, uids must be explicitly denyed
- */
- public boolean isFirewallAllowList(final int chain) {
- switch (chain) {
- case FIREWALL_CHAIN_DOZABLE:
- case FIREWALL_CHAIN_POWERSAVE:
- case FIREWALL_CHAIN_RESTRICTED:
- case FIREWALL_CHAIN_LOW_POWER_STANDBY:
- return true;
- case FIREWALL_CHAIN_STANDBY:
- case FIREWALL_CHAIN_OEM_DENY_1:
- case FIREWALL_CHAIN_OEM_DENY_2:
- case FIREWALL_CHAIN_OEM_DENY_3:
- return false;
- default:
- throw new ServiceSpecificException(EINVAL, "Invalid firewall chain: " + chain);
- }
- }
-
private void maybeThrow(final int err, final String msg) {
if (err != 0) {
throw new ServiceSpecificException(err, msg + ": " + Os.strerror(err));
@@ -420,15 +444,11 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void addNaughtyApp(final int uid) {
throwIfPreT("addNaughtyApp is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
- } else {
- final int err = native_addNaughtyApp(uid);
- maybeThrow(err, "Unable to add naughty app");
- }
+ addRule(uid, PENALTY_BOX_MATCH, "addNaughtyApp");
}
/**
@@ -438,15 +458,11 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void removeNaughtyApp(final int uid) {
throwIfPreT("removeNaughtyApp is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
- } else {
- final int err = native_removeNaughtyApp(uid);
- maybeThrow(err, "Unable to remove naughty app");
- }
+ removeRule(uid, PENALTY_BOX_MATCH, "removeNaughtyApp");
}
/**
@@ -456,15 +472,11 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void addNiceApp(final int uid) {
throwIfPreT("addNiceApp is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
- } else {
- final int err = native_addNiceApp(uid);
- maybeThrow(err, "Unable to add nice app");
- }
+ addRule(uid, HAPPY_BOX_MATCH, "addNiceApp");
}
/**
@@ -474,15 +486,11 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void removeNiceApp(final int uid) {
throwIfPreT("removeNiceApp is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
- } else {
- final int err = native_removeNiceApp(uid);
- maybeThrow(err, "Unable to remove nice app");
- }
+ removeRule(uid, HAPPY_BOX_MATCH, "removeNiceApp");
}
/**
@@ -494,24 +502,20 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setChildChain(final int childChain, final boolean enable) {
throwIfPreT("setChildChain is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- final long match = getMatchByFirewallChain(childChain);
- try {
- synchronized (sUidRulesConfigBpfMapLock) {
- final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
- final long newConfig = enable ? (config.val | match) : (config.val & ~match);
- sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
- }
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to set child chain: " + Os.strerror(e.errno));
+ final long match = getMatchByFirewallChain(childChain);
+ try {
+ synchronized (sUidRulesConfigBpfMapLock) {
+ final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
+ final long newConfig = enable ? (config.val | match) : (config.val & ~match);
+ sConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(newConfig));
}
- } else {
- final int err = native_setChildChain(childChain, enable);
- maybeThrow(err, "Unable to set child chain");
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno,
+ "Unable to set child chain: " + Os.strerror(e.errno));
}
}
@@ -523,23 +527,19 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ *
+ * @deprecated Use {@link BpfNetMapsReader#isChainEnabled} instead.
*/
+ // TODO: Migrate the callers to use {@link BpfNetMapsReader#isChainEnabled} instead.
+ @Deprecated
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public boolean isChainEnabled(final int childChain) {
- throwIfPreT("isChainEnabled is not available on pre-T devices");
-
- final long match = getMatchByFirewallChain(childChain);
- try {
- final U32 config = sConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY);
- return (config.val & match) != 0;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get firewall chain status: " + Os.strerror(e.errno));
- }
+ return BpfNetMapsReader.isChainEnabled(sConfigurationMap, childChain);
}
private Set<Integer> asSet(final int[] uids) {
final Set<Integer> uidSet = new ArraySet<>();
- for (final int uid: uids) {
+ for (final int uid : uids) {
uidSet.add(uid);
}
return uidSet;
@@ -554,78 +554,42 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws IllegalArgumentException if {@code chain} is not a valid chain.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void replaceUidChain(final int chain, final int[] uids) {
throwIfPreT("replaceUidChain is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- final long match;
- try {
- match = getMatchByFirewallChain(chain);
- } catch (ServiceSpecificException e) {
- // Throws IllegalArgumentException to keep the behavior of
- // ConnectivityManager#replaceFirewallChain API
- throw new IllegalArgumentException("Invalid firewall chain: " + chain);
- }
- final Set<Integer> uidSet = asSet(uids);
- final Set<Integer> uidSetToRemoveRule = new ArraySet<>();
- try {
- synchronized (sUidOwnerMap) {
- sUidOwnerMap.forEach((uid, config) -> {
- // config could be null if there is a concurrent entry deletion.
- // http://b/220084230. But sUidOwnerMap update must be done while holding a
- // lock, so this should not happen.
- if (config == null) {
- Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
- } else if (!uidSet.contains((int) uid.val) && (config.rule & match) != 0) {
- uidSetToRemoveRule.add((int) uid.val);
- }
- });
+ final long match;
+ try {
+ match = getMatchByFirewallChain(chain);
+ } catch (ServiceSpecificException e) {
+ // Throws IllegalArgumentException to keep the behavior of
+ // ConnectivityManager#replaceFirewallChain API
+ throw new IllegalArgumentException("Invalid firewall chain: " + chain);
+ }
+ final Set<Integer> uidSet = asSet(uids);
+ final Set<Integer> uidSetToRemoveRule = new ArraySet<>();
+ try {
+ synchronized (sUidOwnerMap) {
+ sUidOwnerMap.forEach((uid, config) -> {
+ // config could be null if there is a concurrent entry deletion.
+ // http://b/220084230. But sUidOwnerMap update must be done while holding a
+ // lock, so this should not happen.
+ if (config == null) {
+ Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
+ } else if (!uidSet.contains((int) uid.val) && (config.rule & match) != 0) {
+ uidSetToRemoveRule.add((int) uid.val);
+ }
+ });
- for (final int uid : uidSetToRemoveRule) {
- removeRule(uid, match, "replaceUidChain");
- }
- for (final int uid : uids) {
- addRule(uid, match, "replaceUidChain");
- }
+ for (final int uid : uidSetToRemoveRule) {
+ removeRule(uid, match, "replaceUidChain");
}
- } catch (ErrnoException | ServiceSpecificException e) {
- Log.e(TAG, "replaceUidChain failed: " + e);
+ for (final int uid : uids) {
+ addRule(uid, match, "replaceUidChain");
+ }
}
- } else {
- final int err;
- switch (chain) {
- case FIREWALL_CHAIN_DOZABLE:
- err = native_replaceUidChain("fw_dozable", true /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_STANDBY:
- err = native_replaceUidChain("fw_standby", false /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_POWERSAVE:
- err = native_replaceUidChain("fw_powersave", true /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_RESTRICTED:
- err = native_replaceUidChain("fw_restricted", true /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_LOW_POWER_STANDBY:
- err = native_replaceUidChain(
- "fw_low_power_standby", true /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_OEM_DENY_1:
- err = native_replaceUidChain("fw_oem_deny_1", false /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_OEM_DENY_2:
- err = native_replaceUidChain("fw_oem_deny_2", false /* isAllowList */, uids);
- break;
- case FIREWALL_CHAIN_OEM_DENY_3:
- err = native_replaceUidChain("fw_oem_deny_3", false /* isAllowList */, uids);
- break;
- default:
- throw new IllegalArgumentException("replaceFirewallChain with invalid chain: "
- + chain);
- }
- if (err != 0) {
- Log.e(TAG, "replaceUidChain failed: " + Os.strerror(-err));
- }
+ } catch (ErrnoException | ServiceSpecificException e) {
+ Log.e(TAG, "replaceUidChain failed: " + e);
}
}
@@ -638,23 +602,19 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setUidRule(final int childChain, final int uid, final int firewallRule) {
throwIfPreT("setUidRule is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- final long match = getMatchByFirewallChain(childChain);
- final boolean isAllowList = isFirewallAllowList(childChain);
- final boolean add = (firewallRule == FIREWALL_RULE_ALLOW && isAllowList)
- || (firewallRule == FIREWALL_RULE_DENY && !isAllowList);
+ final long match = getMatchByFirewallChain(childChain);
+ final boolean isAllowList = isFirewallAllowList(childChain);
+ final boolean add = (firewallRule == FIREWALL_RULE_ALLOW && isAllowList)
+ || (firewallRule == FIREWALL_RULE_DENY && !isAllowList);
- if (add) {
- addRule(uid, match, "setUidRule");
- } else {
- removeRule(uid, match, "setUidRule");
- }
+ if (add) {
+ addRule(uid, match, "setUidRule");
} else {
- final int err = native_setUidRule(childChain, uid, firewallRule);
- maybeThrow(err, "Unable to set uid rule");
+ removeRule(uid, match, "setUidRule");
}
}
@@ -667,20 +627,12 @@
* @throws UnsupportedOperationException if called on pre-T devices.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ *
+ * @deprecated use {@link BpfNetMapsReader#getUidRule} instead.
*/
+ // TODO: Migrate the callers to use {@link BpfNetMapsReader#getUidRule} instead.
public int getUidRule(final int childChain, final int uid) {
- throwIfPreT("isUidChainEnabled is not available on pre-T devices");
-
- final long match = getMatchByFirewallChain(childChain);
- final boolean isAllowList = isFirewallAllowList(childChain);
- try {
- final UidOwnerValue uidMatch = sUidOwnerMap.getValue(new S32(uid));
- final boolean isMatchEnabled = uidMatch != null && (uidMatch.rule & match) != 0;
- return isMatchEnabled == isAllowList ? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno,
- "Unable to get uid rule status: " + Os.strerror(e.errno));
- }
+ return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);
}
private Set<Integer> getUidsMatchEnabled(final int childChain) throws ErrnoException {
@@ -765,29 +717,24 @@
return;
}
- if (sEnableJavaBpfMap) {
- // Null ifName is a wildcard to allow apps to receive packets on all interfaces and
- // ifIndex is set to 0.
- final int ifIndex;
- if (ifName == null) {
- ifIndex = 0;
- } else {
- ifIndex = mDeps.getIfIndex(ifName);
- if (ifIndex == 0) {
- throw new ServiceSpecificException(ENODEV,
- "Failed to get index of interface " + ifName);
- }
- }
- for (final int uid : uids) {
- try {
- addRule(uid, IIF_MATCH, ifIndex, "addUidInterfaceRules");
- } catch (ServiceSpecificException e) {
- Log.e(TAG, "addRule failed uid=" + uid + " ifName=" + ifName + ", " + e);
- }
- }
+ // Null ifName is a wildcard to allow apps to receive packets on all interfaces and
+ // ifIndex is set to 0.
+ final int ifIndex;
+ if (ifName == null) {
+ ifIndex = 0;
} else {
- final int err = native_addUidInterfaceRules(ifName, uids);
- maybeThrow(err, "Unable to add uid interface rules");
+ ifIndex = mDeps.getIfIndex(ifName);
+ if (ifIndex == 0) {
+ throw new ServiceSpecificException(ENODEV,
+ "Failed to get index of interface " + ifName);
+ }
+ }
+ for (final int uid : uids) {
+ try {
+ addRule(uid, IIF_MATCH, ifIndex, "addUidInterfaceRules");
+ } catch (ServiceSpecificException e) {
+ Log.e(TAG, "addRule failed uid=" + uid + " ifName=" + ifName + ", " + e);
+ }
}
}
@@ -808,17 +755,12 @@
return;
}
- if (sEnableJavaBpfMap) {
- for (final int uid : uids) {
- try {
- removeRule(uid, IIF_MATCH, "removeUidInterfaceRules");
- } catch (ServiceSpecificException e) {
- Log.e(TAG, "removeRule failed uid=" + uid + ", " + e);
- }
+ for (final int uid : uids) {
+ try {
+ removeRule(uid, IIF_MATCH, "removeUidInterfaceRules");
+ } catch (ServiceSpecificException e) {
+ Log.e(TAG, "removeRule failed uid=" + uid + ", " + e);
}
- } else {
- final int err = native_removeUidInterfaceRules(uids);
- maybeThrow(err, "Unable to remove uid interface rules");
}
}
@@ -830,18 +772,14 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void updateUidLockdownRule(final int uid, final boolean add) {
throwIfPreT("updateUidLockdownRule is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- if (add) {
- addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
- } else {
- removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
- }
+ if (add) {
+ addRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
} else {
- final int err = native_updateUidLockdownRule(uid, add);
- maybeThrow(err, "Unable to update lockdown rule");
+ removeRule(uid, LOCKDOWN_VPN_MATCH, "updateUidLockdownRule");
}
}
@@ -852,36 +790,32 @@
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void swapActiveStatsMap() {
throwIfPreT("swapActiveStatsMap is not available on pre-T devices");
- if (sEnableJavaBpfMap) {
- try {
- synchronized (sCurrentStatsMapConfigLock) {
- final long config = sConfigurationMap.getValue(
- CURRENT_STATS_MAP_CONFIGURATION_KEY).val;
- final long newConfig = (config == STATS_SELECT_MAP_A)
- ? STATS_SELECT_MAP_B : STATS_SELECT_MAP_A;
- sConfigurationMap.updateEntry(CURRENT_STATS_MAP_CONFIGURATION_KEY,
- new U32(newConfig));
- }
- } catch (ErrnoException e) {
- throw new ServiceSpecificException(e.errno, "Failed to swap active stats map");
+ try {
+ synchronized (sCurrentStatsMapConfigLock) {
+ final long config = sConfigurationMap.getValue(
+ CURRENT_STATS_MAP_CONFIGURATION_KEY).val;
+ final long newConfig = (config == STATS_SELECT_MAP_A)
+ ? STATS_SELECT_MAP_B : STATS_SELECT_MAP_A;
+ sConfigurationMap.updateEntry(CURRENT_STATS_MAP_CONFIGURATION_KEY,
+ new U32(newConfig));
}
-
- // After changing the config, it's needed to make sure all the current running eBPF
- // programs are finished and all the CPUs are aware of this config change before the old
- // map is modified. So special hack is needed here to wait for the kernel to do a
- // synchronize_rcu(). Once the kernel called synchronize_rcu(), the updated config will
- // be available to all cores and the next eBPF programs triggered inside the kernel will
- // use the new map configuration. So once this function returns it is safe to modify the
- // old stats map without concerning about race between the kernel and userspace.
- final int err = mDeps.synchronizeKernelRCU();
- maybeThrow(err, "synchronizeKernelRCU failed");
- } else {
- final int err = native_swapActiveStatsMap();
- maybeThrow(err, "Unable to swap active stats map");
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, "Failed to swap active stats map");
}
+
+ // After changing the config, it's needed to make sure all the current running eBPF
+ // programs are finished and all the CPUs are aware of this config change before the old
+ // map is modified. So special hack is needed here to wait for the kernel to do a
+ // synchronize_rcu(). Once the kernel called synchronize_rcu(), the updated config will
+ // be available to all cores and the next eBPF programs triggered inside the kernel will
+ // use the new map configuration. So once this function returns it is safe to modify the
+ // old stats map without concerning about race between the kernel and userspace.
+ final int err = mDeps.synchronizeKernelRCU();
+ maybeThrow(err, "synchronizeKernelRCU failed");
}
/**
@@ -900,33 +834,90 @@
return;
}
- if (sEnableJavaBpfMap) {
- // Remove the entry if package is uninstalled or uid has only INTERNET permission.
- if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
- for (final int uid : uids) {
- try {
- sUidPermissionMap.deleteEntry(new S32(uid));
- } catch (ErrnoException e) {
- Log.e(TAG, "Failed to remove uid " + uid + " from permission map: " + e);
- }
- }
- return;
- }
-
+ // Remove the entry if package is uninstalled or uid has only INTERNET permission.
+ if (permissions == PERMISSION_UNINSTALLED || permissions == PERMISSION_INTERNET) {
for (final int uid : uids) {
try {
- sUidPermissionMap.updateEntry(new S32(uid), new U8((short) permissions));
+ sUidPermissionMap.deleteEntry(new S32(uid));
} catch (ErrnoException e) {
- Log.e(TAG, "Failed to set permission "
- + permissions + " to uid " + uid + ": " + e);
+ Log.e(TAG, "Failed to remove uid " + uid + " from permission map: " + e);
}
}
- } else {
- native_setPermissionForUids(permissions, uids);
+ return;
+ }
+
+ for (final int uid : uids) {
+ try {
+ sUidPermissionMap.updateEntry(new S32(uid), new U8((short) permissions));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set permission "
+ + permissions + " to uid " + uid + ": " + e);
+ }
+ }
+ }
+
+ /**
+ * Set Data Saver enabled or disabled
+ *
+ * @param enable whether Data Saver is enabled or disabled.
+ * @throws UnsupportedOperationException if called on pre-T devices.
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public void setDataSaverEnabled(boolean enable) {
+ throwIfPreT("setDataSaverEnabled is not available on pre-T devices");
+
+ try {
+ final short config = enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED;
+ sDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(config));
+ } catch (ErrnoException e) {
+ throw new ServiceSpecificException(e.errno, "Unable to set data saver: "
+ + Os.strerror(e.errno));
+ }
+ }
+
+ /**
+ * Set ingress discard rule
+ *
+ * @param address target address to set the ingress discard rule
+ * @param iface allowed interface
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public void setIngressDiscardRule(final InetAddress address, final String iface) {
+ throwIfPreT("setIngressDiscardRule is not available on pre-T devices");
+ final int ifIndex = mDeps.getIfIndex(iface);
+ if (ifIndex == 0) {
+ Log.e(TAG, "Failed to get if index, skip setting ingress discard rule for " + address
+ + "(" + iface + ")");
+ return;
+ }
+ try {
+ sIngressDiscardMap.updateEntry(new IngressDiscardKey(address),
+ new IngressDiscardValue(ifIndex, ifIndex));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to set ingress discard rule for " + address + "("
+ + iface + "), " + e);
+ }
+ }
+
+ /**
+ * Remove ingress discard rule
+ *
+ * @param address target address to remove the ingress discard rule
+ */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ public void removeIngressDiscardRule(final InetAddress address) {
+ throwIfPreT("removeIngressDiscardRule is not available on pre-T devices");
+ try {
+ sIngressDiscardMap.deleteEntry(new IngressDiscardKey(address));
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Failed to remove ingress discard rule for " + address + ", " + e);
}
}
/** Register callback for statsd to pull atom. */
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void setPullAtomCallback(final Context context) {
throwIfPreT("setPullAtomCallback is not available on pre-T devices");
@@ -1007,6 +998,15 @@
}
}
+ private void dumpDataSaverConfig(final IndentingPrintWriter pw) {
+ try {
+ final short config = sDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val;
+ // Any non-zero value converted from short to boolean is true by convention.
+ pw.println("sDataSaverEnabledMap: " + (config != DATA_SAVER_DISABLED));
+ } catch (ErrnoException e) {
+ pw.println("Failed to read data saver configuration: " + e);
+ }
+ }
/**
* Dump BPF maps
*
@@ -1016,6 +1016,7 @@
* @throws IOException when file descriptor is invalid.
* @throws ServiceSpecificException when the method is called on an unsupported device.
*/
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public void dump(final IndentingPrintWriter pw, final FileDescriptor fd, boolean verbose)
throws IOException, ServiceSpecificException {
if (PRE_T) {
@@ -1027,7 +1028,6 @@
pw.println("TrafficController"); // required by CTS testDumpBpfNetMaps
pw.println();
- pw.println("sEnableJavaBpfMap: " + sEnableJavaBpfMap);
if (verbose) {
pw.println();
pw.println("BPF map content:");
@@ -1056,49 +1056,12 @@
});
BpfDump.dumpMap(sUidPermissionMap, pw, "sUidPermissionMap",
(uid, permission) -> uid.val + " " + permissionToString(permission.val));
+ BpfDump.dumpMap(sIngressDiscardMap, pw, "sIngressDiscardMap",
+ (key, value) -> "[" + key.dstAddr + "]: "
+ + value.iif1 + "(" + mDeps.getIfName(value.iif1) + "), "
+ + value.iif2 + "(" + mDeps.getIfName(value.iif2) + ")");
+ dumpDataSaverConfig(pw);
pw.decreaseIndent();
}
}
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private static native void native_init(boolean startSkDestroyListener);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_addNaughtyApp(int uid);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_removeNaughtyApp(int uid);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_addNiceApp(int uid);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_removeNiceApp(int uid);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_setChildChain(int childChain, boolean enable);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_replaceUidChain(String name, boolean isAllowlist, int[] uids);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_setUidRule(int childChain, int uid, int firewallRule);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_addUidInterfaceRules(String ifName, int[] uids);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_removeUidInterfaceRules(int[] uids);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_updateUidLockdownRule(int uid, boolean add);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native int native_swapActiveStatsMap();
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private native void native_setPermissionForUids(int permissions, int[] uids);
-
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
- private static native int native_synchronizeKernelRCU();
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b5dbf96..8f29078 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
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;
@@ -67,6 +68,7 @@
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_LOCAL_NETWORK;
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;
@@ -97,17 +99,25 @@
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_EGRESS;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_INGRESS;
+import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET_SOCK_CREATE;
import static com.android.net.module.util.NetworkMonitorUtils.isPrivateDnsValidationRequired;
import static com.android.net.module.util.PermissionUtils.checkAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
+
import static java.util.Map.Entry;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.ActivityManager.UidFrozenStateChangedCallback;
@@ -126,6 +136,7 @@
import android.content.pm.PackageManager;
import android.content.res.XmlResourceParser;
import android.database.ContentObserver;
+import android.net.BpfNetMapsUtils;
import android.net.CaptivePortal;
import android.net.CaptivePortalData;
import android.net.ConnectionInfo;
@@ -156,6 +167,8 @@
import android.net.IpMemoryStore;
import android.net.IpPrefix;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
+import android.net.LocalNetworkInfo;
import android.net.MatchAllNetworkSpecifier;
import android.net.NativeNetworkConfig;
import android.net.NativeNetworkType;
@@ -188,7 +201,6 @@
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;
@@ -274,6 +286,7 @@
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.BinderUtils;
import com.android.net.module.util.BitUtils;
+import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.InterfaceParams;
@@ -300,6 +313,7 @@
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.HandlerUtils;
import com.android.server.connectivity.InvalidTagException;
import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
@@ -317,6 +331,7 @@
import com.android.server.connectivity.ProfileNetworkPreferenceInfo;
import com.android.server.connectivity.ProxyTracker;
import com.android.server.connectivity.QosCallbackTracker;
+import com.android.server.connectivity.RoutingCoordinatorService;
import com.android.server.connectivity.UidRangeUtils;
import com.android.server.connectivity.VpnNetworkPreferenceInfo;
import com.android.server.connectivity.wear.CompanionDeviceManagerProxyService;
@@ -480,6 +495,7 @@
@GuardedBy("mTNSLock")
private TestNetworkService mTNS;
private final CompanionDeviceManagerProxyService mCdmps;
+ private final RoutingCoordinatorService mRoutingCoordinatorService;
private final Object mTNSLock = new Object();
@@ -953,6 +969,9 @@
// Flag to optimize closing frozen app sockets by waiting for the cellular modem to wake up.
private final boolean mDelayDestroyFrozenSockets;
+ // Flag to allow SysUI to receive connectivity reports for wifi picker UI.
+ private final boolean mAllowSysUiConnectivityReports;
+
// Uids that ConnectivityService is pending to close sockets of.
private final Set<Integer> mPendingFrozenUids = new ArraySet<>();
@@ -1252,16 +1271,24 @@
private static final String PRIORITY_ARG = "--dump-priority";
private static final String PRIORITY_ARG_HIGH = "HIGH";
private static final String PRIORITY_ARG_NORMAL = "NORMAL";
+ private static final int DUMPSYS_DEFAULT_TIMEOUT_MS = 10_000;
LocalPriorityDump() {}
private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
- doDump(fd, pw, new String[] {DIAG_ARG});
- doDump(fd, pw, new String[] {SHORT_ARG});
+ if (!HandlerUtils.runWithScissors(mHandler, () -> {
+ doDump(fd, pw, new String[]{DIAG_ARG});
+ doDump(fd, pw, new String[]{SHORT_ARG});
+ }, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
+ pw.println("dumpHigh timeout");
+ }
}
private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
- doDump(fd, pw, args);
+ if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args),
+ DUMPSYS_DEFAULT_TIMEOUT_MS)) {
+ pw.println("dumpNormal timeout");
+ }
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
@@ -1314,6 +1341,10 @@
return SdkLevel.isAtLeastU();
}
+ public boolean isAtLeastV() {
+ return SdkLevel.isAtLeastV();
+ }
+
/**
* Get system properties to use in ConnectivityService.
*/
@@ -1331,8 +1362,8 @@
/**
* Create a HandlerThread to use in ConnectivityService.
*/
- public HandlerThread makeHandlerThread() {
- return new HandlerThread("ConnectivityServiceThread");
+ public HandlerThread makeHandlerThread(@NonNull final String tag) {
+ return new HandlerThread(tag);
}
/**
@@ -1427,7 +1458,7 @@
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
@NonNull final Context context, @NonNull final TelephonyManager tm) {
if (isAtLeastT()) {
- return new CarrierPrivilegeAuthenticator(context, this, tm);
+ return new CarrierPrivilegeAuthenticator(context, tm);
} else {
return null;
}
@@ -1441,6 +1472,13 @@
}
/**
+ * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut
+ */
+ public boolean isFeatureNotChickenedOut(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name);
+ }
+
+ /**
* Get the BpfNetMaps implementation to use in ConnectivityService.
* @param netd a netd binder
* @return BpfNetMaps implementation.
@@ -1510,6 +1548,14 @@
}
/**
+ * Get BPF program Id from CGROUP. See {@link BpfUtils#getProgramId}.
+ */
+ public int getBpfProgramId(final int attachType)
+ throws IOException {
+ return BpfUtils.getProgramId(attachType);
+ }
+
+ /**
* Wraps {@link BroadcastOptionsShimImpl#newInstance(BroadcastOptions)}
*/
// TODO: when available in all active branches:
@@ -1660,7 +1706,7 @@
mNetd = netd;
mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
- mHandlerThread = mDeps.makeHandlerThread();
+ mHandlerThread = mDeps.makeHandlerThread("ConnectivityServiceThread");
mPermissionMonitor =
new PermissionMonitor(mContext, mNetd, mBpfNetMaps, mHandlerThread);
mHandlerThread.start();
@@ -1724,7 +1770,12 @@
mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
null /* broadcastPermission */, mHandler);
- mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler);
+ // TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
+ // But reading the trunk stable flags from mainline modules is not supported yet.
+ // So enabling this feature on V+ release.
+ mTrackMultiNetworkActivities = mDeps.isAtLeastV();
+ mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mNetd, mHandler,
+ mTrackMultiNetworkActivities);
final NetdCallback netdCallback = new NetdCallback();
try {
@@ -1766,7 +1817,7 @@
mNoServiceNetwork = new NetworkAgentInfo(null,
new Network(INetd.UNREACHABLE_NET_ID),
new NetworkInfo(TYPE_NONE, 0, "", ""),
- new LinkProperties(), new NetworkCapabilities(),
+ new LinkProperties(), new NetworkCapabilities(), null /* localNetworkConfig */,
new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
new NetworkAgentConfig(), this, null, null, 0, INVALID_UID,
mLingerDelayMs, mQosCallbackTracker, mDeps);
@@ -1793,10 +1844,14 @@
mCdmps = null;
}
+ mRoutingCoordinatorService = new RoutingCoordinatorService(netd);
+
mDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
&& mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
+ mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
+ mContext, ALLOW_SYSUI_CONNECTIVITY_REPORTS);
if (mDestroyFrozenSockets) {
final UidFrozenStateChangedCallback frozenStateChangedCallback =
new UidFrozenStateChangedCallback() {
@@ -2594,6 +2649,13 @@
}
}
+ private boolean canSeeAllowedUids(final int pid, final int uid, final int netOwnerUid) {
+ return Process.SYSTEM_UID == uid
+ || netOwnerUid == uid
+ || checkAnyPermissionOf(mContext, pid, uid,
+ android.Manifest.permission.NETWORK_FACTORY);
+ }
+
@VisibleForTesting
NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
NetworkCapabilities nc, int callerPid, int callerUid) {
@@ -2615,8 +2677,7 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) {
newNc.setAdministratorUids(new int[0]);
}
- if (!checkAnyPermissionOf(mContext,
- callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) {
+ if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) {
newNc.setAllowedUids(new ArraySet<>());
newNc.setSubscriptionIds(Collections.emptySet());
}
@@ -3196,9 +3257,20 @@
private void handleReportNetworkActivity(final NetworkActivityParams params) {
mNetworkActivityTracker.handleReportNetworkActivity(params);
+ final boolean isCellNetworkActivity;
+ if (mTrackMultiNetworkActivities) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(params.label);
+ // nai could be null if netd receives a netlink message and calls the network
+ // activity change callback after the network is unregistered from ConnectivityService.
+ isCellNetworkActivity = nai != null
+ && nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
+ } else {
+ isCellNetworkActivity = params.label == TRANSPORT_CELLULAR;
+ }
+
if (mDelayDestroyFrozenSockets
&& params.isActive
- && params.label == TRANSPORT_CELLULAR
+ && isCellNetworkActivity
&& !mPendingFrozenUids.isEmpty()) {
closePendingFrozenSockets();
}
@@ -3233,12 +3305,36 @@
pw.decreaseIndent();
}
+ private void dumpBpfProgramStatus(IndentingPrintWriter pw) {
+ pw.println("Bpf Program Status:");
+ pw.increaseIndent();
+ try {
+ pw.print("CGROUP_INET_INGRESS: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_INGRESS));
+ pw.print("CGROUP_INET_EGRESS: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_EGRESS));
+ pw.print("CGROUP_INET_SOCK_CREATE: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET_SOCK_CREATE));
+ pw.print("CGROUP_INET4_BIND: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET4_BIND));
+ pw.print("CGROUP_INET6_BIND: ");
+ pw.println(mDeps.getBpfProgramId(BPF_CGROUP_INET6_BIND));
+ } catch (IOException e) {
+ pw.println(" IOException");
+ }
+ pw.decreaseIndent();
+ }
+
@VisibleForTesting
static final String KEY_DESTROY_FROZEN_SOCKETS_VERSION = "destroy_frozen_sockets_version";
@VisibleForTesting
static final String DELAY_DESTROY_FROZEN_SOCKETS_VERSION =
"delay_destroy_frozen_sockets_version";
+ @VisibleForTesting
+ public static final String ALLOW_SYSUI_CONNECTIVITY_REPORTS =
+ "allow_sysui_connectivity_reports";
+
private void enforceInternetPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERNET,
@@ -3402,6 +3498,11 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
+ private boolean checkSystemBarServicePermission(int pid, int uid) {
+ return checkAnyPermissionOf(mContext, pid, uid,
+ android.Manifest.permission.STATUS_BAR_SERVICE);
+ }
+
private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
return checkAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
@@ -3480,6 +3581,8 @@
sendStickyBroadcast(makeGeneralIntent(info, bcastType));
}
+ // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
+ @SuppressLint("NewApi")
// TODO: Set the mini sdk to 31 and remove @TargetApi annotation when b/205923322 is addressed.
@TargetApi(Build.VERSION_CODES.S)
private void sendStickyBroadcast(Intent intent) {
@@ -3845,6 +3948,14 @@
dumpCloseFrozenAppSockets(pw);
pw.println();
+ dumpBpfProgramStatus(pw);
+
+ if (null != mCarrierPrivilegeAuthenticator) {
+ pw.println();
+ mCarrierPrivilegeAuthenticator.dump(pw);
+ }
+
+ pw.println();
if (!CollectionUtils.contains(args, SHORT_ARG)) {
pw.println();
@@ -4094,7 +4205,14 @@
switch (msg.what) {
case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
- nai.setDeclaredCapabilities((NetworkCapabilities) arg.second);
+ final NetworkCapabilities proposed = (NetworkCapabilities) arg.second;
+ if (!nai.respectsNcStructuralConstraints(proposed)) {
+ Log.wtf(TAG, "Agent " + nai + " violates nc structural constraints : "
+ + nai.networkCapabilities + " -> " + proposed);
+ disconnectAndDestroyNetwork(nai);
+ return;
+ }
+ nai.setDeclaredCapabilities(proposed);
final NetworkCapabilities sanitized =
nai.getDeclaredCapabilitiesSanitized(mCarrierPrivilegeAuthenticator);
maybeUpdateWifiRoamTimestamp(nai, sanitized);
@@ -4112,6 +4230,11 @@
updateNetworkInfo(nai, info);
break;
}
+ case NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED: {
+ final LocalNetworkConfig config = (LocalNetworkConfig) arg.second;
+ handleUpdateLocalNetworkConfig(nai, nai.localNetworkConfig, config);
+ break;
+ }
case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
updateNetworkScore(nai, (NetworkScore) arg.second);
break;
@@ -4869,7 +4992,23 @@
if (wasDefault) {
mDefaultInetConditionPublished = 0;
}
+ if (mTrackMultiNetworkActivities) {
+ // If trackMultiNetworkActivities is disabled, ActivityTracker removes idleTimer when
+ // the network becomes no longer the default network.
+ mNetworkActivityTracker.removeDataActivityTracking(nai);
+ }
notifyIfacesChangedForNetworkStats();
+ // If this was a local network forwarded to some upstream, or if some local network was
+ // forwarded to this nai, then disable forwarding rules now.
+ maybeDisableForwardRulesForDisconnectingNai(nai, true /* sendCallbacks */);
+ // If this is a local network with an upstream selector, remove the associated network
+ // request.
+ if (nai.isLocalNetwork()) {
+ final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+ if (null != selector) {
+ handleRemoveNetworkRequest(mNetworkRequests.get(selector));
+ }
+ }
// 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
@@ -4911,12 +5050,7 @@
}
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);
+ mNetworkActivityTracker.updateDefaultNetwork(null /* newNetwork */, nai);
maybeClosePendingFrozenSockets(null /* newNetwork */, nai);
ensureNetworkTransitionWakelock(nai.toShortString());
}
@@ -4983,6 +5117,55 @@
mNetIdManager.releaseNetId(nai.network.getNetId());
}
+ private void maybeDisableForwardRulesForDisconnectingNai(
+ @NonNull final NetworkAgentInfo disconnecting, final boolean sendCallbacks) {
+ // Step 1 : maybe this network was the upstream for one or more local networks.
+ for (final NetworkAgentInfo local : mNetworkAgentInfos) {
+ if (!local.isLocalNetwork()) continue;
+ final NetworkRequest selector = local.localNetworkConfig.getUpstreamSelector();
+ if (null == selector) continue;
+ final NetworkRequestInfo nri = mNetworkRequests.get(selector);
+ // null == nri can happen while disconnecting a network, because destroyNetwork() is
+ // called after removing all associated NRIs from mNetworkRequests.
+ if (null == nri) continue;
+ final NetworkAgentInfo satisfier = nri.getSatisfier();
+ if (disconnecting != satisfier) continue;
+ removeLocalNetworkUpstream(local, disconnecting);
+ // Set the satisfier to null immediately so that the LOCAL_NETWORK_CHANGED callback
+ // correctly contains null as an upstream.
+ if (sendCallbacks) {
+ nri.setSatisfier(null, null);
+ notifyNetworkCallbacks(local,
+ ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ }
+ }
+
+ // Step 2 : maybe this is a local network that had an upstream.
+ if (!disconnecting.isLocalNetwork()) return;
+ final NetworkRequest selector = disconnecting.localNetworkConfig.getUpstreamSelector();
+ if (null == selector) return;
+ final NetworkRequestInfo nri = mNetworkRequests.get(selector);
+ // As above null == nri can happen while disconnecting a network, because destroyNetwork()
+ // is called after removing all associated NRIs from mNetworkRequests.
+ if (null == nri) return;
+ final NetworkAgentInfo satisfier = nri.getSatisfier();
+ if (null == satisfier) return;
+ removeLocalNetworkUpstream(disconnecting, satisfier);
+ }
+
+ private void removeLocalNetworkUpstream(@NonNull final NetworkAgentInfo localAgent,
+ @NonNull final NetworkAgentInfo upstream) {
+ try {
+ mRoutingCoordinatorService.removeInterfaceForward(
+ localAgent.linkProperties.getInterfaceName(),
+ upstream.linkProperties.getInterfaceName());
+ } catch (RemoteException e) {
+ loge("Couldn't remove interface forward for "
+ + localAgent.linkProperties.getInterfaceName() + " to "
+ + upstream.linkProperties.getInterfaceName() + " while disconnecting");
+ }
+ }
+
private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
try {
// This should never fail. Specifying an already in use NetID will cause failure.
@@ -4997,7 +5180,9 @@
!nai.networkAgentConfig.allowBypass /* secure */,
getVpnType(nai), nai.networkAgentConfig.excludeLocalRouteVpn);
} else {
- config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
+ config = new NativeNetworkConfig(nai.network.getNetId(),
+ nai.isLocalNetwork() ? NativeNetworkType.PHYSICAL_LOCAL
+ : NativeNetworkType.PHYSICAL,
getNetworkPermission(nai.networkCapabilities),
false /* secure */,
VpnManager.TYPE_VPN_NONE,
@@ -5005,8 +5190,8 @@
}
mNetd.networkCreate(config);
mDnsResolver.createNetworkCache(nai.network.getNetId());
- mDnsManager.updateTransportsForNetwork(nai.network.getNetId(),
- nai.networkCapabilities.getTransportTypes());
+ mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(),
+ nai.networkCapabilities);
return true;
} catch (RemoteException | ServiceSpecificException e) {
loge("Error creating network " + nai.toShortString() + ": " + e.getMessage());
@@ -5018,6 +5203,11 @@
if (mDscpPolicyTracker != null) {
mDscpPolicyTracker.removeAllDscpPolicies(nai, false);
}
+ // Remove any forwarding rules to and from the interface for this network, since
+ // the interface is going to go away. Don't send the callbacks however ; if the network
+ // was is being disconnected the callbacks have already been sent, and if it is being
+ // destroyed pending replacement they will be sent when it is disconnected.
+ maybeDisableForwardRulesForDisconnectingNai(nai, false /* sendCallbacks */);
try {
mNetd.networkDestroy(nai.network.getNetId());
} catch (RemoteException | ServiceSpecificException e) {
@@ -5073,7 +5263,7 @@
private boolean hasCarrierPrivilegeForNetworkCaps(final int callingUid,
@NonNull final NetworkCapabilities caps) {
if (mCarrierPrivilegeAuthenticator != null) {
- return mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ return mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
callingUid, caps);
}
return false;
@@ -5204,7 +5394,14 @@
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
+ // While destroyed network sometimes satisfy requests (including occasionally newly
+ // satisfying requests), *potential* satisfiers are networks that might beat a current
+ // champion if they validate. As such, a destroyed network is never a potential satisfier,
+ // because it's never a good idea to keep a destroyed network in case it validates.
+ // For example, declaring it a potential satisfier would keep an unvalidated destroyed
+ // candidate after it's been replaced by another unvalidated network.
+ if (candidate.isDestroyed()) return false;
+ // 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()
@@ -5222,8 +5419,12 @@
if (req.isListen() || req.isListenForBest()) {
continue;
}
- // If this Network is already the best Network for a request, or if
- // there is hope for it to become one if it validated, then it is needed.
+ // If there is hope for this network might validate and subsequently become the best
+ // network for that request, then it is needed. Note that this network can't already
+ // be the best for this request, or it would be the current satisfier, and therefore
+ // there would be no need to call this method to find out if it is a *potential*
+ // satisfier ("unneeded", the only caller, only calls this if this network currently
+ // satisfies no request).
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
@@ -6072,8 +6273,10 @@
if (!networkFound) return;
if (underpinnedNetworkFound) {
+ final NetworkCapabilities underpinnedNc =
+ getNetworkCapabilitiesInternal(underpinnedNetwork);
mKeepaliveTracker.handleMonitorAutomaticKeepalive(ki,
- underpinnedNetwork.netId);
+ underpinnedNetwork.netId, underpinnedNc.getUids());
} else {
// If no underpinned network, then make sure the keepalive is running.
mKeepaliveTracker.handleMaybeResumeKeepalive(ki);
@@ -8025,6 +8228,7 @@
}
}
}
+ if (!highestPriorityNri.isBeingSatisfied()) return null;
return highestPriorityNri.getSatisfier();
}
@@ -8047,6 +8251,18 @@
}
/**
+ * Returns whether local agents are supported on this device.
+ *
+ * Local agents are supported from U on TVs, and from V on all devices.
+ */
+ @VisibleForTesting
+ public boolean areLocalAgentsSupported() {
+ final PackageManager pm = mContext.getPackageManager();
+ // Local agents are supported starting on U on TVs and on V on everything else.
+ return mDeps.isAtLeastV() || (mDeps.isAtLeastU() && pm.hasSystemFeature(FEATURE_LEANBACK));
+ }
+
+ /**
* Register a new agent with ConnectivityService to handle a network.
*
* @param na a reference for ConnectivityService to contact the agent asynchronously.
@@ -8057,13 +8273,18 @@
* @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#getScore}.
+ * @param localNetworkConfig config about this local network, or null if not a local network
* @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,
+ public Network registerNetworkAgent(INetworkAgent na,
+ NetworkInfo networkInfo,
+ LinkProperties linkProperties,
+ NetworkCapabilities networkCapabilities,
+ @NonNull NetworkScore initialScore,
+ @Nullable LocalNetworkConfig localNetworkConfig,
+ NetworkAgentConfig networkAgentConfig,
int providerId) {
Objects.requireNonNull(networkInfo, "networkInfo must not be null");
Objects.requireNonNull(linkProperties, "linkProperties must not be null");
@@ -8075,12 +8296,26 @@
} else {
enforceNetworkFactoryPermission();
}
+ final boolean hasLocalCap =
+ networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ if (hasLocalCap && !areLocalAgentsSupported()) {
+ // Before U, netd doesn't support PHYSICAL_LOCAL networks so this can't work.
+ throw new IllegalArgumentException("Local agents are not supported in this version");
+ }
+ final boolean hasLocalNetworkConfig = null != localNetworkConfig;
+ if (hasLocalCap != hasLocalNetworkConfig) {
+ throw new IllegalArgumentException(null != localNetworkConfig
+ ? "Only local network agents can have a LocalNetworkConfig"
+ : "Local network agents must have a LocalNetworkConfig"
+ );
+ }
final int uid = mDeps.getCallingUid();
final long token = Binder.clearCallingIdentity();
try {
return registerNetworkAgentInternal(na, networkInfo, linkProperties,
- networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
+ networkCapabilities, initialScore, networkAgentConfig, localNetworkConfig,
+ providerId, uid);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -8088,7 +8323,8 @@
private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
- NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
+ NetworkScore currentScore, NetworkAgentConfig networkAgentConfig,
+ @Nullable LocalNetworkConfig localNetworkConfig, int providerId,
int uid) {
// Make a copy of the passed NI, LP, NC as the caller may hold a reference to them
@@ -8096,6 +8332,7 @@
final NetworkInfo niCopy = new NetworkInfo(networkInfo);
final NetworkCapabilities ncCopy = new NetworkCapabilities(networkCapabilities);
final LinkProperties lpCopy = new LinkProperties(linkProperties);
+ // No need to copy |localNetworkConfiguration| as it is immutable.
// At this point the capabilities/properties are untrusted and unverified, e.g. checks that
// the capabilities' access UIDs comply with security limitations. They will be sanitized
@@ -8103,9 +8340,9 @@
// because some of the checks must happen on the handler thread.
final NetworkAgentInfo nai = new NetworkAgentInfo(na,
new Network(mNetIdManager.reserveNetId()), niCopy, lpCopy, ncCopy,
- currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
- this, mNetd, mDnsResolver, providerId, uid, mLingerDelayMs,
- mQosCallbackTracker, mDeps);
+ localNetworkConfig, currentScore, mContext, mTrackerHandler,
+ new NetworkAgentConfig(networkAgentConfig), this, mNetd, mDnsResolver, providerId,
+ uid, mLingerDelayMs, mQosCallbackTracker, mDeps);
final String extraInfo = niCopy.getExtraInfo();
final String name = TextUtils.isEmpty(extraInfo)
@@ -8142,6 +8379,9 @@
e.rethrowAsRuntimeException();
}
+ if (nai.isLocalNetwork()) {
+ handleUpdateLocalNetworkConfig(nai, null /* oldConfig */, nai.localNetworkConfig);
+ }
nai.notifyRegistered();
NetworkInfo networkInfo = nai.networkInfo;
updateNetworkInfo(nai, networkInfo);
@@ -8415,7 +8655,7 @@
for (final String iface : interfaceDiff.added) {
try {
if (DBG) log("Adding iface " + iface + " to network " + netId);
- mNetd.networkAddInterface(netId, iface);
+ mRoutingCoordinatorService.addInterfaceToNetwork(netId, iface);
wakeupModifyInterface(iface, nai, true);
mDeps.reportNetworkInterfaceForTransports(mContext, iface,
nai.networkCapabilities.getTransportTypes());
@@ -8428,45 +8668,13 @@
try {
if (DBG) log("Removing iface " + iface + " from network " + netId);
wakeupModifyInterface(iface, nai, false);
- mNetd.networkRemoveInterface(netId, iface);
+ mRoutingCoordinatorService.removeInterfaceFromNetwork(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
@@ -8487,10 +8695,10 @@
if (route.hasGateway()) continue;
if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
try {
- mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.addRoute(netId, route);
} catch (Exception e) {
if ((route.getDestination().getAddress() instanceof Inet4Address) || VDBG) {
- loge("Exception in networkAddRouteParcel for non-gateway: " + e);
+ loge("Exception in addRoute for non-gateway: " + e);
}
}
}
@@ -8498,10 +8706,10 @@
if (!route.hasGateway()) continue;
if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
try {
- mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.addRoute(netId, route);
} catch (Exception e) {
if ((route.getGateway() instanceof Inet4Address) || VDBG) {
- loge("Exception in networkAddRouteParcel for gateway: " + e);
+ loge("Exception in addRoute for gateway: " + e);
}
}
}
@@ -8509,18 +8717,18 @@
for (RouteInfo route : routeDiff.removed) {
if (VDBG || DDBG) log("Removing Route [" + route + "] from network " + netId);
try {
- mNetd.networkRemoveRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.removeRoute(netId, route);
} catch (Exception e) {
- loge("Exception in networkRemoveRouteParcel: " + e);
+ loge("Exception in removeRoute: " + e);
}
}
for (RouteInfo route : routeDiff.updated) {
if (VDBG || DDBG) log("Updating Route [" + route + "] from network " + netId);
try {
- mNetd.networkUpdateRouteParcel(netId, convertRouteInfo(route));
+ mRoutingCoordinatorService.updateRoute(netId, route);
} catch (Exception e) {
- loge("Exception in networkUpdateRouteParcel: " + e);
+ loge("Exception in updateRoute: " + e);
}
}
return !routeDiff.added.isEmpty() || !routeDiff.removed.isEmpty()
@@ -8827,9 +9035,8 @@
// 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());
+ if (meteredChanged || !newNc.equalsTransportTypes(prevNc)) {
+ mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(), newNc);
}
maybeSendProxyBroadcast(nai, prevNc, newNc);
@@ -8840,6 +9047,80 @@
updateCapabilities(nai.getScore(), nai, nai.networkCapabilities);
}
+ // oldConfig is null iff this is the original registration of the local network config
+ private void handleUpdateLocalNetworkConfig(@NonNull final NetworkAgentInfo nai,
+ @Nullable final LocalNetworkConfig oldConfig,
+ @NonNull final LocalNetworkConfig newConfig) {
+ if (!nai.isLocalNetwork()) {
+ Log.wtf(TAG, "Ignoring update of a local network info on non-local network " + nai);
+ return;
+ }
+
+ if (VDBG) {
+ Log.v(TAG, "Update local network config " + nai.network.netId + " : " + newConfig);
+ }
+ final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
+ // TODO : apply the diff for multicast routing.
+ configBuilder.setUpstreamMulticastRoutingConfig(
+ newConfig.getUpstreamMulticastRoutingConfig());
+ configBuilder.setDownstreamMulticastRoutingConfig(
+ newConfig.getDownstreamMulticastRoutingConfig());
+
+ final NetworkRequest oldRequest =
+ (null == oldConfig) ? null : oldConfig.getUpstreamSelector();
+ final NetworkCapabilities oldCaps =
+ (null == oldRequest) ? null : oldRequest.networkCapabilities;
+ final NetworkRequestInfo oldNri =
+ null == oldRequest ? null : mNetworkRequests.get(oldRequest);
+ final NetworkAgentInfo oldSatisfier =
+ null == oldNri ? null : oldNri.getSatisfier();
+ final NetworkRequest newRequest = newConfig.getUpstreamSelector();
+ final NetworkCapabilities newCaps =
+ (null == newRequest) ? null : newRequest.networkCapabilities;
+ final boolean requestUpdated = !Objects.equals(newCaps, oldCaps);
+ if (null != oldRequest && requestUpdated) {
+ handleRemoveNetworkRequest(mNetworkRequests.get(oldRequest));
+ if (null == newRequest && null != oldSatisfier) {
+ // If there is an old satisfier, but no new request, then remove the old upstream.
+ removeLocalNetworkUpstream(nai, oldSatisfier);
+ nai.localNetworkConfig = configBuilder.build();
+ // When there is a new request, the rematch sees the new request and sends the
+ // LOCAL_NETWORK_INFO_CHANGED callbacks accordingly.
+ // But here there is no new request, so the rematch won't see anything. Send
+ // callbacks to apps now to tell them about the loss of upstream.
+ notifyNetworkCallbacks(nai,
+ ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ return;
+ }
+ }
+ if (null != newRequest && requestUpdated) {
+ // File the new request if :
+ // - it has changed (requestUpdated), or
+ // - it's the first time this local info (null == oldConfig)
+ // is updated and the request has not been filed yet.
+ // Requests for local info are always LISTEN_FOR_BEST, because they have at most one
+ // upstream (the best) but never request it to be brought up.
+ final NetworkRequest nr = new NetworkRequest(newCaps, ConnectivityManager.TYPE_NONE,
+ nextNetworkRequestId(), LISTEN_FOR_BEST);
+ configBuilder.setUpstreamSelector(nr);
+ final NetworkRequestInfo nri = new NetworkRequestInfo(
+ nai.creatorUid, nr, null /* messenger */, null /* binder */,
+ 0 /* callbackFlags */, null /* attributionTag */);
+ if (null != oldSatisfier) {
+ // Set the old satisfier in the new NRI so that the rematch will see any changes
+ nri.setSatisfier(oldSatisfier, nr);
+ }
+ nai.localNetworkConfig = configBuilder.build();
+ // handleRegisterNetworkRequest causes a rematch. The rematch must happen after
+ // nai.localNetworkConfig is set, since it will base its callbacks on the old
+ // satisfier and the new request.
+ handleRegisterNetworkRequest(nri);
+ } else {
+ configBuilder.setUpstreamSelector(oldRequest);
+ nai.localNetworkConfig = configBuilder.build();
+ }
+ }
+
/**
* Returns the interface which requires VPN isolation (ingress interface filtering).
*
@@ -9135,6 +9416,8 @@
// else not handled
}
+ // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
+ @SuppressLint("NewApi")
private void sendIntent(PendingIntent pendingIntent, Intent intent) {
mPendingIntentWakeLock.acquire();
try {
@@ -9166,6 +9449,21 @@
releasePendingNetworkRequestWithDelay(pendingIntent);
}
+ @Nullable
+ private LocalNetworkInfo localNetworkInfoForNai(@NonNull final NetworkAgentInfo nai) {
+ if (!nai.isLocalNetwork()) return null;
+ final Network upstream;
+ final NetworkRequest selector = nai.localNetworkConfig.getUpstreamSelector();
+ if (null == selector) {
+ upstream = null;
+ } else {
+ final NetworkRequestInfo upstreamNri = mNetworkRequests.get(selector);
+ final NetworkAgentInfo satisfier = upstreamNri.getSatisfier();
+ upstream = (null == satisfier) ? null : satisfier.network;
+ }
+ return new LocalNetworkInfo.Builder().setUpstreamNetwork(upstream).build();
+ }
+
// networkAgent is only allowed to be null if notificationType is
// CALLBACK_UNAVAIL. This is because UNAVAIL is about no network being
// available, while all other cases are about some particular network.
@@ -9178,7 +9476,7 @@
// are Type.LISTEN, but should not have NetworkCallbacks invoked.
return;
}
- Bundle bundle = new Bundle();
+ final Bundle bundle = new Bundle();
// TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
// TODO: check if defensive copies of data is needed.
final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
@@ -9201,6 +9499,10 @@
putParcelable(bundle, nc);
putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
networkAgent.linkProperties, nri.mPid, nri.mUid));
+ // The local network info is often null, so can't use the static putParcelable
+ // method here.
+ bundle.putParcelable(LocalNetworkInfo.class.getSimpleName(),
+ localNetworkInfoForNai(networkAgent));
// For this notification, arg1 contains the blocked status.
msg.arg1 = arg1;
break;
@@ -9232,6 +9534,14 @@
msg.arg1 = arg1;
break;
}
+ case ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED: {
+ if (!networkAgent.isLocalNetwork()) {
+ Log.wtf(TAG, "Callback for local info for a non-local network");
+ return;
+ }
+ putParcelable(bundle, localNetworkInfoForNai(networkAgent));
+ break;
+ }
}
msg.what = notificationType;
msg.setData(bundle);
@@ -9363,7 +9673,7 @@
if (oldDefaultNetwork != null) {
mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
}
- mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+ mNetworkActivityTracker.updateDefaultNetwork(newDefaultNetwork, oldDefaultNetwork);
maybeClosePendingFrozenSockets(newDefaultNetwork, oldDefaultNetwork);
mProxyTracker.setDefaultProxy(null != newDefaultNetwork
? newDefaultNetwork.linkProperties.getHttpProxy() : null);
@@ -9617,7 +9927,8 @@
if (VDBG) log("rematch for " + newSatisfier.toShortString());
if (null != previousRequest && null != previousSatisfier) {
if (VDBG || DDBG) {
- log(" accepting network in place of " + previousSatisfier.toShortString());
+ log(" accepting network in place of " + previousSatisfier.toShortString()
+ + " for " + newRequest);
}
previousSatisfier.removeRequest(previousRequest.requestId);
if (canSupportGracefulNetworkSwitch(previousSatisfier, newSatisfier)
@@ -9636,7 +9947,7 @@
previousSatisfier.lingerRequest(previousRequest.requestId, now);
}
} else {
- if (VDBG || DDBG) log(" accepting network in place of null");
+ if (VDBG || DDBG) log(" accepting network in place of null for " + newRequest);
}
// To prevent constantly CPU wake up for nascent timer, if a network comes up
@@ -9752,6 +10063,14 @@
}
}
+ private boolean hasSameInterfaceName(@Nullable final NetworkAgentInfo nai1,
+ @Nullable final NetworkAgentInfo nai2) {
+ if (null == nai1) return null == nai2;
+ if (null == nai2) return false;
+ return nai1.linkProperties.getInterfaceName()
+ .equals(nai2.linkProperties.getInterfaceName());
+ }
+
private void applyNetworkReassignment(@NonNull final NetworkReassignment changes,
final long now) {
final Collection<NetworkAgentInfo> nais = mNetworkAgentInfos;
@@ -9777,6 +10096,45 @@
// Process default network changes if applicable.
processDefaultNetworkChanges(changes);
+ // Update forwarding rules for the upstreams of local networks. Do this before sending
+ // onAvailable so that by the time onAvailable is sent the forwarding rules are set up.
+ // Don't send CALLBACK_LOCAL_NETWORK_INFO_CHANGED yet though : they should be sent after
+ // onAvailable so clients know what network the change is about. Store such changes in
+ // an array that's only allocated if necessary (because it's almost never necessary).
+ ArrayList<NetworkAgentInfo> localInfoChangedAgents = null;
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (!nai.isLocalNetwork()) continue;
+ final NetworkRequest nr = nai.localNetworkConfig.getUpstreamSelector();
+ if (null == nr) continue; // No upstream for this local network
+ final NetworkRequestInfo nri = mNetworkRequests.get(nr);
+ final NetworkReassignment.RequestReassignment change = changes.getReassignment(nri);
+ if (null == change) continue; // No change in upstreams for this network
+ final String fromIface = nai.linkProperties.getInterfaceName();
+ if (!hasSameInterfaceName(change.mOldNetwork, change.mNewNetwork)
+ || change.mOldNetwork.isDestroyed()) {
+ // There can be a change with the same interface name if the new network is the
+ // replacement for the old network that was unregisteredAfterReplacement.
+ try {
+ if (null != change.mOldNetwork) {
+ mRoutingCoordinatorService.removeInterfaceForward(fromIface,
+ change.mOldNetwork.linkProperties.getInterfaceName());
+ }
+ // If the new upstream is already destroyed, there is no point in setting up
+ // a forward (in fact, it might forward to the interface for some new network !)
+ // Later when the upstream disconnects CS will try to remove the forward, which
+ // is ignored with a benign log by RoutingCoordinatorService.
+ if (null != change.mNewNetwork && !change.mNewNetwork.isDestroyed()) {
+ mRoutingCoordinatorService.addInterfaceForward(fromIface,
+ change.mNewNetwork.linkProperties.getInterfaceName());
+ }
+ } catch (final RemoteException e) {
+ loge("Can't update forwarding rules", e);
+ }
+ }
+ if (null == localInfoChangedAgents) localInfoChangedAgents = new ArrayList<>();
+ localInfoChangedAgents.add(nai);
+ }
+
// Notify requested networks are available after the default net is switched, but
// before LegacyTypeTracker sends legacy broadcasts
for (final NetworkReassignment.RequestReassignment event :
@@ -9825,6 +10183,14 @@
notifyNetworkLosing(nai, now);
}
+ // Send LOCAL_NETWORK_INFO_CHANGED callbacks now that onAvailable and onLost have been sent.
+ if (null != localInfoChangedAgents) {
+ for (final NetworkAgentInfo nai : localInfoChangedAgents) {
+ notifyNetworkCallbacks(nai,
+ ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
+ }
+ }
+
updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
// Tear down all unneeded networks.
@@ -10149,7 +10515,7 @@
// If a rate limit has been configured and is applicable to this network (network
// provides internet connectivity), apply it. The tc police filter cannot be attached
// before the clsact qdisc is added which happens as part of updateLinkProperties ->
- // updateInterfaces -> INetd#networkAddInterface.
+ // updateInterfaces -> RoutingCoordinatorManager#addInterfaceToNetwork
// Note: in case of a system server crash, the NetworkController constructor in netd
// (called when netd starts up) deletes the clsact qdisc of all interfaces.
if (canNetworkBeRateLimited(networkAgent) && mIngressRateLimit >= 0) {
@@ -10220,6 +10586,15 @@
SystemClock.elapsedRealtime(), mNascentDelayMs);
networkAgent.setInactive();
+ if (mTrackMultiNetworkActivities) {
+ // Start tracking activity of this network.
+ // This must be called before rematchAllNetworksAndRequests since the network
+ // should be tracked when the network becomes the default network.
+ // This method does not trigger any callbacks or broadcasts. Callbacks or broadcasts
+ // can be triggered later if this network becomes the default network.
+ mNetworkActivityTracker.setupDataActivityTracking(networkAgent);
+ }
+
// Consider network even though it is not yet validated.
rematchAllNetworksAndRequests();
@@ -10726,6 +11101,17 @@
Log.d(TAG, "Reevaluating network " + nai.network);
reportNetworkConnectivity(nai.network, !nai.isValidated());
return 0;
+ case "bpf-get-cgroup-program-id": {
+ // Usage : adb shell cmd connectivity bpf-get-cgroup-program-id <type>
+ // Get cgroup bpf program Id for the given type. See BpfUtils#getProgramId
+ // for more detail.
+ // If type can't be parsed, this throws NumberFormatException, which
+ // is passed back to adb who prints it.
+ final int type = Integer.parseInt(getNextArg());
+ final int ret = BpfUtils.getProgramId(type);
+ pw.println(ret);
+ return 0;
+ }
default:
return handleDefaultCommands(cmd);
}
@@ -11267,6 +11653,10 @@
if (checkNetworkStackPermission(callbackPid, callbackUid)) {
return true;
}
+ if (mAllowSysUiConnectivityReports
+ && checkSystemBarServicePermission(callbackPid, callbackUid)) {
+ return true;
+ }
// Administrator UIDs also contains the Owner UID
final int[] administratorUids = nai.networkCapabilities.getAdministratorUids();
@@ -11396,8 +11786,8 @@
*/
private static final class NetworkActivityParams {
public final boolean isActive;
- // Label used for idle timer. Transport type is used as label.
- // label is int since NMS was using the identifier as int, and it has not been changed
+ // If TrackMultiNetworkActivities is enabled, idleTimer label is netid.
+ // If TrackMultiNetworkActivities is disabled, idleTimer label is transport type.
public final int label;
public final long timestampNs;
// Uid represents the uid that was responsible for waking the radio.
@@ -11439,13 +11829,15 @@
}
}
+ private final boolean mTrackMultiNetworkActivities;
private final LegacyNetworkActivityTracker mNetworkActivityTracker;
/**
* Class used for updating network activity tracking with netd and notify network activity
* changes.
*/
- private static final class LegacyNetworkActivityTracker {
+ @VisibleForTesting
+ public static final class LegacyNetworkActivityTracker {
private static final int NO_UID = -1;
private final Context mContext;
private final INetd mNetd;
@@ -11457,7 +11849,14 @@
// If there is no default network, default network is considered active to keep the existing
// behavior. Initial value is used until first connect to the default network.
private volatile boolean mIsDefaultNetworkActive = true;
- private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap<>();
+ private Network mDefaultNetwork;
+ // Key is netId. Value is configured idle timer information.
+ private final SparseArray<IdleTimerParams> mActiveIdleTimers = new SparseArray<>();
+ private final boolean mTrackMultiNetworkActivities;
+ // Store netIds of Wi-Fi networks whose idletimers report that they are active
+ private final Set<Integer> mActiveWifiNetworks = new ArraySet<>();
+ // Store netIds of cellular networks whose idletimers report that they are active
+ private final Set<Integer> mActiveCellularNetworks = new ArraySet<>();
private static class IdleTimerParams {
public final int timeout;
@@ -11470,10 +11869,11 @@
}
LegacyNetworkActivityTracker(@NonNull Context context, @NonNull INetd netd,
- @NonNull Handler handler) {
+ @NonNull Handler handler, boolean trackMultiNetworkActivities) {
mContext = context;
mNetd = netd;
mHandler = handler;
+ mTrackMultiNetworkActivities = trackMultiNetworkActivities;
}
private void ensureRunningOnConnectivityServiceThread() {
@@ -11483,19 +11883,97 @@
}
}
- public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
- ensureRunningOnConnectivityServiceThread();
- if (mActiveIdleTimers.isEmpty()) {
+ /**
+ * Update network activity and call BatteryStats to update radio power state if the
+ * mobile or Wi-Fi activity is changed.
+ * LegacyNetworkActivityTracker considers the mobile network is active if at least one
+ * mobile network is active since BatteryStatsService only maintains a single power state
+ * for the mobile network.
+ * The Wi-Fi network is also the same.
+ *
+ * {@link #setupDataActivityTracking} and {@link #removeDataActivityTracking} use
+ * TRANSPORT_CELLULAR as the transportType argument if the network has both cell and Wi-Fi
+ * transports.
+ */
+ private void maybeUpdateRadioPowerState(final int netId, final int transportType,
+ final boolean isActive, final int uid) {
+ if (transportType != TRANSPORT_WIFI && transportType != TRANSPORT_CELLULAR) {
+ Log.e(TAG, "Unexpected transportType in maybeUpdateRadioPowerState: "
+ + transportType);
+ return;
+ }
+ final Set<Integer> activeNetworks = transportType == TRANSPORT_WIFI
+ ? mActiveWifiNetworks : mActiveCellularNetworks;
+
+ final boolean wasEmpty = activeNetworks.isEmpty();
+ if (isActive) {
+ activeNetworks.add(netId);
+ } else {
+ activeNetworks.remove(netId);
+ }
+
+ if (wasEmpty != activeNetworks.isEmpty()) {
+ updateRadioPowerState(isActive, transportType, uid);
+ }
+ }
+
+ private void handleDefaultNetworkActivity(final int transportType,
+ final boolean isActive, final long timestampNs) {
+ mIsDefaultNetworkActive = isActive;
+ sendDataActivityBroadcast(transportTypeToLegacyType(transportType),
+ isActive, timestampNs);
+ if (isActive) {
+ reportNetworkActive();
+ }
+ }
+
+ private void handleReportNetworkActivityWithNetIdLabel(
+ NetworkActivityParams activityParams) {
+ final int netId = activityParams.label;
+ final IdleTimerParams idleTimerParams = mActiveIdleTimers.get(netId);
+ if (idleTimerParams == null) {
+ // This network activity change is not tracked anymore
+ // This can happen if netd callback post activity change event message but idle
+ // timer is removed before processing this message.
+ return;
+ }
+ // TODO: if a network changes transports, storing the transport type in the
+ // IdleTimerParams is not correct. Consider getting it from the network's
+ // NetworkCapabilities instead.
+ final int transportType = idleTimerParams.transportType;
+ maybeUpdateRadioPowerState(netId, transportType,
+ activityParams.isActive, activityParams.uid);
+
+ if (mDefaultNetwork == null || mDefaultNetwork.netId != netId) {
+ // This activity change is not for the default network.
+ return;
+ }
+
+ handleDefaultNetworkActivity(transportType, activityParams.isActive,
+ activityParams.timestampNs);
+ }
+
+ private void handleReportNetworkActivityWithTransportTypeLabel(
+ NetworkActivityParams activityParams) {
+ if (mActiveIdleTimers.size() == 0) {
// This activity change is not for the current default network.
// This can happen if netd callback post activity change event message but
// the default network is lost before processing this message.
return;
}
- sendDataActivityBroadcast(transportTypeToLegacyType(activityParams.label),
- activityParams.isActive, activityParams.timestampNs);
- mIsDefaultNetworkActive = activityParams.isActive;
- if (mIsDefaultNetworkActive) {
- reportNetworkActive();
+ handleDefaultNetworkActivity(activityParams.label, activityParams.isActive,
+ activityParams.timestampNs);
+ }
+
+ /**
+ * Handle network activity change
+ */
+ public void handleReportNetworkActivity(NetworkActivityParams activityParams) {
+ ensureRunningOnConnectivityServiceThread();
+ if (mTrackMultiNetworkActivities) {
+ handleReportNetworkActivityWithNetIdLabel(activityParams);
+ } else {
+ handleReportNetworkActivityWithTransportTypeLabel(activityParams);
}
}
@@ -11552,6 +12030,30 @@
}
/**
+ * Get idle timer label
+ */
+ @VisibleForTesting
+ public static int getIdleTimerLabel(final boolean trackMultiNetworkActivities,
+ final int netId, final int transportType) {
+ return trackMultiNetworkActivities ? netId : transportType;
+ }
+
+ private boolean maybeCreateIdleTimer(
+ String iface, int netId, int timeout, int transportType) {
+ if (timeout <= 0 || iface == null) return false;
+ try {
+ final String label = Integer.toString(getIdleTimerLabel(
+ mTrackMultiNetworkActivities, netId, transportType));
+ mNetd.idletimerAddInterface(iface, timeout, label);
+ mActiveIdleTimers.put(netId, new IdleTimerParams(timeout, transportType));
+ return true;
+ } catch (Exception e) {
+ loge("Exception in createIdleTimer", e);
+ return false;
+ }
+ }
+
+ /**
* Setup data activity tracking for the given network.
*
* Every {@code setupDataActivityTracking} should be paired with a
@@ -11560,12 +12062,17 @@
* @return true if the idleTimer is added to the network, false otherwise
*/
private boolean setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+ ensureRunningOnConnectivityServiceThread();
final String iface = networkAgent.linkProperties.getInterfaceName();
+ final int netId = networkAgent.network().netId;
final int timeout;
final int type;
- if (networkAgent.networkCapabilities.hasTransport(
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+ // Do not track VPN network.
+ return false;
+ } else if (networkAgent.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_CELLULAR)) {
timeout = Settings.Global.getInt(mContext.getContentResolver(),
ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
@@ -11581,32 +12088,32 @@
return false; // do not track any other networks
}
- updateRadioPowerState(true /* isActive */, type);
-
- if (timeout > 0 && iface != null) {
- try {
- mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
- mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
- return true;
- } catch (Exception e) {
- // You shall not crash!
- loge("Exception in setupDataActivityTracking " + e);
- }
+ final boolean hasIdleTimer = maybeCreateIdleTimer(iface, netId, timeout, type);
+ if (hasIdleTimer || !mTrackMultiNetworkActivities) {
+ // If trackMultiNetwork is disabled, NetworkActivityTracker updates radio power
+ // state in all cases. If trackMultiNetwork is enabled, it updates radio power
+ // state only about a network that has an idletimer.
+ maybeUpdateRadioPowerState(netId, type, true /* isActive */, NO_UID);
}
- return false;
+ return hasIdleTimer;
}
/**
* Remove data activity tracking when network disconnects.
*/
- private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ public void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ ensureRunningOnConnectivityServiceThread();
final String iface = networkAgent.linkProperties.getInterfaceName();
+ final int netId = networkAgent.network().netId;
final NetworkCapabilities caps = networkAgent.networkCapabilities;
if (iface == null) return;
final int type;
- if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_VPN)) {
+ // Do not track VPN network.
+ return;
+ } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
type = NetworkCapabilities.TRANSPORT_CELLULAR;
} else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
type = NetworkCapabilities.TRANSPORT_WIFI;
@@ -11615,15 +12122,17 @@
}
try {
- updateRadioPowerState(false /* isActive */, type);
- final IdleTimerParams params = mActiveIdleTimers.remove(iface);
+ maybeUpdateRadioPowerState(netId, type, false /* isActive */, NO_UID);
+ final IdleTimerParams params = mActiveIdleTimers.get(netId);
if (params == null) {
// IdleTimer is not added if the configured timeout is 0 or negative value
return;
}
- // The call fails silently if no idle timer setup for this interface
- mNetd.idletimerRemoveInterface(iface, params.timeout,
- Integer.toString(params.transportType));
+ mActiveIdleTimers.remove(netId);
+ final String label = Integer.toString(getIdleTimerLabel(
+ mTrackMultiNetworkActivities, netId, params.transportType));
+ // The call fails silently if no idle timer setup for this interface
+ mNetd.idletimerRemoveInterface(iface, params.timeout, label);
} catch (Exception e) {
// You shall not crash!
loge("Exception in removeDataActivityTracking " + e);
@@ -11633,12 +12142,15 @@
private void updateDefaultNetworkActivity(NetworkAgentInfo defaultNetwork,
boolean hasIdleTimer) {
if (defaultNetwork != null) {
+ mDefaultNetwork = defaultNetwork.network();
mIsDefaultNetworkActive = true;
- // Callbacks are called only when the network has the idle timer.
- if (hasIdleTimer) {
+ // If only the default network is tracked, callbacks are called only when the
+ // network has the idle timer.
+ if (mTrackMultiNetworkActivities || hasIdleTimer) {
reportNetworkActive();
}
} else {
+ mDefaultNetwork = null;
// If there is no default network, default network is considered active to keep the
// existing behavior.
mIsDefaultNetworkActive = true;
@@ -11646,29 +12158,34 @@
}
/**
- * Update data activity tracking when network state is updated.
+ * Update the default network this class tracks the activity of.
*/
- public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
+ public void updateDefaultNetwork(NetworkAgentInfo newNetwork,
NetworkAgentInfo oldNetwork) {
ensureRunningOnConnectivityServiceThread();
+ // If TrackMultiNetworkActivities is enabled, devices add idleTimer when the network is
+ // first connected and remove when the network is disconnected.
+ // If TrackMultiNetworkActivities is disabled, devices add idleTimer when the network
+ // becomes the default network and remove when the network becomes no longer the default
+ // network.
boolean hasIdleTimer = false;
- if (newNetwork != null) {
+ if (!mTrackMultiNetworkActivities && newNetwork != null) {
hasIdleTimer = setupDataActivityTracking(newNetwork);
}
updateDefaultNetworkActivity(newNetwork, hasIdleTimer);
- if (oldNetwork != null) {
+ if (!mTrackMultiNetworkActivities && oldNetwork != null) {
removeDataActivityTracking(oldNetwork);
}
}
- private void updateRadioPowerState(boolean isActive, int transportType) {
+ private void updateRadioPowerState(boolean isActive, int transportType, int uid) {
final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
switch (transportType) {
case NetworkCapabilities.TRANSPORT_CELLULAR:
- bs.reportMobileRadioPowerState(isActive, NO_UID);
+ bs.reportMobileRadioPowerState(isActive, uid);
break;
case NetworkCapabilities.TRANSPORT_WIFI:
- bs.reportWifiRadioPowerState(isActive, NO_UID);
+ bs.reportWifiRadioPowerState(isActive, uid);
break;
default:
logw("Untracked transport type:" + transportType);
@@ -11688,20 +12205,24 @@
}
public void dump(IndentingPrintWriter pw) {
+ pw.print("mTrackMultiNetworkActivities="); pw.println(mTrackMultiNetworkActivities);
pw.print("mIsDefaultNetworkActive="); pw.println(mIsDefaultNetworkActive);
+ pw.print("mDefaultNetwork="); pw.println(mDefaultNetwork);
pw.println("Idle timers:");
try {
- for (Map.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
- pw.print(" "); pw.print(ent.getKey()); pw.println(":");
- final IdleTimerParams params = ent.getValue();
+ for (int i = 0; i < mActiveIdleTimers.size(); i++) {
+ pw.print(" "); pw.print(mActiveIdleTimers.keyAt(i)); pw.println(":");
+ final IdleTimerParams params = mActiveIdleTimers.valueAt(i);
pw.print(" timeout="); pw.print(params.timeout);
pw.print(" type="); pw.println(params.transportType);
}
+ pw.println("WiFi active networks: " + mActiveWifiNetworks);
+ pw.println("Cellular active networks: " + mActiveCellularNetworks);
} catch (Exception e) {
- // mActiveIdleTimers should only be accessed from handler thread, except dump().
- // As dump() is never called in normal usage, it would be needlessly expensive
- // to lock the collection only for its benefit.
- // Also, mActiveIdleTimers is not expected to be updated frequently.
+ // mActiveIdleTimers, mActiveWifiNetworks, and mActiveCellularNetworks should only
+ // be accessed from handler thread, except dump(). As dump() is never called in
+ // normal usage, it would be needlessly expensive to lock the collection only for
+ // its benefit. Also, they are not expected to be updated frequently.
// So catching the exception and logging.
pw.println("Failed to dump NetworkActivityTracker: " + e);
}
@@ -12459,6 +12980,27 @@
}
}
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @Override
+ public void setDataSaverEnabled(final boolean enable) {
+ enforceNetworkStackOrSettingsPermission();
+ try {
+ final boolean ret = mNetd.bandwidthEnableDataSaver(enable);
+ if (!ret) {
+ throw new IllegalStateException("Error when changing iptables: " + enable);
+ }
+ } catch (RemoteException e) {
+ // Lack of permission or binder errors.
+ throw new IllegalStateException(e);
+ }
+
+ try {
+ mBpfNetMaps.setDataSaverEnabled(enable);
+ } catch (ServiceSpecificException | UnsupportedOperationException e) {
+ Log.e(TAG, "Failed to set data saver " + enable + " : " + e);
+ }
+ }
+
@Override
public void updateMeteredNetworkAllowList(final int uid, final boolean add) {
enforceNetworkStackOrSettingsPermission();
@@ -12548,6 +13090,7 @@
case ConnectivityManager.FIREWALL_CHAIN_POWERSAVE:
case ConnectivityManager.FIREWALL_CHAIN_RESTRICTED:
case ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY:
+ case ConnectivityManager.FIREWALL_CHAIN_BACKGROUND:
defaultRule = FIREWALL_RULE_DENY;
break;
default:
@@ -12560,7 +13103,7 @@
private void closeSocketsForFirewallChainLocked(final int chain)
throws ErrnoException, SocketException, InterruptedIOException {
- if (mBpfNetMaps.isFirewallAllowList(chain)) {
+ if (BpfNetMapsUtils.isFirewallAllowList(chain)) {
// Allowlist means the firewall denies all by default, uids must be explicitly allowed
// So, close all non-system socket owned by uids that are not explicitly allowed
Set<Range<Integer>> ranges = new ArraySet<>();
@@ -12613,4 +13156,10 @@
enforceNetworkStackPermission(mContext);
return mCdmps;
}
+
+ @Override
+ public IBinder getRoutingCoordinatorService() {
+ enforceNetworkStackPermission(mContext);
+ return mRoutingCoordinatorService;
+ }
}
diff --git a/service/src/com/android/server/NetIdManager.java b/service/src/com/android/server/NetIdManager.java
index 61925c8..27b6b9b 100644
--- a/service/src/com/android/server/NetIdManager.java
+++ b/service/src/com/android/server/NetIdManager.java
@@ -27,6 +27,16 @@
* Class used to reserve and release net IDs.
*
* <p>Instances of this class are thread-safe.
+ *
+ * NetIds are currently 16 bits long and consume 16 bits in the fwmark.
+ * The reason they are large is that applications might get confused if the netId counter
+ * wraps - for example, Network#equals would return true for a current network
+ * and a long-disconnected network.
+ * We could in theory fix this by splitting the identifier in two, e.g., a 24-bit generation
+ * counter and an 8-bit netId. Java Network objects would be constructed from the full 32-bit
+ * number, but only the 8-bit number would be used by netd and the fwmark.
+ * We'd have to fix all code that assumes that it can take a netId or a mark and construct
+ * a Network object from it.
*/
public class NetIdManager {
// Sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
diff --git a/service/src/com/android/server/ServiceManagerWrapper.java b/service/src/com/android/server/ServiceManagerWrapper.java
new file mode 100644
index 0000000..6d99f33
--- /dev/null
+++ b/service/src/com/android/server/ServiceManagerWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server;
+
+import android.annotation.RequiresApi;
+import android.os.IBinder;
+import android.os.Build;
+import android.os.ServiceManager;
+
+/** Provides a way to access {@link ServiceManager#waitForService} API. */
+@RequiresApi(Build.VERSION_CODES.S)
+public final class ServiceManagerWrapper {
+ static {
+ System.loadLibrary("service-connectivity");
+ }
+
+ private ServiceManagerWrapper() {}
+
+ /**
+ * Returns the specified service from the service manager.
+ *
+ * If the service is not running, service manager will attempt to start it, and this function
+ * will wait for it to be ready.
+ *
+ * @return {@code null} only if there are permission problems or fatal errors
+ */
+ public static IBinder waitForService(String serviceName) {
+ return nativeWaitForService(serviceName);
+ }
+
+ private static native IBinder nativeWaitForService(String serviceName);
+}
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 11345d3..8036ae9 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -53,6 +53,7 @@
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
+import android.util.Range;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -77,6 +78,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* Manages automatic on/off socket keepalive requests.
@@ -373,26 +375,27 @@
* Determine if any state transition is needed for the specific automatic keepalive.
*/
public void handleMonitorAutomaticKeepalive(@NonNull final AutomaticOnOffKeepalive ki,
- final int vpnNetId) {
+ final int vpnNetId, @NonNull Set<Range<Integer>> vpnUidRanges) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- handleMonitorTcpConnections(ki, vpnNetId);
+ handleMonitorTcpConnections(ki, vpnNetId, vpnUidRanges);
}
/**
* Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
*/
- private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
+ private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId,
+ @NonNull Set<Range<Integer>> vpnUidRanges) {
// Might happen if the automatic keepalive was removed by the app just as the alarm fires.
if (!mAutomaticOnOffKeepalives.contains(ki)) return;
if (STATE_ALWAYS_ON == ki.mAutomaticOnOffState) {
throw new IllegalStateException("Should not monitor non-auto keepalive");
}
- if (!isAnyTcpSocketConnected(vpnNetId)) {
+ if (!isAnyTcpSocketConnected(vpnNetId, vpnUidRanges)) {
// No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
// SUSPENDED.
if (ki.mAutomaticOnOffState == STATE_ENABLED) {
@@ -692,8 +695,10 @@
/**
* Dump AutomaticOnOffKeepaliveTracker state.
+ * This should be only be called in ConnectivityService handler thread.
*/
public void dump(IndentingPrintWriter pw) {
+ ensureRunningOnHandlerThread();
mKeepaliveTracker.dump(pw);
// Reading DeviceConfig will check if the calling uid and calling package name are the same.
// Clear calling identity to align the calling uid and package so that it won't fail if cts
@@ -712,6 +717,9 @@
pw.increaseIndent();
mEventLog.reverseDump(pw);
pw.decreaseIndent();
+
+ pw.println();
+ mKeepaliveStatsTracker.dump(pw);
}
/**
@@ -739,7 +747,7 @@
}
@VisibleForTesting
- boolean isAnyTcpSocketConnected(int netId) {
+ boolean isAnyTcpSocketConnected(int netId, @NonNull Set<Range<Integer>> vpnUidRanges) {
FileDescriptor fd = null;
try {
@@ -752,7 +760,8 @@
// Send request for each IP family
for (final int family : ADDRESS_FAMILIES) {
- if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
+ if (isAnyTcpSocketConnectedForFamily(
+ fd, family, networkMark, networkMask, vpnUidRanges)) {
return true;
}
}
@@ -766,7 +775,8 @@
}
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
- int networkMask) throws ErrnoException, InterruptedIOException {
+ int networkMask, @NonNull Set<Range<Integer>> vpnUidRanges)
+ throws ErrnoException, InterruptedIOException {
ensureRunningOnHandlerThread();
// Build SocketDiag messages and cache it.
if (mSockDiagMsg.get(family) == null) {
@@ -895,7 +905,7 @@
public FileDescriptor createConnectedNetlinkSocket()
throws ErrnoException, SocketException {
final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
- NetlinkUtils.connectSocketToNetlink(fd);
+ NetlinkUtils.connectToKernel(fd);
Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
StructTimeval.fromMillis(IO_TIMEOUT_MS));
return fd;
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index ab7b1a7..5705ebe 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -17,6 +17,7 @@
package com.android.server.connectivity;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
@@ -31,6 +32,8 @@
import android.net.NetworkCapabilities;
import android.net.NetworkSpecifier;
import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
@@ -41,12 +44,13 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.HandlerExecutor;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.networkstack.apishim.TelephonyManagerShimImpl;
import com.android.networkstack.apishim.common.TelephonyManagerShim;
import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.ConnectivityService;
import java.util.ArrayList;
import java.util.List;
@@ -77,13 +81,13 @@
private final boolean mUseCallbacksForServiceChanged;
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
- @NonNull final ConnectivityService.Dependencies deps,
+ @NonNull final Dependencies deps,
@NonNull final TelephonyManager t,
@NonNull final TelephonyManagerShim telephonyManagerShim) {
mContext = c;
mTelephonyManager = t;
mTelephonyManagerShim = telephonyManagerShim;
- final HandlerThread thread = new HandlerThread(TAG);
+ final HandlerThread thread = deps.makeHandlerThread();
thread.start();
mHandler = new Handler(thread.getLooper());
mUseCallbacksForServiceChanged = deps.isFeatureEnabled(
@@ -109,9 +113,24 @@
}
public CarrierPrivilegeAuthenticator(@NonNull final Context c,
- @NonNull final ConnectivityService.Dependencies deps,
@NonNull final TelephonyManager t) {
- this(c, deps, t, TelephonyManagerShimImpl.newInstance(t));
+ this(c, new Dependencies(), t, TelephonyManagerShimImpl.newInstance(t));
+ }
+
+ public static class Dependencies {
+ /**
+ * Create a HandlerThread to use in CarrierPrivilegeAuthenticator.
+ */
+ public HandlerThread makeHandlerThread() {
+ return new HandlerThread(TAG);
+ }
+
+ /**
+ * @see DeviceConfigUtils#isTetheringFeatureEnabled
+ */
+ public boolean isFeatureEnabled(Context context, String name) {
+ return DeviceConfigUtils.isTetheringFeatureEnabled(context, name);
+ }
}
private void simConfigChanged() {
@@ -125,11 +144,13 @@
private class PrivilegeListener implements CarrierPrivilegesListenerShim {
public final int mLogicalSlot;
+
PrivilegeListener(final int logicalSlot) {
mLogicalSlot = logicalSlot;
}
- @Override public void onCarrierPrivilegesChanged(
+ @Override
+ public void onCarrierPrivilegesChanged(
@NonNull List<String> privilegedPackageNames,
@NonNull int[] privilegedUids) {
if (mUseCallbacksForServiceChanged) return;
@@ -193,12 +214,13 @@
*
* This returns whether the passed UID is the carrier service package for the subscription ID
* stored in the telephony network specifier in the passed network capabilities.
- * If the capabilities don't code for a cellular network, or if they don't have the
+ * If the capabilities don't code for a cellular or Wi-Fi network, or if they don't have the
* subscription ID in their specifier, this returns false.
*
- * This method can be used to check that a network request for {@link NET_CAPABILITY_CBS} is
- * allowed for the UID of a caller, which must hold carrier privilege and provide the carrier
- * config.
+ * This method can be used to check that a network request that requires the UID to be
+ * the carrier service UID is indeed called by such a UID. An example of such a network could
+ * be a network with the {@link android.net.NetworkCapabilities#NET_CAPABILITY_CBS}
+ * capability.
* It can also be used to check that a factory is entitled to grant access to a given network
* to a given UID on grounds that it is the carrier service package.
*
@@ -206,11 +228,28 @@
* @param networkCapabilities the network capabilities for which carrier privilege is checked.
* @return true if uid provides the relevant carrier config else false.
*/
- public boolean hasCarrierPrivilegeForNetworkCapabilities(int callingUid,
+ public boolean isCarrierServiceUidForNetworkCapabilities(int callingUid,
@NonNull NetworkCapabilities networkCapabilities) {
if (callingUid == Process.INVALID_UID) return false;
- if (!networkCapabilities.hasSingleTransport(TRANSPORT_CELLULAR)) return false;
- final int subId = getSubIdFromNetworkSpecifier(networkCapabilities.getNetworkSpecifier());
+ final int subId;
+ if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)) {
+ subId = getSubIdFromTelephonySpecifier(networkCapabilities.getNetworkSpecifier());
+ } else if (networkCapabilities.hasSingleTransportBesidesTest(TRANSPORT_WIFI)) {
+ subId = getSubIdFromWifiTransportInfo(networkCapabilities.getTransportInfo());
+ } else {
+ subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+ if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ && !networkCapabilities.getSubscriptionIds().contains(subId)) {
+ // Ideally, the code above should just use networkCapabilities.getSubscriptionIds()
+ // for simplicity and future-proofing. However, this is not the historical behavior,
+ // and there is no enforcement that they do not differ, so log a terrible failure if
+ // they do not match to gain confidence this never happens.
+ // TODO : when there is confidence that this never happens, rewrite the code above
+ // with NetworkCapabilities#getSubscriptionIds.
+ Log.wtf(TAG, "NetworkCapabilities subIds are inconsistent between "
+ + "specifier/transportInfo and mSubIds : " + networkCapabilities);
+ }
if (SubscriptionManager.INVALID_SUBSCRIPTION_ID == subId) return false;
return callingUid == getCarrierServiceUidForSubId(subId);
}
@@ -239,14 +278,6 @@
}
@VisibleForTesting
- int getSubIdFromNetworkSpecifier(NetworkSpecifier specifier) {
- if (specifier instanceof TelephonyNetworkSpecifier) {
- return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
- }
- return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
- }
-
- @VisibleForTesting
int getUidForPackage(String pkgName) {
if (pkgName == null) {
return Process.INVALID_UID;
@@ -271,8 +302,22 @@
return getUidForPackage(getCarrierServicePackageNameForLogicalSlot(slotId));
}
- // Helper methods to avoid having to deal with UnsupportedApiLevelException.
+ @VisibleForTesting
+ int getSubIdFromTelephonySpecifier(@Nullable final NetworkSpecifier specifier) {
+ if (specifier instanceof TelephonyNetworkSpecifier) {
+ return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+ int getSubIdFromWifiTransportInfo(@Nullable final TransportInfo info) {
+ if (info instanceof WifiInfo) {
+ return ((WifiInfo) info).getSubscriptionId();
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
+ // Helper methods to avoid having to deal with UnsupportedApiLevelException.
private void addCarrierPrivilegesListener(@NonNull final Executor executor,
@NonNull final PrivilegeListener listener) {
try {
@@ -292,4 +337,16 @@
Log.e(TAG, "removeCarrierPrivilegesListener API is not available");
}
}
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("CarrierPrivilegeAuthenticator:");
+ synchronized (mLock) {
+ final int size = mCarrierServiceUid.size();
+ for (int i = 0; i < size; ++i) {
+ final int logicalSlot = mCarrierServiceUid.keyAt(i);
+ final int serviceUid = mCarrierServiceUid.valueAt(i);
+ pw.println("Logical slot = " + logicalSlot + " : uid = " + serviceUid);
+ }
+ }
+ }
}
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 5aac8f1..f8f76ef 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -33,7 +33,6 @@
public static final String NO_REMATCH_ALL_REQUESTS_ON_REGISTER =
"no_rematch_all_requests_on_register";
- @VisibleForTesting
public static final String CARRIER_SERVICE_CHANGED_USE_CALLBACK =
"carrier_service_changed_use_callback_version";
diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
index c1ba40e..cf6127f 100644
--- a/service/src/com/android/server/connectivity/ConnectivityNativeService.java
+++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java
@@ -16,9 +16,6 @@
package com.android.server.connectivity;
-import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND;
-import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -31,11 +28,9 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.BpfBitmap;
-import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.PermissionUtils;
-import java.io.IOException;
import java.util.ArrayList;
/**
@@ -45,11 +40,7 @@
public static final String SERVICE_NAME = "connectivity_native";
private static final String TAG = ConnectivityNativeService.class.getSimpleName();
- private static final String CGROUP_PATH = "/sys/fs/cgroup";
- private static final String V4_PROG_PATH =
- "/sys/fs/bpf/net_shared/prog_block_bind4_block_port";
- private static final String V6_PROG_PATH =
- "/sys/fs/bpf/net_shared/prog_block_bind6_block_port";
+
private static final String BLOCKED_PORTS_MAP_PATH =
"/sys/fs/bpf/net_shared/map_block_blocked_ports_map";
@@ -95,7 +86,6 @@
protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) {
mContext = context;
mBpfBlockedPortsMap = deps.getBlockPortsMap();
- attachProgram();
}
@Override
@@ -155,23 +145,4 @@
public String getInterfaceHash() {
return this.HASH;
}
-
- /**
- * Attach BPF program
- */
- private void attachProgram() {
- try {
- BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0);
- } catch (IOException e) {
- throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: "
- + e);
- }
- try {
- BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0);
- } catch (IOException e) {
- throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: "
- + e);
- }
- Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs");
- }
}
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 1493cae..894bcc4 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -38,6 +38,7 @@
import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.ResolverParamsParcel;
import android.net.Uri;
import android.net.shared.PrivateDnsConfig;
@@ -251,7 +252,7 @@
// 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 final Map<Integer, NetworkCapabilities> mNetworkCapabilitiesMap;
private int mSampleValidity;
private int mSuccessThreshold;
@@ -265,7 +266,7 @@
mPrivateDnsMap = new ConcurrentHashMap<>();
mPrivateDnsValidationMap = new HashMap<>();
mLinkPropertiesMap = new HashMap<>();
- mTransportsMap = new HashMap<>();
+ mNetworkCapabilitiesMap = new HashMap<>();
// TODO: Create and register ContentObservers to track every setting
// used herein, posting messages to respond to changes.
@@ -278,7 +279,7 @@
public void removeNetwork(Network network) {
mPrivateDnsMap.remove(network.getNetId());
mPrivateDnsValidationMap.remove(network.getNetId());
- mTransportsMap.remove(network.getNetId());
+ mNetworkCapabilitiesMap.remove(network.getNetId());
mLinkPropertiesMap.remove(network.getNetId());
}
@@ -325,13 +326,17 @@
}
/**
- * 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.
+ * Update {@link NetworkCapabilities} stored in this instance.
+ *
+ * In order to ensure that the resolver has access to necessary information when other events
+ * occur, capabilities are always saved to a hashMap before updating the DNS configuration
+ * whenever a new network is created, transport types are modified, or metered capabilities are
+ * altered for a network. When a network is destroyed, the corresponding entry is removed from
+ * the hashMap. To prevent concurrency issues, the hashMap should always be accessed from the
+ * same thread.
*/
- public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
- mTransportsMap.put(netId, transportTypes);
+ public void updateCapabilitiesForNetwork(int netId, @NonNull final NetworkCapabilities nc) {
+ mNetworkCapabilitiesMap.put(netId, nc);
sendDnsConfigurationForNetwork(netId);
}
@@ -351,8 +356,8 @@
*/
public void sendDnsConfigurationForNetwork(int netId) {
final LinkProperties lp = mLinkPropertiesMap.get(netId);
- final int[] transportTypes = mTransportsMap.get(netId);
- if (lp == null || transportTypes == null) return;
+ final NetworkCapabilities nc = mNetworkCapabilitiesMap.get(netId);
+ if (lp == null || nc == null) return;
updateParametersSettings();
final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
@@ -383,7 +388,8 @@
.collect(Collectors.toList()))
: useTls ? paramsParcel.servers // Opportunistic
: new String[0]; // Off
- paramsParcel.transportTypes = transportTypes;
+ paramsParcel.transportTypes = nc.getTransportTypes();
+ paramsParcel.meteredNetwork = nc.isMetered();
// 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) {
@@ -397,12 +403,13 @@
}
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,
+ + "%d, %d, %s, %s, %s, %b)", paramsParcel.netId,
+ Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
+ paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
+ paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
paramsParcel.retryCount, paramsParcel.tlsName,
- Arrays.toString(paramsParcel.tlsServers)));
+ Arrays.toString(paramsParcel.tlsServers),
+ Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork));
try {
mDnsResolver.setResolverConfiguration(paramsParcel);
diff --git a/service/src/com/android/server/connectivity/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java
new file mode 100644
index 0000000..997ecbf
--- /dev/null
+++ b/service/src/com/android/server/connectivity/HandlerUtils.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+
+/**
+ * Helper class for Handler related utilities.
+ *
+ * @hide
+ */
+public class HandlerUtils {
+ // Note: @hide methods copied from android.os.Handler
+ /**
+ * Runs the specified task synchronously.
+ * <p>
+ * If the current thread is the same as the handler thread, then the runnable
+ * runs immediately without being enqueued. Otherwise, posts the runnable
+ * to the handler and waits for it to complete before returning.
+ * </p><p>
+ * This method is dangerous! Improper use can result in deadlocks.
+ * Never call this method while any locks are held or use it in a
+ * possibly re-entrant manner.
+ * </p><p>
+ * This method is occasionally useful in situations where a background thread
+ * must synchronously await completion of a task that must run on the
+ * handler's thread. However, this problem is often a symptom of bad design.
+ * Consider improving the design (if possible) before resorting to this method.
+ * </p><p>
+ * One example of where you might want to use this method is when you just
+ * set up a Handler thread and need to perform some initialization steps on
+ * it before continuing execution.
+ * </p><p>
+ * If timeout occurs then this method returns <code>false</code> but the runnable
+ * will remain posted on the handler and may already be in progress or
+ * complete at a later time.
+ * </p><p>
+ * When using this method, be sure to use {@link Looper#quitSafely} when
+ * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely.
+ * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
+ * </p>
+ *
+ * @param h The target handler.
+ * @param r The Runnable that will be executed synchronously.
+ * @param timeout The timeout in milliseconds, or 0 to wait indefinitely.
+ *
+ * @return Returns true if the Runnable was successfully executed.
+ * Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ *
+ * @hide This method is prone to abuse and should probably not be in the API.
+ * If we ever do make it part of the API, we might want to rename it to something
+ * less funny like runUnsafe().
+ */
+ public static boolean runWithScissors(@NonNull Handler h, @NonNull Runnable r, long timeout) {
+ if (r == null) {
+ throw new IllegalArgumentException("runnable must not be null");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout must be non-negative");
+ }
+
+ if (Looper.myLooper() == h.getLooper()) {
+ r.run();
+ return true;
+ }
+
+ BlockingRunnable br = new BlockingRunnable(r);
+ return br.postAndWait(h, timeout);
+ }
+
+ private static final class BlockingRunnable implements Runnable {
+ private final Runnable mTask;
+ private boolean mDone;
+
+ BlockingRunnable(Runnable task) {
+ mTask = task;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mTask.run();
+ } finally {
+ synchronized (this) {
+ mDone = true;
+ notifyAll();
+ }
+ }
+ }
+
+ public boolean postAndWait(Handler handler, long timeout) {
+ if (!handler.post(this)) {
+ return false;
+ }
+
+ synchronized (this) {
+ if (timeout > 0) {
+ final long expirationTime = SystemClock.uptimeMillis() + timeout;
+ while (!mDone) {
+ long delay = expirationTime - SystemClock.uptimeMillis();
+ if (delay <= 0) {
+ return false; // timeout
+ }
+ try {
+ wait(delay);
+ } catch (InterruptedException ex) {
+ }
+ }
+ } else {
+ while (!mDone) {
+ try {
+ wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 0c2ed18..7a8b41b 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -34,6 +34,7 @@
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -73,6 +74,9 @@
public class KeepaliveStatsTracker {
private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
private static final int INVALID_KEEPALIVE_ID = -1;
+ // 1 hour acceptable deviation in metrics collection duration time.
+ private static final long MAX_EXPECTED_DURATION_MS =
+ AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 1 * 60 * 60 * 1_000L;
@NonNull private final Handler mConnectivityServiceHandler;
@NonNull private final Dependencies mDependencies;
@@ -709,6 +713,36 @@
return mEnabled.get();
}
+ /**
+ * Checks the DailykeepaliveInfoReported for the following:
+ * 1. total active durations/lifetimes <= total registered durations/lifetimes.
+ * 2. Total time in Durations == total time in Carrier lifetime stats
+ * 3. The total elapsed real time spent is within expectations.
+ */
+ @VisibleForTesting
+ public boolean allMetricsExpected(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
+ int totalRegistered = 0;
+ int totalActiveDurations = 0;
+ int totalTimeSpent = 0;
+ for (DurationForNumOfKeepalive durationForNumOfKeepalive: dailyKeepaliveInfoReported
+ .getDurationPerNumOfKeepalive().getDurationForNumOfKeepaliveList()) {
+ final int n = durationForNumOfKeepalive.getNumOfKeepalive();
+ totalRegistered += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec() * n;
+ totalActiveDurations += durationForNumOfKeepalive.getKeepaliveActiveDurationsMsec() * n;
+ totalTimeSpent += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec();
+ }
+ int totalLifetimes = 0;
+ int totalActiveLifetimes = 0;
+ for (KeepaliveLifetimeForCarrier keepaliveLifetimeForCarrier: dailyKeepaliveInfoReported
+ .getKeepaliveLifetimePerCarrier().getKeepaliveLifetimeForCarrierList()) {
+ totalLifetimes += keepaliveLifetimeForCarrier.getLifetimeMsec();
+ totalActiveLifetimes += keepaliveLifetimeForCarrier.getActiveLifetimeMsec();
+ }
+ return totalActiveDurations <= totalRegistered && totalActiveLifetimes <= totalLifetimes
+ && totalLifetimes == totalRegistered && totalActiveLifetimes == totalActiveDurations
+ && totalTimeSpent <= MAX_EXPECTED_DURATION_MS;
+ }
+
/** Writes the stored metrics to ConnectivityStatsLog and resets. */
public void writeAndResetMetrics() {
ensureRunningOnHandlerThread();
@@ -724,9 +758,21 @@
}
final DailykeepaliveInfoReported dailyKeepaliveInfoReported = buildAndResetMetrics();
+ if (!allMetricsExpected(dailyKeepaliveInfoReported)) {
+ Log.wtf(TAG, "Unexpected metrics values: " + dailyKeepaliveInfoReported.toString());
+ }
mDependencies.writeStats(dailyKeepaliveInfoReported);
}
+ /** Dump KeepaliveStatsTracker state. */
+ public void dump(IndentingPrintWriter pw) {
+ ensureRunningOnHandlerThread();
+ pw.println("KeepaliveStatsTracker enabled: " + isEnabled());
+ pw.increaseIndent();
+ pw.println(buildKeepaliveMetrics().toString());
+ pw.decreaseIndent();
+ }
+
private void ensureRunningOnHandlerThread() {
if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
throw new IllegalStateException(
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index bdd841f..50cad45 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -17,10 +17,13 @@
package com.android.server.connectivity;
import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.NetworkCapabilities.transportNamesOf;
import android.annotation.NonNull;
@@ -35,6 +38,7 @@
import android.net.INetworkAgentRegistry;
import android.net.INetworkMonitor;
import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
import android.net.NattKeepalivePacketData;
import android.net.Network;
import android.net.NetworkAgent;
@@ -64,7 +68,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
-import com.android.modules.utils.build.SdkLevel;
import com.android.server.ConnectivityService;
import java.io.PrintWriter;
@@ -174,6 +177,7 @@
// TODO: make this private with a getter.
@NonNull public NetworkCapabilities networkCapabilities;
@NonNull public final NetworkAgentConfig networkAgentConfig;
+ @Nullable public LocalNetworkConfig localNetworkConfig;
// Underlying networks declared by the agent.
// The networks in this list might be declared by a VPN using setUnderlyingNetworks and are
@@ -427,12 +431,28 @@
private final boolean mHasAutomotiveFeature;
/**
+ * Checks that a proposed update to the NCs of this NAI satisfies structural constraints.
+ *
+ * Some changes to NetworkCapabilities are structurally not supported by the stack, and
+ * NetworkAgents are absolutely never allowed to try and do them. When one of these is
+ * violated, this method returns false, which has ConnectivityService disconnect the network ;
+ * this is meant to guarantee that no implementor ever tries to do this.
+ */
+ public boolean respectsNcStructuralConstraints(@NonNull final NetworkCapabilities proposedNc) {
+ if (networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ != proposedNc.hasCapability(NET_CAPABILITY_LOCAL_NETWORK)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
* Sets the capabilities sent by the agent for later retrieval.
- *
- * This method does not sanitize the capabilities ; instead, use
- * {@link #getDeclaredCapabilitiesSanitized} to retrieve a sanitized
- * copy of the capabilities as they were passed here.
- *
+ * <p>
+ * This method does not sanitize the capabilities before storing them ; instead, use
+ * {@link #getDeclaredCapabilitiesSanitized} to retrieve a sanitized copy of the capabilities
+ * as they were passed here.
+ * <p>
* This method makes a defensive copy to avoid issues where the passed object is later mutated.
*
* @param caps the caps sent by the agent
@@ -453,6 +473,8 @@
* apply to the allowedUids field.
* They also should not mutate immutable capabilities, although for backward-compatibility
* this is not enforced and limited to just a log.
+ * Forbidden capabilities also make no sense for networks, so they are disallowed and
+ * will be ignored with a warning.
*
* @param carrierPrivilegeAuthenticator the authenticator, to check access UIDs.
*/
@@ -461,14 +483,15 @@
final NetworkCapabilities nc = new NetworkCapabilities(mDeclaredCapabilitiesUnsanitized);
if (nc.hasConnectivityManagedCapability()) {
Log.wtf(TAG, "BUG: " + this + " has CS-managed capability.");
+ nc.removeAllForbiddenCapabilities();
}
if (networkCapabilities.getOwnerUid() != nc.getOwnerUid()) {
Log.e(TAG, toShortString() + ": ignoring attempt to change owner from "
+ networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid());
nc.setOwnerUid(networkCapabilities.getOwnerUid());
}
- restrictCapabilitiesFromNetworkAgent(
- nc, creatorUid, mHasAutomotiveFeature, carrierPrivilegeAuthenticator);
+ restrictCapabilitiesFromNetworkAgent(nc, creatorUid, mHasAutomotiveFeature,
+ mConnServiceDeps, carrierPrivilegeAuthenticator);
return nc;
}
@@ -598,6 +621,7 @@
private static final String TAG = ConnectivityService.class.getSimpleName();
private static final boolean VDBG = false;
private final ConnectivityService mConnService;
+ private final ConnectivityService.Dependencies mConnServiceDeps;
private final Context mContext;
private final Handler mHandler;
private final QosCallbackTracker mQosCallbackTracker;
@@ -606,6 +630,7 @@
public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info,
@NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @Nullable LocalNetworkConfig localNetworkConfig,
@NonNull NetworkScore score, Context context,
Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
@@ -623,8 +648,10 @@
networkInfo = info;
linkProperties = lp;
networkCapabilities = nc;
+ this.localNetworkConfig = localNetworkConfig;
networkAgentConfig = config;
mConnService = connService;
+ mConnServiceDeps = deps;
setScore(score); // uses members connService, networkCapabilities and networkAgentConfig
clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
mContext = context;
@@ -901,6 +928,12 @@
}
@Override
+ public void sendLocalNetworkConfig(@NonNull final LocalNetworkConfig config) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_LOCAL_NETWORK_CONFIG_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, config)).sendToTarget();
+ }
+
+ @Override
public void sendScore(@NonNull final NetworkScore score) {
mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED,
new Pair<>(NetworkAgentInfo.this, score)).sendToTarget();
@@ -1227,6 +1260,11 @@
return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
}
+ /** Whether this network is a local network */
+ public boolean isLocalNetwork() {
+ return networkCapabilities.hasCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
+
/**
* Whether this network should propagate the capabilities from its underlying networks.
* Currently only true for VPNs.
@@ -1513,44 +1551,54 @@
* @param hasAutomotiveFeature true if this device has the automotive feature, false otherwise
* @param authenticator the carrier privilege authenticator to check for telephony constraints
*/
- public static void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
+ public void restrictCapabilitiesFromNetworkAgent(@NonNull final NetworkCapabilities nc,
final int creatorUid, final boolean hasAutomotiveFeature,
+ @NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator authenticator) {
if (nc.hasTransport(TRANSPORT_TEST)) {
nc.restrictCapabilitiesForTestNetwork(creatorUid);
}
- if (!areAllowedUidsAcceptableFromNetworkAgent(nc, hasAutomotiveFeature, authenticator)) {
+ if (!areAllowedUidsAcceptableFromNetworkAgent(
+ nc, hasAutomotiveFeature, deps, authenticator)) {
nc.setAllowedUids(new ArraySet<>());
}
}
- private static boolean areAllowedUidsAcceptableFromNetworkAgent(
+ private boolean areAllowedUidsAcceptableFromNetworkAgent(
@NonNull final NetworkCapabilities nc, final boolean hasAutomotiveFeature,
+ @NonNull final ConnectivityService.Dependencies deps,
@Nullable final CarrierPrivilegeAuthenticator carrierPrivilegeAuthenticator) {
// NCs without access UIDs are fine.
if (!nc.hasAllowedUids()) return true;
// S and below must never accept access UIDs, even if an agent sends them, because netd
// didn't support the required feature in S.
- if (!SdkLevel.isAtLeastT()) return false;
+ if (!deps.isAtLeastT()) return false;
// On a non-restricted network, access UIDs make no sense
if (nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) return false;
- // If this network has TRANSPORT_TEST, then the caller can do whatever they want to
- // access UIDs
- if (nc.hasTransport(TRANSPORT_TEST)) return true;
+ // If this network has TRANSPORT_TEST and nothing else, then the caller can do whatever
+ // they want to access UIDs
+ if (nc.hasSingleTransport(TRANSPORT_TEST)) return true;
- // Factories that make ethernet networks can allow UIDs for automotive devices.
- if (nc.hasSingleTransport(TRANSPORT_ETHERNET) && hasAutomotiveFeature) {
- return true;
+ if (nc.hasTransport(TRANSPORT_ETHERNET)) {
+ // Factories that make ethernet networks can allow UIDs for automotive devices.
+ if (hasAutomotiveFeature) return true;
+ // It's also admissible if the ethernet network has TRANSPORT_TEST, as long as it
+ // doesn't have NET_CAPABILITY_INTERNET so it can't become the default network.
+ if (nc.hasTransport(TRANSPORT_TEST) && !nc.hasCapability(NET_CAPABILITY_INTERNET)) {
+ return true;
+ }
+ return false;
}
- // Factories that make cell networks can allow the UID for the carrier service package.
+ // Factories that make cell/wifi networks can allow the UID for the carrier service package.
// This can only work in T where there is support for CarrierPrivilegeAuthenticator
if (null != carrierPrivilegeAuthenticator
- && nc.hasSingleTransport(TRANSPORT_CELLULAR)
+ && (nc.hasSingleTransportBesidesTest(TRANSPORT_CELLULAR)
+ || nc.hasSingleTransportBesidesTest(TRANSPORT_WIFI))
&& (1 == nc.getAllowedUidsNoCopy().size())
- && (carrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ && (carrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
nc.getAllowedUidsNoCopy().valueAt(0), nc))) {
return true;
}
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
new file mode 100644
index 0000000..3350d2d
--- /dev/null
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.net.module.util.NetdUtils.toRouteInfoParcel;
+
+import android.annotation.NonNull;
+import android.net.INetd;
+import android.net.IRoutingCoordinator;
+import android.net.RouteInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Objects;
+
+/**
+ * Class to coordinate routing across multiple clients.
+ *
+ * At present this is just a wrapper for netd methods, but it will be used to host some more
+ * coordination logic in the near future. It can be used to pull up some of the routing logic
+ * from netd into Java land.
+ *
+ * Note that usage of this class is not thread-safe. Clients are responsible for their own
+ * synchronization.
+ */
+public class RoutingCoordinatorService extends IRoutingCoordinator.Stub {
+ private static final String TAG = RoutingCoordinatorService.class.getSimpleName();
+ private final INetd mNetd;
+
+ public RoutingCoordinatorService(@NonNull INetd netd) {
+ mNetd = netd;
+ }
+
+ /**
+ * Add a route for specific network
+ *
+ * @param netId the network to add the route to
+ * @param route the route to add
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @Override
+ public void addRoute(final int netId, final RouteInfo route)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkAddRouteParcel(netId, toRouteInfoParcel(route));
+ }
+
+ /**
+ * Remove a route for specific network
+ *
+ * @param netId the network to remove the route from
+ * @param route the route to remove
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @Override
+ public void removeRoute(final int netId, final RouteInfo route)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkRemoveRouteParcel(netId, toRouteInfoParcel(route));
+ }
+
+ /**
+ * Update a route for specific network
+ *
+ * @param netId the network to update the route for
+ * @param route parcelable with route information
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ @Override
+ public void updateRoute(final int netId, final RouteInfo route)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkUpdateRouteParcel(netId, toRouteInfoParcel(route));
+ }
+
+ /**
+ * Adds an interface to a network. The interface must not be assigned to any network, including
+ * the specified network.
+ *
+ * @param netId the network to add the interface to.
+ * @param iface the name of the interface to add.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ @Override
+ public void addInterfaceToNetwork(final int netId, final String iface)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkAddInterface(netId, iface);
+ }
+
+ /**
+ * Removes an interface from a network. The interface must be assigned to the specified network.
+ *
+ * @param netId the network to remove the interface from.
+ * @param iface the name of the interface to remove.
+ *
+ * @throws ServiceSpecificException in case of failure, with an error code corresponding to the
+ * unix errno.
+ */
+ @Override
+ public void removeInterfaceFromNetwork(final int netId, final String iface)
+ throws ServiceSpecificException, RemoteException {
+ mNetd.networkRemoveInterface(netId, iface);
+ }
+
+ private final Object mIfacesLock = new Object();
+ private static final class ForwardingPair {
+ @NonNull public final String fromIface;
+ @NonNull public final String toIface;
+ ForwardingPair(@NonNull final String fromIface, @NonNull final String toIface) {
+ this.fromIface = fromIface;
+ this.toIface = toIface;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ForwardingPair)) return false;
+
+ final ForwardingPair that = (ForwardingPair) o;
+
+ return fromIface.equals(that.fromIface) && toIface.equals(that.toIface);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = fromIface.hashCode();
+ result = 2 * result + toIface.hashCode();
+ return result;
+ }
+ }
+
+ @GuardedBy("mIfacesLock")
+ private final ArraySet<ForwardingPair> mForwardedInterfaces = new ArraySet<>();
+
+ /**
+ * Add forwarding ip rule
+ *
+ * @param fromIface interface name to add forwarding ip rule
+ * @param toIface interface name to add forwarding ip rule
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void addInterfaceForward(final String fromIface, final String toIface)
+ throws ServiceSpecificException, RemoteException {
+ Objects.requireNonNull(fromIface);
+ Objects.requireNonNull(toIface);
+ Log.i(TAG, "Adding interface forward " + fromIface + " → " + toIface);
+ synchronized (mIfacesLock) {
+ if (mForwardedInterfaces.size() == 0) {
+ mNetd.ipfwdEnableForwarding("RoutingCoordinator");
+ }
+ final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
+ if (mForwardedInterfaces.contains(fwp)) {
+ throw new IllegalStateException("Forward already exists between ifaces "
+ + fromIface + " → " + toIface);
+ }
+ mForwardedInterfaces.add(fwp);
+ // Enables NAT for v4 and filters packets from unknown interfaces
+ mNetd.tetherAddForward(fromIface, toIface);
+ mNetd.ipfwdAddInterfaceForward(fromIface, toIface);
+ }
+ }
+
+ /**
+ * Remove forwarding ip rule
+ *
+ * @param fromIface interface name to remove forwarding ip rule
+ * @param toIface interface name to remove forwarding ip rule
+ * @throws ServiceSpecificException in case of failure, with an error code indicating the
+ * cause of the failure.
+ */
+ public void removeInterfaceForward(final String fromIface, final String toIface)
+ throws ServiceSpecificException, RemoteException {
+ Objects.requireNonNull(fromIface);
+ Objects.requireNonNull(toIface);
+ Log.i(TAG, "Removing interface forward " + fromIface + " → " + toIface);
+ synchronized (mIfacesLock) {
+ final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
+ if (!mForwardedInterfaces.contains(fwp)) {
+ // This can happen when an upstream was unregisteredAfterReplacement. The forward
+ // is removed immediately when the upstream is destroyed, but later when the
+ // network actually disconnects CS does not know that and it asks for removal
+ // again.
+ // This can also happen if the network was destroyed before being set as an
+ // upstream, because then CS does not set up the forward rules seeing how the
+ // interface was removed anyway.
+ // Either way, this is benign.
+ Log.i(TAG, "No forward set up between interfaces " + fromIface + " → " + toIface);
+ return;
+ }
+ mForwardedInterfaces.remove(fwp);
+ try {
+ mNetd.ipfwdRemoveInterfaceForward(fromIface, toIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Exception in ipfwdRemoveInterfaceForward", e);
+ }
+ try {
+ mNetd.tetherRemoveForward(fromIface, toIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Exception in tetherRemoveForward", e);
+ }
+ if (mForwardedInterfaces.size() == 0) {
+ mNetd.ipfwdDisableForwarding("RoutingCoordinator");
+ }
+ }
+ }
+}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 59a63f2..9f1debc 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -74,7 +74,10 @@
"framework-configinfrastructure",
"framework-connectivity.stubs.module_lib",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_defaults {
@@ -93,12 +96,17 @@
srcs: [
"framework/**/DnsPacket.java",
"framework/**/DnsPacketUtils.java",
+ "framework/**/DnsSvcbPacket.java",
+ "framework/**/DnsSvcbRecord.java",
+ "framework/**/HexDump.java",
+ "framework/**/NetworkStackConstants.java",
],
sdk_version: "module_current",
visibility: [
"//packages/services/Iwlan:__subpackages__",
],
libs: [
+ "androidx.annotation_annotation",
"framework-annotations-lib",
"framework-connectivity.stubs.module_lib",
],
@@ -141,7 +149,10 @@
"com.android.tethering",
"//apex_available:platform",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_library {
@@ -169,13 +180,18 @@
"com.android.tethering",
"//apex_available:platform",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
+// The net-utils-device-common-netlink library requires the callers to contain
+// net-utils-device-common-struct.
java_library {
name: "net-utils-device-common-netlink",
srcs: [
- "device/com/android/net/module/util/netlink/*.java",
+ "device/com/android/net/module/util/netlink/**/*.java",
],
sdk_version: "module_current",
min_sdk_version: "30",
@@ -183,20 +199,26 @@
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
],
- static_libs: [
- "net-utils-device-common-struct",
- ],
libs: [
"androidx.annotation_annotation",
"framework-connectivity.stubs.module_lib",
+ // For libraries which are statically linked in framework-connectivity, do not
+ // statically link here because callers of this library might already have a static
+ // version linked.
+ "net-utils-device-common-struct",
],
apex_available: [
"com.android.tethering",
"//apex_available:platform",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
+// The net-utils-device-common-ip library requires the callers to contain
+// net-utils-device-common-struct.
java_library {
// TODO : this target should probably be folded into net-utils-device-common
name: "net-utils-device-common-ip",
@@ -223,7 +245,10 @@
"com.android.tethering",
"//apex_available:platform",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_library {
@@ -258,7 +283,10 @@
"//packages/modules/Wifi/framework/tests:__subpackages__",
"//packages/apps/Settings",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
errorprone: {
enabled: true,
// Error-prone checking only warns of problems when building. To make the build fail with
@@ -301,7 +329,10 @@
"//packages/modules/Bluetooth/android/app",
"//packages/modules/Wifi/service:__subpackages__",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_library {
@@ -323,7 +354,10 @@
"com.android.tethering",
"//apex_available:platform",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
java_library {
@@ -346,7 +380,10 @@
"com.android.tethering",
"//apex_available:platform",
],
- lint: { strict_updatability_linting: true },
+ lint: {
+ strict_updatability_linting: true,
+ error_checks: ["NewApi"],
+ },
}
// Limited set of utilities for use by service-connectivity-mdns-standalone-build-test, to make sure
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 98fda56..1d8b4eb 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -28,6 +28,7 @@
import android.net.InterfaceConfigurationParcel;
import android.net.IpPrefix;
import android.net.RouteInfo;
+import android.net.RouteInfoParcel;
import android.net.TetherConfigParcel;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -159,9 +160,11 @@
throws RemoteException, ServiceSpecificException {
netd.tetherInterfaceAdd(iface);
networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
- List<RouteInfo> routes = new ArrayList<>();
- routes.add(new RouteInfo(dest, null, iface, RTN_UNICAST));
- addRoutesToLocalNetwork(netd, iface, routes);
+ // Activate a route to dest and IPv6 link local.
+ modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ new RouteInfo(dest, null, iface, RTN_UNICAST));
+ modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
}
/**
@@ -255,7 +258,7 @@
}
/** Add or remove |route|. */
- public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
+ private static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
final RouteInfo route) {
final String ifName = route.getInterface();
final String dst = route.getDestination().toString();
@@ -276,4 +279,38 @@
throw new IllegalStateException(e);
}
}
+
+ /**
+ * Convert a RouteInfo into a RouteInfoParcel.
+ */
+ public static RouteInfoParcel toRouteInfoParcel(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;
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/BpfBitmap.java b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
index d2a5b65..acb3ca5 100644
--- a/staticlibs/device/com/android/net/module/util/BpfBitmap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfBitmap.java
@@ -16,9 +16,11 @@
package com.android.net.module.util;
+import android.os.Build;
import android.system.ErrnoException;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
/**
*
@@ -26,6 +28,7 @@
* array type with key->int and value->uint64_t defined in the bpf program.
*
*/
+@RequiresApi(Build.VERSION_CODES.S)
public class BpfBitmap {
private BpfMap<Struct.S32, Struct.S64> mBpfMap;
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index d45cace..e3ef0f0 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -18,12 +18,14 @@
import static android.system.OsConstants.EEXIST;
import static android.system.OsConstants.ENOENT;
+import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.system.ErrnoException;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -40,6 +42,7 @@
* @param <K> the key of the map.
* @param <V> the value of the map.
*/
+@RequiresApi(Build.VERSION_CODES.S)
public class BpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
static {
System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage()));
@@ -187,12 +190,6 @@
return nativeDeleteMapEntry(mMapFd.getFd(), key.writeToBytes());
}
- /** Returns {@code true} if this map contains no elements. */
- @Override
- public boolean isEmpty() throws ErrnoException {
- return getFirstKey() == null;
- }
-
private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
byte[] rawKey = new byte[mKeySize];
@@ -245,47 +242,9 @@
return Struct.parse(mValueClass, buffer);
}
- /**
- * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
- * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
- * other structural modifications to the map, such as adding entries or deleting other entries.
- * Otherwise, iteration will result in undefined behaviour.
- */
- @Override
- public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
- @Nullable K nextKey = getFirstKey();
-
- while (nextKey != null) {
- @NonNull final K curKey = nextKey;
- @NonNull final V value = getValue(curKey);
-
- nextKey = getNextKey(curKey);
- action.accept(curKey, value);
- }
- }
-
- /* Empty implementation to implement AutoCloseable, so we can use BpfMaps
- * with try with resources, but due to persistent FD cache, there is no actual
- * need to close anything. File descriptors will actually be closed when we
- * unlock the BpfMap class and destroy the ParcelFileDescriptor objects.
- */
- @Override
- public void close() throws IOException {
- }
-
- /**
- * Clears the map. The map may already be empty.
- *
- * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
- * or if a non-ENOENT error occurred when deleting a key.
- */
- @Override
- public void clear() throws ErrnoException {
- K key = getFirstKey();
- while (key != null) {
- deleteEntry(key); // ignores ENOENT.
- key = getFirstKey();
- }
+ /** Synchronize Kernel RCU */
+ public static void synchronizeKernelRCU() throws ErrnoException {
+ nativeSynchronizeKernelRCU();
}
private static native int nativeBpfFdGet(String path, int mode, int keySize, int valueSize)
@@ -309,4 +268,6 @@
private native boolean nativeFindMapEntry(int fd, byte[] key, byte[] value)
throws ErrnoException;
+
+ private static native void nativeSynchronizeKernelRCU() throws ErrnoException;
}
diff --git a/staticlibs/device/com/android/net/module/util/BpfUtils.java b/staticlibs/device/com/android/net/module/util/BpfUtils.java
index 94af11b..cdd6fd7 100644
--- a/staticlibs/device/com/android/net/module/util/BpfUtils.java
+++ b/staticlibs/device/com/android/net/module/util/BpfUtils.java
@@ -15,7 +15,10 @@
*/
package com.android.net.module.util;
+import android.os.Build;
+
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import java.io.IOException;
@@ -24,6 +27,7 @@
*
* {@hide}
*/
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class BpfUtils {
static {
System.loadLibrary(JniUtil.getJniLibraryName(BpfUtils.class.getPackage()));
@@ -32,38 +36,27 @@
// Defined in include/uapi/linux/bpf.h. Only adding the CGROUPS currently being used for now.
public static final int BPF_CGROUP_INET_INGRESS = 0;
public static final int BPF_CGROUP_INET_EGRESS = 1;
+ public static final int BPF_CGROUP_INET_SOCK_CREATE = 2;
public static final int BPF_CGROUP_INET4_BIND = 8;
public static final int BPF_CGROUP_INET6_BIND = 9;
+ // Note: This is only guaranteed to be accurate on U+ devices. It is likely to be accurate
+ // on T+ devices as well, but this is not guaranteed.
+ public static final String CGROUP_PATH = "/sys/fs/cgroup/";
/**
- * Attach BPF program to CGROUP
+ * Get BPF program Id from CGROUP.
+ *
+ * Note: This requires a 4.19 kernel which is only guaranteed on V+.
+ *
+ * @param attachType Bpf attach type. See bpf_attach_type in include/uapi/linux/bpf.h.
+ * @return Positive integer for a Program Id. 0 if no program is attached.
+ * @throws IOException if failed to open the cgroup directory or query bpf program.
*/
- public static void attachProgram(int type, @NonNull String programPath,
- @NonNull String cgroupPath, int flags) throws IOException {
- native_attachProgramToCgroup(type, programPath, cgroupPath, flags);
+ public static int getProgramId(int attachType) throws IOException {
+ return native_getProgramIdFromCgroup(attachType, CGROUP_PATH);
}
- /**
- * Detach BPF program from CGROUP
- */
- public static void detachProgram(int type, @NonNull String cgroupPath)
- throws IOException {
- native_detachProgramFromCgroup(type, cgroupPath);
- }
-
- /**
- * Detach single BPF program from CGROUP
- */
- public static void detachSingleProgram(int type, @NonNull String programPath,
- @NonNull String cgroupPath) throws IOException {
- native_detachSingleProgramFromCgroup(type, programPath, cgroupPath);
- }
-
- private static native boolean native_attachProgramToCgroup(int type, String programPath,
- String cgroupPath, int flags) throws IOException;
- private static native boolean native_detachProgramFromCgroup(int type, String cgroupPath)
+ private static native int native_getProgramIdFromCgroup(int type, String cgroupPath)
throws IOException;
- private static native boolean native_detachSingleProgramFromCgroup(int type,
- String programPath, String cgroupPath) throws IOException;
}
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 5b75699..42f26f4 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -64,6 +64,9 @@
@VisibleForTesting
public static final long DEFAULT_PACKAGE_VERSION = 1000;
+ private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
+ private static final String CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE = "com.android.net.flags";
+
@VisibleForTesting
public static void resetPackageVersionCacheForTest() {
sPackageVersion = -1;
@@ -165,32 +168,18 @@
*
* This is useful to ensure that if a module install is rolled back, flags are not left fully
* rolled out on a version where they have not been well tested.
+ *
+ * If the feature is disabled by default and enabled by flag push, this method should be used.
+ * If the feature is enabled by default and disabled by flag push (kill switch),
+ * {@link #isNetworkStackFeatureNotChickenedOut(Context, String)} should be used.
+ *
* @param context The global context information about an app environment.
* @param name The name of the property to look up.
* @return true if this feature is enabled, or false if disabled.
*/
public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
@NonNull String name) {
- return isNetworkStackFeatureEnabled(context, name, false /* defaultEnabled */);
- }
-
- /**
- * Check whether or not one specific experimental feature for a particular namespace from
- * {@link DeviceConfig} is enabled by comparing module package version
- * with current version of property. If this property version is valid, the corresponding
- * experimental feature would be enabled, otherwise disabled.
- *
- * This is useful to ensure that if a module install is rolled back, flags are not left fully
- * rolled out on a version where they have not been well tested.
- * @param context The global context information about an app environment.
- * @param name The name of the property to look up.
- * @param defaultEnabled The value to return if the property does not exist or its value is
- * null.
- * @return true if this feature is enabled, or false if disabled.
- */
- public static boolean isNetworkStackFeatureEnabled(@NonNull Context context,
- @NonNull String name, boolean defaultEnabled) {
- return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, defaultEnabled,
+ return isFeatureEnabled(NAMESPACE_CONNECTIVITY, name, false /* defaultEnabled */,
() -> getPackageVersion(context));
}
@@ -420,4 +409,31 @@
return pkgs.get(0).activityInfo.applicationInfo.packageName;
}
+
+ /**
+ * Check whether one specific trunk stable flag in android_core_networking namespace is enabled.
+ * This method reads trunk stable feature flag value from DeviceConfig directly since
+ * java_aconfig_library soong module is not available in the mainline branch.
+ * After the mainline branch support the aconfig soong module, this function must be removed and
+ * java_aconfig_library must be used instead to check if the feature is enabled.
+ *
+ * @param flagName The name of the trunk stable flag
+ * @return true if this feature is enabled, or false if disabled.
+ */
+ public static boolean isTrunkStableFeatureEnabled(final String flagName) {
+ return isTrunkStableFeatureEnabled(
+ CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+ CORE_NETWORKING_TRUNK_STABLE_FLAG_PACKAGE,
+ flagName
+ );
+ }
+
+ private static boolean isTrunkStableFeatureEnabled(final String namespace,
+ final String packageName, final String flagName) {
+ return DeviceConfig.getBoolean(
+ namespace,
+ packageName + "." + flagName,
+ false /* defaultValue */
+ );
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/FeatureVersions.java b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
index 149756c..d5f8124 100644
--- a/staticlibs/device/com/android/net/module/util/FeatureVersions.java
+++ b/staticlibs/device/com/android/net/module/util/FeatureVersions.java
@@ -42,4 +42,10 @@
// M-2023-Sept on July 3rd, 2023.
public static final long FEATURE_CLAT_ADDRESS_TRANSLATE =
NETWORK_STACK_MODULE_ID + 34_09_00_000L;
+
+ // IS_UID_NETWORKING_BLOCKED is a feature in ConnectivityManager,
+ // which provides an API to access BPF maps to check whether the networking is blocked
+ // by BPF for the given uid and conditions, introduced in version M-2024-Feb on Nov 6, 2023.
+ public static final long FEATURE_IS_UID_NETWORKING_BLOCKED =
+ CONNECTIVITY_MODULE_ID + 34_14_00_000L;
}
diff --git a/staticlibs/device/com/android/net/module/util/IBpfMap.java b/staticlibs/device/com/android/net/module/util/IBpfMap.java
index 83ff875..ca56830 100644
--- a/staticlibs/device/com/android/net/module/util/IBpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/IBpfMap.java
@@ -18,6 +18,7 @@
import android.system.ErrnoException;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.NoSuchElementException;
@@ -49,15 +50,17 @@
/** Remove existing key from eBpf map. Return true if something was deleted. */
boolean deleteEntry(K key) throws ErrnoException;
- /** Returns {@code true} if this map contains no elements. */
- boolean isEmpty() throws ErrnoException;
-
/** Get the key after the passed-in key. */
K getNextKey(@NonNull K key) throws ErrnoException;
/** Get the first key of the eBpf map. */
K getFirstKey() throws ErrnoException;
+ /** Returns {@code true} if this map contains no elements. */
+ default boolean isEmpty() throws ErrnoException {
+ return getFirstKey() == null;
+ }
+
/** Check whether a key exists in the map. */
boolean containsKey(@NonNull K key) throws ErrnoException;
@@ -70,13 +73,38 @@
/**
* Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+ * other structural modifications to the map, such as adding entries or deleting other entries.
+ * Otherwise, iteration will result in undefined behaviour.
*/
- void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException;
+ default public void forEach(ThrowingBiConsumer<K, V> action) throws ErrnoException {
+ @Nullable K nextKey = getFirstKey();
- /** Clears the map. */
- void clear() throws ErrnoException;
+ while (nextKey != null) {
+ @NonNull final K curKey = nextKey;
+ @NonNull final V value = getValue(curKey);
+
+ nextKey = getNextKey(curKey);
+ action.accept(curKey, value);
+ }
+ }
+
+ /**
+ * Clears the map. The map may already be empty.
+ *
+ * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+ * or if a non-ENOENT error occurred when deleting a key.
+ */
+ default public void clear() throws ErrnoException {
+ K key = getFirstKey();
+ while (key != null) {
+ deleteEntry(key); // ignores ENOENT.
+ key = getFirstKey();
+ }
+ }
/** Close for AutoCloseable. */
@Override
- void close() throws IOException;
+ default void close() throws IOException {
+ };
}
diff --git a/staticlibs/device/com/android/net/module/util/SocketUtils.java b/staticlibs/device/com/android/net/module/util/SocketUtils.java
index 9878ea5..5e6a6c6 100644
--- a/staticlibs/device/com/android/net/module/util/SocketUtils.java
+++ b/staticlibs/device/com/android/net/module/util/SocketUtils.java
@@ -19,6 +19,8 @@
import static android.net.util.SocketUtils.closeSocket;
import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.os.Build;
import android.system.NetlinkSocketAddress;
import java.io.FileDescriptor;
@@ -39,7 +41,7 @@
/**
* Make a socket address to communicate with netlink.
*/
- @NonNull
+ @NonNull @RequiresApi(Build.VERSION_CODES.S)
public static SocketAddress makeNetlinkSocketAddress(int portId, int groupsMask) {
return new NetlinkSocketAddress(portId, groupsMask);
}
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index b638a46..ff7a711 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -146,6 +146,14 @@
int arraysize() default 0;
}
+ /**
+ * Indicates that this field contains a computed value and is ignored for the purposes of Struct
+ * parsing.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Computed {}
+
private static class FieldInfo {
@NonNull
public final Field annotation;
@@ -414,7 +422,14 @@
final byte[] address = new byte[isIpv6 ? 16 : 4];
buf.get(address);
try {
- value = InetAddress.getByAddress(address);
+ if (isIpv6) {
+ // Using Inet6Address.getByAddress since InetAddress.getByAddress converts
+ // v4-mapped v6 address to v4 address internally and returns Inet4Address.
+ value = Inet6Address.getByAddress(
+ null /* host */, address, -1 /* scope_id */);
+ } else {
+ value = InetAddress.getByAddress(address);
+ }
} catch (UnknownHostException e) {
throw new IllegalArgumentException("illegal length of IP address", e);
}
@@ -533,6 +548,7 @@
final FieldInfo[] annotationFields = new FieldInfo[getAnnotationFieldCount(clazz)];
for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) continue;
+ if (field.getAnnotation(Computed.class) != null) continue;
final Field annotation = field.getAnnotation(Field.class);
if (annotation == null) {
diff --git a/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
index f882483..15a4633 100644
--- a/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
+++ b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
@@ -109,7 +109,7 @@
}
}
Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
- NetlinkUtils.connectSocketToNetlink(fd);
+ NetlinkUtils.connectToKernel(fd);
if (DBG) {
final SocketAddress nlAddr = Os.getsockname(fd);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index f8b4716..4f76577 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -33,7 +33,7 @@
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import static com.android.net.module.util.netlink.NetlinkUtils.TCP_ALIVE_STATE_FILTER;
-import static com.android.net.module.util.netlink.NetlinkUtils.connectSocketToNetlink;
+import static com.android.net.module.util.netlink.NetlinkUtils.connectToKernel;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -266,7 +266,7 @@
FileDescriptor fd = null;
try {
fd = NetlinkUtils.netlinkSocketForProto(NETLINK_INET_DIAG);
- NetlinkUtils.connectSocketToNetlink(fd);
+ connectToKernel(fd);
uid = lookupUid(protocol, local, remote, fd);
} catch (ErrnoException | SocketException | IllegalArgumentException
| InterruptedIOException e) {
@@ -426,8 +426,8 @@
try {
dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
- connectSocketToNetlink(dumpFd);
- connectSocketToNetlink(destroyFd);
+ connectToKernel(dumpFd);
+ connectToKernel(destroyFd);
for (int family : List.of(AF_INET, AF_INET6)) {
try {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 33bd36d..f1f30d3 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -153,7 +153,7 @@
final FileDescriptor fd = netlinkSocketForProto(nlProto);
try {
- connectSocketToNetlink(fd);
+ connectToKernel(fd);
sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT_MS);
receiveNetlinkAck(fd);
} catch (InterruptedIOException e) {
@@ -244,7 +244,7 @@
* @throws ErrnoException if the {@code fd} could not connect to kernel successfully
* @throws SocketException if there is an error accessing a socket.
*/
- public static void connectSocketToNetlink(FileDescriptor fd)
+ public static void connectToKernel(@NonNull FileDescriptor fd)
throws ErrnoException, SocketException {
Os.connect(fd, makeNetlinkSocketAddress(0, 0));
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS b/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS
new file mode 100644
index 0000000..97b4da0
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 1364804
+
+per-file **Xfrm* = file:platform/frameworks/base:main:/services/core/java/com/android/server/vcn/OWNERS
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java
new file mode 100644
index 0000000..cef1f56
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmAddressT.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Struct xfrm_address_t
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * typedef union {
+ * __be32 a4;
+ * __be32 a6[4];
+ * struct in6_addr in6;
+ * } xfrm_address_t;
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmAddressT extends Struct {
+ public static final int STRUCT_SIZE = 16;
+
+ @Field(order = 0, type = Type.ByteArray, arraysize = STRUCT_SIZE)
+ public final byte[] address;
+
+ // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+ public StructXfrmAddressT(@NonNull final byte[] address) {
+ this.address = address.clone();
+ }
+
+ // Constructor to build a new message
+ public StructXfrmAddressT(@NonNull final InetAddress inetAddress) {
+ this.address = new byte[STRUCT_SIZE];
+ final byte[] addressBytes = inetAddress.getAddress();
+ System.arraycopy(addressBytes, 0, address, 0, addressBytes.length);
+ }
+
+ /** Return the address in InetAddress */
+ public InetAddress getAddress(int family) {
+ final byte[] addressBytes;
+ if (family == OsConstants.AF_INET6) {
+ addressBytes = this.address;
+ } else if (family == OsConstants.AF_INET) {
+ addressBytes = new byte[IPV4_ADDR_LEN];
+ System.arraycopy(this.address, 0, addressBytes, 0, addressBytes.length);
+ } else {
+ throw new IllegalArgumentException("Invalid IP family " + family);
+ }
+
+ try {
+ return InetAddress.getByAddress(addressBytes);
+ } catch (UnknownHostException e) {
+ // This should never happen
+ throw new IllegalArgumentException(
+ "Illegal length of IP address " + addressBytes.length, e);
+ }
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java
new file mode 100644
index 0000000..5ebc69c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaId.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+
+/**
+ * Struct xfrm_usersa_id
+ *
+ * <p>see include/uapi/linux/xfrm.h
+ *
+ * <pre>
+ * struct xfrm_usersa_id {
+ * xfrm_address_t daddr;
+ * __be32 spi;
+ * __u16 family;
+ * __u8 proto;
+ * };
+ * </pre>
+ *
+ * @hide
+ */
+public class StructXfrmUsersaId extends Struct {
+ public static final int STRUCT_SIZE = 24;
+
+ @Field(order = 0, type = Type.ByteArray, arraysize = 16)
+ public final byte[] nestedStructDAddr; // xfrm_address_t
+
+ @Field(order = 1, type = Type.UBE32)
+ public final long spi;
+
+ @Field(order = 2, type = Type.U16)
+ public final int family;
+
+ @Field(order = 3, type = Type.U8, padding = 1)
+ public final short proto;
+
+ @Computed private final StructXfrmAddressT mDestXfrmAddressT;
+
+ // Constructor that allows Strutc.parse(Class<T>, ByteBuffer) to work
+ public StructXfrmUsersaId(
+ @NonNull final byte[] nestedStructDAddr, long spi, int family, short proto) {
+ this.nestedStructDAddr = nestedStructDAddr.clone();
+ this.spi = spi;
+ this.family = family;
+ this.proto = proto;
+
+ mDestXfrmAddressT = new StructXfrmAddressT(this.nestedStructDAddr);
+ }
+
+ // Constructor to build a new message
+ public StructXfrmUsersaId(
+ @NonNull final InetAddress destAddress, long spi, int family, short proto) {
+ this(new StructXfrmAddressT(destAddress).writeToBytes(), spi, family, proto);
+ }
+
+ /** Return the destination address */
+ public InetAddress getDestAddress() {
+ return mDestXfrmAddressT.getAddress(family);
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java
new file mode 100644
index 0000000..ee34e57
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/xfrm/XfrmNetlinkMessage.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.StructNlMsgHdr;
+
+/** Base calss for XFRM netlink messages */
+// Developer notes: The Linux kernel includes a number of XFRM structs that are not standard netlink
+// attributes (e.g., xfrm_usersa_id). These structs are unlikely to change size, so this XFRM
+// netlink message implementation assumes their sizes will remain stable. If any non-attribute
+// struct size changes, it should be caught by CTS and then developers should add
+// kernel-version-based behvaiours.
+public abstract class XfrmNetlinkMessage extends NetlinkMessage {
+ // TODO: STOPSHIP: b/308011229 Remove it when OsConstants.IPPROTO_ESP is exposed
+ public static final int IPPROTO_ESP = 50;
+
+ public XfrmNetlinkMessage(@NonNull final StructNlMsgHdr header) {
+ super(header);
+ }
+
+ // TODO: Add the support for parsing messages
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
index 59d655c..b0f19e2 100644
--- a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -18,10 +18,17 @@
import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+import android.net.IpPrefix;
+import android.util.Log;
+
import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Computed;
import com.android.net.module.util.Struct.Field;
import com.android.net.module.util.Struct.Type;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -52,6 +59,7 @@
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
public class IaPrefixOption extends Struct {
+ private static final String TAG = IaPrefixOption.class.getSimpleName();
public static final int LENGTH = 25; // option length excluding IAprefix-options
@Field(order = 0, type = Type.S16)
@@ -67,7 +75,11 @@
@Field(order = 5, type = Type.ByteArray, arraysize = 16)
public final byte[] prefix;
- public IaPrefixOption(final short code, final short length, final long preferred,
+ @Computed
+ private final IpPrefix mIpPrefix;
+
+ // Constructor used by Struct.parse()
+ protected IaPrefixOption(final short code, final short length, final long preferred,
final long valid, final byte prefixLen, final byte[] prefix) {
this.code = code;
this.length = length;
@@ -75,6 +87,57 @@
this.valid = valid;
this.prefixLen = prefixLen;
this.prefix = prefix.clone();
+
+ try {
+ final Inet6Address addr = (Inet6Address) InetAddress.getByAddress(prefix);
+ mIpPrefix = new IpPrefix(addr, prefixLen);
+ } catch (UnknownHostException | ClassCastException e) {
+ // UnknownHostException should never happen unless prefix is null.
+ // ClassCastException can occur when prefix is an IPv6 mapped IPv4 address.
+ // Both scenarios should throw an exception in the context of Struct#parse().
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public IaPrefixOption(final short length, final long preferred, final long valid,
+ final byte prefixLen, final byte[] prefix) {
+ this((byte) DHCP6_OPTION_IAPREFIX, length, preferred, valid, prefixLen, prefix);
+ }
+
+ /**
+ * Check whether or not IA Prefix option in IA_PD option is valid per RFC8415#section-21.22.
+ *
+ * Note: an expired prefix can still be valid.
+ */
+ public boolean isValid() {
+ if (preferred < 0) {
+ Log.w(TAG, "Invalid preferred lifetime: " + this);
+ return false;
+ }
+ if (valid < 0) {
+ Log.w(TAG, "Invalid valid lifetime: " + this);
+ return false;
+ }
+ if (preferred > valid) {
+ Log.w(TAG, "Invalid lifetime. Preferred lifetime > valid lifetime: " + this);
+ return false;
+ }
+ if (prefixLen > 64) {
+ Log.w(TAG, "Invalid prefix length: " + this);
+ return false;
+ }
+ return true;
+ }
+
+ public IpPrefix getIpPrefix() {
+ return mIpPrefix;
+ }
+
+ /**
+ * Check whether or not IA Prefix option has 0 preferred and valid lifetimes.
+ */
+ public boolean withZeroLifetimes() {
+ return preferred == 0 && valid == 0;
}
/**
@@ -82,8 +145,14 @@
*/
public static ByteBuffer build(final short length, final long preferred, final long valid,
final byte prefixLen, final byte[] prefix) {
- final IaPrefixOption option = new IaPrefixOption((byte) DHCP6_OPTION_IAPREFIX,
+ final IaPrefixOption option = new IaPrefixOption(
length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix);
return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
}
+
+ @Override
+ public String toString() {
+ return "IA Prefix, length " + length + ": " + mIpPrefix + ", pref " + preferred + ", valid "
+ + valid;
+ }
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacket.java b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
index 0dcdf1e..63106a1 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacket.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacket.java
@@ -56,6 +56,7 @@
*/
// TODO: Define the constant as a public constant in DnsResolver since it can never change.
private static final int TYPE_CNAME = 5;
+ public static final int TYPE_SVCB = 64;
/**
* Thrown when parsing packet failed.
@@ -282,7 +283,7 @@
* @param buf ByteBuffer input of record, must be in network byte order
* (which is the default).
*/
- private DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
+ protected DnsRecord(@RecordType int rType, @NonNull ByteBuffer buf)
throws BufferUnderflowException, ParseException {
Objects.requireNonNull(buf);
this.rType = rType;
@@ -326,6 +327,8 @@
// Return a DnsRecord instance by default for backward compatibility, this is useful
// when a partner supports new type of DnsRecord but does not inherit DnsRecord.
switch (nsType) {
+ case TYPE_SVCB:
+ return new DnsSvcbRecord(rType, buf);
default:
return new DnsRecord(rType, buf);
}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
new file mode 100644
index 0000000..d298599
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbPacket.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A class for a DNS SVCB response packet.
+ *
+ * @hide
+ */
+public class DnsSvcbPacket extends DnsPacket {
+ public static final int TYPE_SVCB = 64;
+
+ private static final String TAG = DnsSvcbPacket.class.getSimpleName();
+
+ /**
+ * Creates a DnsSvcbPacket object from the given wire-format DNS packet.
+ */
+ private DnsSvcbPacket(@NonNull byte[] data) throws DnsPacket.ParseException {
+ // If data is null, ParseException will be thrown.
+ super(data);
+
+ final int questions = mHeader.getRecordCount(QDSECTION);
+ if (questions != 1) {
+ throw new DnsPacket.ParseException("Unexpected question count " + questions);
+ }
+ final int nsType = mRecords[QDSECTION].get(0).nsType;
+ if (nsType != TYPE_SVCB) {
+ throw new DnsPacket.ParseException("Unexpected query type " + nsType);
+ }
+ }
+
+ /**
+ * Returns true if the DnsSvcbPacket is a DNS response.
+ */
+ public boolean isResponse() {
+ return mHeader.isResponse();
+ }
+
+ /**
+ * Returns whether the given protocol alpn is supported.
+ */
+ public boolean isSupported(@NonNull String alpn) {
+ return findSvcbRecord(alpn) != null;
+ }
+
+ /**
+ * Returns the TargetName associated with the given protocol alpn.
+ * If the alpn is not supported, a null is returned.
+ */
+ @Nullable
+ public String getTargetName(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ return (record != null) ? record.getTargetName() : null;
+ }
+
+ /**
+ * Returns the TargetName that associated with the given protocol alpn.
+ * If the alpn is not supported, -1 is returned.
+ */
+ public int getPort(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ return (record != null) ? record.getPort() : -1;
+ }
+
+ /**
+ * Returns the IP addresses that support the given protocol alpn.
+ * If the alpn is not supported, an empty list is returned.
+ */
+ @NonNull
+ public List<InetAddress> getAddresses(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ if (record == null) return Collections.EMPTY_LIST;
+
+ // As per draft-ietf-dnsop-svcb-https-10#section-7.4 and draft-ietf-add-ddr-10#section-4,
+ // if A/AAAA records are available in the Additional section, use the IP addresses in
+ // those records instead of the IP addresses in ipv4hint/ipv6hint.
+ final List<InetAddress> out = getAddressesFromAdditionalSection();
+ if (out.size() > 0) return out;
+
+ return record.getAddresses();
+ }
+
+ /**
+ * Returns the value of SVCB key dohpath that associated with the given protocol alpn.
+ * If the alpn is not supported, a null is returned.
+ */
+ @Nullable
+ public String getDohPath(@NonNull String alpn) {
+ final DnsSvcbRecord record = findSvcbRecord(alpn);
+ return (record != null) ? record.getDohPath() : null;
+ }
+
+ /**
+ * Returns the DnsSvcbRecord associated with the given protocol alpn.
+ * If the alpn is not supported, a null is returned.
+ */
+ @Nullable
+ private DnsSvcbRecord findSvcbRecord(@NonNull String alpn) {
+ for (final DnsRecord record : mRecords[ANSECTION]) {
+ if (record instanceof DnsSvcbRecord) {
+ final DnsSvcbRecord svcbRecord = (DnsSvcbRecord) record;
+ if (svcbRecord.getAlpns().contains(alpn)) {
+ return svcbRecord;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the IP addresses in additional section.
+ */
+ @NonNull
+ private List<InetAddress> getAddressesFromAdditionalSection() {
+ final List<InetAddress> out = new ArrayList<InetAddress>();
+ if (mHeader.getRecordCount(ARSECTION) == 0) {
+ return out;
+ }
+ for (final DnsRecord record : mRecords[ARSECTION]) {
+ if (record.nsType != TYPE_A && record.nsType != TYPE_AAAA) {
+ Log.d(TAG, "Found type other than A/AAAA in Additional section: " + record.nsType);
+ continue;
+ }
+ try {
+ out.add(InetAddress.getByAddress(record.getRR()));
+ } catch (UnknownHostException e) {
+ Log.w(TAG, "Failed to parse address");
+ }
+ }
+ return out;
+ }
+
+ /**
+ * Creates a DnsSvcbPacket object from the given wire-format DNS answer.
+ */
+ public static DnsSvcbPacket fromResponse(@NonNull byte[] data) throws DnsPacket.ParseException {
+ DnsSvcbPacket out = new DnsSvcbPacket(data);
+ if (!out.isResponse()) {
+ throw new DnsPacket.ParseException("Not an answer packet");
+ }
+ return out;
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
new file mode 100644
index 0000000..935cdf6
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/DnsSvcbRecord.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
+import static com.android.net.module.util.DnsPacket.ParseException;
+
+import android.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * A class for an SVCB record.
+ * https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public final class DnsSvcbRecord extends DnsPacket.DnsRecord {
+ /**
+ * The following SvcParamKeys KEY_* are defined in
+ * https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml.
+ */
+
+ // The SvcParamKey "mandatory". The associated implementation of SvcParam is SvcParamMandatory.
+ private static final int KEY_MANDATORY = 0;
+
+ // The SvcParamKey "alpn". The associated implementation of SvcParam is SvcParamAlpn.
+ private static final int KEY_ALPN = 1;
+
+ // The SvcParamKey "no-default-alpn". The associated implementation of SvcParam is
+ // SvcParamNoDefaultAlpn.
+ private static final int KEY_NO_DEFAULT_ALPN = 2;
+
+ // The SvcParamKey "port". The associated implementation of SvcParam is SvcParamPort.
+ private static final int KEY_PORT = 3;
+
+ // The SvcParamKey "ipv4hint". The associated implementation of SvcParam is SvcParamIpv4Hint.
+ private static final int KEY_IPV4HINT = 4;
+
+ // The SvcParamKey "ech". The associated implementation of SvcParam is SvcParamEch.
+ private static final int KEY_ECH = 5;
+
+ // The SvcParamKey "ipv6hint". The associated implementation of SvcParam is SvcParamIpv6Hint.
+ private static final int KEY_IPV6HINT = 6;
+
+ // The SvcParamKey "dohpath". The associated implementation of SvcParam is SvcParamDohPath.
+ private static final int KEY_DOHPATH = 7;
+
+ // The minimal size of a SvcParam.
+ // https://www.ietf.org/archive/id/draft-ietf-dnsop-svcb-https-12.html#name-rdata-wire-format
+ private static final int MINSVCPARAMSIZE = 4;
+
+ private static final String TAG = DnsSvcbRecord.class.getSimpleName();
+
+ private final int mSvcPriority;
+
+ @NonNull
+ private final String mTargetName;
+
+ @NonNull
+ private final SparseArray<SvcParam> mAllSvcParams = new SparseArray<>();
+
+ @VisibleForTesting(visibility = PACKAGE)
+ public DnsSvcbRecord(@DnsPacket.RecordType int rType, @NonNull ByteBuffer buff)
+ throws IllegalStateException, ParseException {
+ super(rType, buff);
+ if (nsType != DnsPacket.TYPE_SVCB) {
+ throw new IllegalStateException("incorrect nsType: " + nsType);
+ }
+ if (nsClass != CLASS_IN) {
+ throw new ParseException("incorrect nsClass: " + nsClass);
+ }
+
+ // DNS Record in Question Section doesn't have Rdata.
+ if (rType == DnsPacket.QDSECTION) {
+ mSvcPriority = 0;
+ mTargetName = "";
+ return;
+ }
+
+ final byte[] rdata = getRR();
+ if (rdata == null) {
+ throw new ParseException("SVCB rdata is empty");
+ }
+
+ final ByteBuffer buf = ByteBuffer.wrap(rdata).asReadOnlyBuffer();
+ mSvcPriority = Short.toUnsignedInt(buf.getShort());
+ mTargetName = DnsPacketUtils.DnsRecordParser.parseName(buf, 0 /* Parse depth */,
+ false /* isNameCompressionSupported */);
+
+ if (mTargetName.length() > DnsPacket.DnsRecord.MAXNAMESIZE) {
+ throw new ParseException(
+ "Failed to parse SVCB target name, name size is too long: "
+ + mTargetName.length());
+ }
+ while (buf.remaining() >= MINSVCPARAMSIZE) {
+ final SvcParam svcParam = parseSvcParam(buf);
+ final int key = svcParam.getKey();
+ if (mAllSvcParams.get(key) != null) {
+ throw new ParseException("Invalid DnsSvcbRecord, key " + key + " is repeated");
+ }
+ mAllSvcParams.put(key, svcParam);
+ }
+ if (buf.hasRemaining()) {
+ throw new ParseException("Invalid DnsSvcbRecord. Got "
+ + buf.remaining() + " remaining bytes after parsing");
+ }
+ }
+
+ /**
+ * Returns the TargetName.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public String getTargetName() {
+ return mTargetName;
+ }
+
+ /**
+ * Returns an unmodifiable list of alpns from SvcParam alpn.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public List<String> getAlpns() {
+ final SvcParamAlpn sp = (SvcParamAlpn) mAllSvcParams.get(KEY_ALPN);
+ final List<String> list = (sp != null) ? sp.getValue() : Collections.EMPTY_LIST;
+ return Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Returns the port number from SvcParam port.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ public int getPort() {
+ final SvcParamPort sp = (SvcParamPort) mAllSvcParams.get(KEY_PORT);
+ return (sp != null) ? sp.getValue() : -1;
+ }
+
+ /**
+ * Returns a list of the IP addresses from both of SvcParam ipv4hint and ipv6hint.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public List<InetAddress> getAddresses() {
+ final List<InetAddress> out = new ArrayList<>();
+ final SvcParamIpHint sp4 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV4HINT);
+ if (sp4 != null) {
+ out.addAll(sp4.getValue());
+ }
+ final SvcParamIpHint sp6 = (SvcParamIpHint) mAllSvcParams.get(KEY_IPV6HINT);
+ if (sp6 != null) {
+ out.addAll(sp6.getValue());
+ }
+ return out;
+ }
+
+ /**
+ * Returns the doh path from SvcParam dohPath.
+ */
+ @VisibleForTesting(visibility = PACKAGE)
+ @NonNull
+ public String getDohPath() {
+ final SvcParamDohPath sp = (SvcParamDohPath) mAllSvcParams.get(KEY_DOHPATH);
+ return (sp != null) ? sp.getValue() : "";
+ }
+
+ @Override
+ public String toString() {
+ if (rType == DnsPacket.QDSECTION) {
+ return dName + " IN SVCB";
+ }
+
+ final StringJoiner sj = new StringJoiner(" ");
+ for (int i = 0; i < mAllSvcParams.size(); i++) {
+ sj.add(mAllSvcParams.valueAt(i).toString());
+ }
+ return dName + " " + ttl + " IN SVCB " + mSvcPriority + " " + mTargetName + " "
+ + sj.toString();
+ }
+
+ private static SvcParam parseSvcParam(@NonNull ByteBuffer buf) throws ParseException {
+ try {
+ final int key = Short.toUnsignedInt(buf.getShort());
+ switch (key) {
+ case KEY_MANDATORY: return new SvcParamMandatory(buf);
+ case KEY_ALPN: return new SvcParamAlpn(buf);
+ case KEY_NO_DEFAULT_ALPN: return new SvcParamNoDefaultAlpn(buf);
+ case KEY_PORT: return new SvcParamPort(buf);
+ case KEY_IPV4HINT: return new SvcParamIpv4Hint(buf);
+ case KEY_ECH: return new SvcParamEch(buf);
+ case KEY_IPV6HINT: return new SvcParamIpv6Hint(buf);
+ case KEY_DOHPATH: return new SvcParamDohPath(buf);
+ default: return new SvcParamGeneric(key, buf);
+ }
+ } catch (BufferUnderflowException e) {
+ throw new ParseException("Malformed packet", e);
+ }
+ }
+
+ /**
+ * The base class for all SvcParam.
+ */
+ private abstract static class SvcParam<T> {
+ private final int mKey;
+
+ SvcParam(int key) {
+ mKey = key;
+ }
+
+ int getKey() {
+ return mKey;
+ }
+
+ abstract T getValue();
+ }
+
+ private static class SvcParamMandatory extends SvcParam<short[]> {
+ private final short[] mValue;
+
+ private SvcParamMandatory(@NonNull ByteBuffer buf) throws BufferUnderflowException,
+ ParseException {
+ super(KEY_MANDATORY);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+ mValue = SvcParamValueUtil.toShortArray(svcParamValue);
+ if (mValue.length == 0) {
+ throw new ParseException("mandatory value must be non-empty");
+ }
+ }
+
+ @Override
+ short[] getValue() {
+ /* Not yet implemented */
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner valueJoiner = new StringJoiner(",");
+ for (short key : mValue) {
+ valueJoiner.add(toKeyName(key));
+ }
+ return toKeyName(getKey()) + "=" + valueJoiner.toString();
+ }
+ }
+
+ private static class SvcParamAlpn extends SvcParam<List<String>> {
+ private final List<String> mValue;
+
+ SvcParamAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_ALPN);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+ mValue = SvcParamValueUtil.toStringList(svcParamValue);
+ if (mValue.isEmpty()) {
+ throw new ParseException("alpn value must be non-empty");
+ }
+ }
+
+ @Override
+ List<String> getValue() {
+ return Collections.unmodifiableList(mValue);
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey()) + "=" + TextUtils.join(",", mValue);
+ }
+ }
+
+ private static class SvcParamNoDefaultAlpn extends SvcParam<Void> {
+ SvcParamNoDefaultAlpn(@NonNull ByteBuffer buf) throws BufferUnderflowException,
+ ParseException {
+ super(KEY_NO_DEFAULT_ALPN);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = buf.getShort();
+ if (len != 0) {
+ throw new ParseException("no-default-alpn value must be empty");
+ }
+ }
+
+ @Override
+ Void getValue() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey());
+ }
+ }
+
+ private static class SvcParamPort extends SvcParam<Integer> {
+ private final int mValue;
+
+ SvcParamPort(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_PORT);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = buf.getShort();
+ if (len != Short.BYTES) {
+ throw new ParseException("key port len is not 2 but " + len);
+ }
+ mValue = Short.toUnsignedInt(buf.getShort());
+ }
+
+ @Override
+ Integer getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey()) + "=" + mValue;
+ }
+ }
+
+ private static class SvcParamIpHint extends SvcParam<List<InetAddress>> {
+ private final List<InetAddress> mValue;
+
+ private SvcParamIpHint(int key, @NonNull ByteBuffer buf, int addrLen) throws
+ BufferUnderflowException, ParseException {
+ super(key);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final ByteBuffer svcParamValue = sliceAndAdvance(buf, len);
+ mValue = SvcParamValueUtil.toInetAddressList(svcParamValue, addrLen);
+ if (mValue.isEmpty()) {
+ throw new ParseException(toKeyName(getKey()) + " value must be non-empty");
+ }
+ }
+
+ @Override
+ List<InetAddress> getValue() {
+ return Collections.unmodifiableList(mValue);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner valueJoiner = new StringJoiner(",");
+ for (InetAddress ip : mValue) {
+ valueJoiner.add(ip.getHostAddress());
+ }
+ return toKeyName(getKey()) + "=" + valueJoiner.toString();
+ }
+ }
+
+ private static class SvcParamIpv4Hint extends SvcParamIpHint {
+ SvcParamIpv4Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_IPV4HINT, buf, NetworkStackConstants.IPV4_ADDR_LEN);
+ }
+ }
+
+ private static class SvcParamIpv6Hint extends SvcParamIpHint {
+ SvcParamIpv6Hint(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_IPV6HINT, buf, NetworkStackConstants.IPV6_ADDR_LEN);
+ }
+ }
+
+ private static class SvcParamEch extends SvcParamGeneric {
+ SvcParamEch(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_ECH, buf);
+ }
+ }
+
+ private static class SvcParamDohPath extends SvcParam<String> {
+ private final String mValue;
+
+ SvcParamDohPath(@NonNull ByteBuffer buf) throws BufferUnderflowException, ParseException {
+ super(KEY_DOHPATH);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ final byte[] value = new byte[len];
+ buf.get(value);
+ mValue = new String(value, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ String getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return toKeyName(getKey()) + "=" + mValue;
+ }
+ }
+
+ // For other unrecognized and unimplemented SvcParams, they are stored as SvcParamGeneric.
+ private static class SvcParamGeneric extends SvcParam<byte[]> {
+ private final byte[] mValue;
+
+ SvcParamGeneric(int key, @NonNull ByteBuffer buf) throws BufferUnderflowException,
+ ParseException {
+ super(key);
+ // The caller already read 2 bytes for SvcParamKey.
+ final int len = Short.toUnsignedInt(buf.getShort());
+ mValue = new byte[len];
+ buf.get(mValue);
+ }
+
+ @Override
+ byte[] getValue() {
+ /* Not yet implemented */
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder out = new StringBuilder();
+ out.append(toKeyName(getKey()));
+ if (mValue != null && mValue.length > 0) {
+ out.append("=");
+ out.append(HexDump.toHexString(mValue));
+ }
+ return out.toString();
+ }
+ }
+
+ private static String toKeyName(int key) {
+ switch (key) {
+ case KEY_MANDATORY: return "mandatory";
+ case KEY_ALPN: return "alpn";
+ case KEY_NO_DEFAULT_ALPN: return "no-default-alpn";
+ case KEY_PORT: return "port";
+ case KEY_IPV4HINT: return "ipv4hint";
+ case KEY_ECH: return "ech";
+ case KEY_IPV6HINT: return "ipv6hint";
+ case KEY_DOHPATH: return "dohpath";
+ default: return "key" + key;
+ }
+ }
+
+ /**
+ * Returns a read-only ByteBuffer (with position = 0, limit = `length`, and capacity = `length`)
+ * sliced from `buf`'s current position, and moves the position of `buf` by `length`.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ public static ByteBuffer sliceAndAdvance(@NonNull ByteBuffer buf, int length)
+ throws BufferUnderflowException {
+ if (buf.remaining() < length) {
+ throw new BufferUnderflowException();
+ }
+ final int pos = buf.position();
+
+ // `out` equals to `buf.slice(pos, length)` that is supported in API level 34.
+ final ByteBuffer out = ((ByteBuffer) buf.slice().limit(length)).slice();
+
+ buf.position(pos + length);
+ return out.asReadOnlyBuffer();
+ }
+
+ // A utility to convert the byte array of SvcParamValue to other types.
+ private static class SvcParamValueUtil {
+ // Refer to draft-ietf-dnsop-svcb-https-10#section-7.1 for the wire format of alpn.
+ @NonNull
+ private static List<String> toStringList(@NonNull ByteBuffer buf)
+ throws BufferUnderflowException, ParseException {
+ final List<String> out = new ArrayList<>();
+ while (buf.hasRemaining()) {
+ final int alpnLen = Byte.toUnsignedInt(buf.get());
+ if (alpnLen == 0) {
+ throw new ParseException("alpn should not be an empty string");
+ }
+ final byte[] alpn = new byte[alpnLen];
+ buf.get(alpn);
+ out.add(new String(alpn, StandardCharsets.UTF_8));
+ }
+ return out;
+ }
+
+ // Refer to draft-ietf-dnsop-svcb-https-10#section-7.5 for the wire format of SvcParamKey
+ // "mandatory".
+ @NonNull
+ private static short[] toShortArray(@NonNull ByteBuffer buf)
+ throws BufferUnderflowException, ParseException {
+ if (buf.remaining() % Short.BYTES != 0) {
+ throw new ParseException("Can't parse whole byte array");
+ }
+ final ShortBuffer sb = buf.asShortBuffer();
+ final short[] out = new short[sb.remaining()];
+ sb.get(out);
+ return out;
+ }
+
+ // Refer to draft-ietf-dnsop-svcb-https-10#section-7.4 for the wire format of ipv4hint and
+ // ipv6hint.
+ @NonNull
+ private static List<InetAddress> toInetAddressList(@NonNull ByteBuffer buf, int addrLen)
+ throws BufferUnderflowException, ParseException {
+ if (buf.remaining() % addrLen != 0) {
+ throw new ParseException("Can't parse whole byte array");
+ }
+
+ final List<InetAddress> out = new ArrayList<>();
+ final byte[] addr = new byte[addrLen];
+ while (buf.remaining() >= addrLen) {
+ buf.get(addr);
+ try {
+ out.add(InetAddress.getByAddress(addr));
+ } catch (UnknownHostException e) {
+ throw new ParseException("Can't parse byte array as an IP address");
+ }
+ }
+ return out;
+ }
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
index 40fc59f..4b27a97 100644
--- a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
@@ -21,6 +21,7 @@
import android.util.Log;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -32,6 +33,7 @@
public class InetAddressUtils {
private static final String TAG = InetAddressUtils.class.getSimpleName();
+ private static final int INET4_ADDR_LENGTH = 4;
private static final int INET6_ADDR_LENGTH = 16;
/**
@@ -93,5 +95,29 @@
}
}
+ /**
+ * Create a v4-mapped v6 address from v4 address
+ *
+ * @param v4Addr Inet4Address which is converted to v4-mapped v6 address
+ * @return v4-mapped v6 address
+ */
+ public static Inet6Address v4MappedV6Address(@NonNull final Inet4Address v4Addr) {
+ final byte[] v6AddrBytes = new byte[INET6_ADDR_LENGTH];
+ v6AddrBytes[10] = (byte) 0xFF;
+ v6AddrBytes[11] = (byte) 0xFF;
+ System.arraycopy(v4Addr.getAddress(), 0 /* srcPos */, v6AddrBytes, 12 /* dstPos */,
+ INET4_ADDR_LENGTH);
+ try {
+ // Using Inet6Address.getByAddress since InetAddress.getByAddress converts v4-mapped v6
+ // address to v4 address internally and returns Inet4Address
+ return Inet6Address.getByAddress(null /* host */, v6AddrBytes, -1 /* scope_id */);
+ } catch (UnknownHostException impossible) {
+ // getByAddress throws UnknownHostException when the argument is the invalid length
+ // but INET6_ADDR_LENGTH(16) is the valid length.
+ Log.wtf(TAG, "Failed to generate v4-mapped v6 address from " + v4Addr, impossible);
+ return null;
+ }
+ }
+
private InetAddressUtils() {}
}
diff --git a/staticlibs/framework/com/android/net/module/util/SdkUtil.java b/staticlibs/framework/com/android/net/module/util/SdkUtil.java
new file mode 100644
index 0000000..5006ba9
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/SdkUtil.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import android.annotation.Nullable;
+
+/**
+ * Utilities to deal with multiple SDKs in a single mainline module.
+ * @hide
+ */
+public class SdkUtil {
+ /**
+ * Holder class taking advantage of erasure to avoid reflection running into class not found
+ * exceptions.
+ *
+ * This is useful to store a reference to a class that might not be present at runtime when
+ * fields are examined through reflection. An example is the MessageUtils class, which tries
+ * to get all fields in a class and therefore will try to load any class for which there
+ * is a member. Another example would be arguments or return values of methods in tests,
+ * when the testing framework uses reflection to list methods and their arguments.
+ *
+ * In these cases, LateSdk<T> can be used to hide type T from reflection, since it's erased
+ * and it becomes a vanilla LateSdk in Java bytecode. The T still can't be instantiated at
+ * runtime of course, but runtime tests will avoid that.
+ *
+ * @param <T> The held type
+ * @hide
+ */
+ public static class LateSdk<T> {
+ @Nullable public final T value;
+ public LateSdk(@Nullable final T value) {
+ this.value = value;
+ }
+ }
+}
diff --git a/staticlibs/lint-baseline.xml b/staticlibs/lint-baseline.xml
deleted file mode 100644
index d413b2a..0000000
--- a/staticlibs/lint-baseline.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
-
- <issue
- id="NewApi"
- message="Call requires API level 30 (current min is 29): `android.net.LinkProperties#getAddresses`"
- errorLine1=" final Collection<InetAddress> leftAddresses = left.getAddresses();"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/libs/net/common/framework/com/android/net/module/util/LinkPropertiesUtils.java"
- line="158"
- column="60"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 30 (current min is 29): `android.net.LinkProperties#getAddresses`"
- errorLine1=" final Collection<InetAddress> rightAddresses = right.getAddresses();"
- errorLine2=" ~~~~~~~~~~~~">
- <location
- file="frameworks/libs/net/common/framework/com/android/net/module/util/LinkPropertiesUtils.java"
- line="159"
- column="62"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 30 (current min is 29): `android.net.NetworkStats#addEntry`"
- errorLine1=" stats = stats.addEntry(entry);"
- errorLine2=" ~~~~~~~~">
- <location
- file="frameworks/libs/net/common/framework/com/android/net/module/util/NetworkStatsUtils.java"
- line="113"
- column="27"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 30 (current min is 29): `new android.net.NetworkStats.Entry`"
- errorLine1=" return new android.net.NetworkStats.Entry("
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/libs/net/common/framework/com/android/net/module/util/NetworkStatsUtils.java"
- line="120"
- column="16"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 30 (current min is 29): `new android.net.NetworkStats`"
- errorLine1=" android.net.NetworkStats stats = new android.net.NetworkStats(0L, 0);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/libs/net/common/framework/com/android/net/module/util/NetworkStatsUtils.java"
- line="108"
- column="42"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 31 (current min is 29): `new android.system.NetlinkSocketAddress`"
- errorLine1=" return new NetlinkSocketAddress(portId, groupsMask);"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="frameworks/libs/net/common/device/com/android/net/module/util/SocketUtils.java"
- line="44"
- column="16"/>
- </issue>
-
-</issues>
\ No newline at end of file
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
index 6c0841c..e81fb92 100644
--- a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
+++ b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
@@ -72,12 +72,31 @@
auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
ASSERT_RESULT_OK(result);
+ EXPECT_TRUE(result.value()->isEmpty());
+
+ struct timespec t1, t2;
+ EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t1));
+ EXPECT_FALSE(result.value()->wait(1000 /*ms*/)); // false because wait should timeout
+ EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t2));
+ long long time1 = t1.tv_sec * 1000000000LL + t1.tv_nsec;
+ long long time2 = t2.tv_sec * 1000000000LL + t2.tv_nsec;
+ EXPECT_GE(time2 - time1, 1000000000 /*ns*/); // 1000 ms as ns
for (int i = 0; i < n; i++) {
RunProgram();
}
+ EXPECT_FALSE(result.value()->isEmpty());
+
+ EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t1));
+ EXPECT_TRUE(result.value()->wait());
+ EXPECT_EQ(0, clock_gettime(CLOCK_MONOTONIC, &t2));
+ time1 = t1.tv_sec * 1000000000LL + t1.tv_nsec;
+ time2 = t2.tv_sec * 1000000000LL + t2.tv_nsec;
+ EXPECT_LE(time2 - time1, 1000000 /*ns*/); // in x86 CF testing < 5000 ns
+
EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(n));
+ EXPECT_TRUE(result.value()->isEmpty());
EXPECT_EQ(output, TEST_RINGBUF_MAGIC_NUM);
EXPECT_EQ(run_count, n);
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
index dd0804c..81be37d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -22,9 +22,15 @@
// Reject the packet
#define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
+// Note arguments to BPF_JUMP(opcode, operand, true_offset, false_offset)
+
+// If not equal, jump over count instructions
+#define BPF_JUMP_IF_NOT_EQUAL(v, count) \
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, (count))
+
// *TWO* instructions: compare and if not equal jump over the accept statement
#define BPF2_ACCEPT_IF_EQUAL(v) \
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, 1), \
+ BPF_JUMP_IF_NOT_EQUAL((v), 1), \
BPF_ACCEPT
// *TWO* instructions: compare and if equal jump over the reject statement
@@ -32,8 +38,24 @@
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
BPF_REJECT
+// *TWO* instructions: compare and if greater or equal jump over the reject statement
+#define BPF2_REJECT_IF_LESS_THAN(v) \
+ BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (v), 1, 0), \
+ BPF_REJECT
+
+// *TWO* instructions: compare and if *NOT* greater jump over the reject statement
+#define BPF2_REJECT_IF_GREATER_THAN(v) \
+ BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (v), 0, 1), \
+ BPF_REJECT
+
+// *THREE* instructions: compare and if *NOT* in range [lo, hi], jump over the reject statement
+#define BPF3_REJECT_IF_NOT_IN_RANGE(lo, hi) \
+ BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, (lo), 0, 1), \
+ BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, (hi), 0, 1), \
+ BPF_REJECT
+
// *TWO* instructions: compare and if none of the bits are set jump over the reject statement
-#define BPF2_REJECT_IF_ANY_BITS_SET(v) \
+#define BPF2_REJECT_IF_ANY_MASKED_BITS_SET(v) \
BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, (v), 0, 1), \
BPF_REJECT
@@ -108,3 +130,55 @@
_Static_assert(field_sizeof(struct ipv6hdr, field) == 4, "field of wrong size"); \
offsetof(ipv6hdr, field); \
}))
+
+// Load the length of the IPv4 header into X index register.
+// ie. X := 4 * IPv4.IHL, where IPv4.IHL is the bottom nibble
+// of the first byte of the IPv4 (aka network layer) header.
+#define BPF_LOADX_NET_RELATIVE_IPV4_HLEN \
+ BPF_STMT(BPF_LDX | BPF_B | BPF_MSH, (__u32)SKF_NET_OFF)
+
+// Blindly assumes no IPv6 extension headers, just does X := 40
+// You may later adjust this as you parse through IPv6 ext hdrs.
+#define BPF_LOADX_CONSTANT_IPV6_HLEN \
+ BPF_STMT(BPF_LDX | BPF_W | BPF_IMM, sizeof(struct ipv6hdr))
+
+// NOTE: all the following require X to be setup correctly (v4: 20+, v6: 40+)
+
+// 8-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_U8(ofs) \
+ BPF_STMT(BPF_LD | BPF_B | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 16-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_BE16(ofs) \
+ BPF_STMT(BPF_LD | BPF_H | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 32-bit load from L4 (TCP/UDP/...) header
+#define BPF_LOAD_NETX_RELATIVE_L4_BE32(ofs) \
+ BPF_STMT(BPF_LD | BPF_W | BPF_IND, (__u32)SKF_NET_OFF + (ofs))
+
+// Both ICMPv4 and ICMPv6 start with u8 type, u8 code
+#define BPF_LOAD_NETX_RELATIVE_ICMP_TYPE BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+#define BPF_LOAD_NETX_RELATIVE_ICMP_CODE BPF_LOAD_NETX_RELATIVE_L4_U8(1)
+
+// IPv6 extension headers (HOPOPTS, DSTOPS, FRAG) begin with a u8 nexthdr
+#define BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR BPF_LOAD_NETX_RELATIVE_L4_U8(0)
+
+// IPv6 fragment header is always exactly 8 bytes long
+#define BPF_LOAD_CONSTANT_V6FRAGHDR_LEN \
+ BPF_STMT(BPF_LD | BPF_IMM, 8)
+
+// HOPOPTS/DSTOPS follow up with 'u8 len', counting 8 byte units, (0->8, 1->16)
+// *THREE* instructions
+#define BPF3_LOAD_NETX_RELATIVE_V6EXTHDR_LEN \
+ BPF_LOAD_NETX_RELATIVE_L4_U8(1), \
+ BPF_STMT(BPF_ALU | BPF_ADD | BPF_K, 1), \
+ BPF_STMT(BPF_ALU | BPF_LSH | BPF_K, 3)
+
+// *TWO* instructions: A += X; X := A
+#define BPF2_ADD_A_TO_X \
+ BPF_STMT(BPF_ALU | BPF_ADD | BPF_X, 0), \
+ BPF_STMT(BPF_MISC | BPF_TAX, 0)
+
+// UDP/UDPLITE/TCP/SCTP/DCCP all start with be16 srcport, dstport
+#define BPF_LOAD_NETX_RELATIVE_SRC_PORT BPF_LOAD_NETX_RELATIVE_L4_BE16(0)
+#define BPF_LOAD_NETX_RELATIVE_DST_PORT BPF_LOAD_NETX_RELATIVE_L4_BE16(2)
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 5d7eb0d..3fede3c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -78,7 +78,7 @@
Result<Key> getFirstKey() const {
Key firstKey;
if (getFirstMapKey(mMapFd, &firstKey)) {
- return ErrnoErrorf("Get firstKey map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::getFirstKey() failed");
}
return firstKey;
}
@@ -86,7 +86,7 @@
Result<Key> getNextKey(const Key& key) const {
Key nextKey;
if (getNextMapKey(mMapFd, &key, &nextKey)) {
- return ErrnoErrorf("Get next key of map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::getNextKey() failed");
}
return nextKey;
}
@@ -94,7 +94,7 @@
Result<Value> readValue(const Key key) const {
Value value;
if (findMapEntry(mMapFd, &key, &value)) {
- return ErrnoErrorf("Read value of map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::readValue() failed");
}
return value;
}
@@ -243,14 +243,14 @@
Result<void> writeValue(const Key& key, const Value& value, uint64_t flags) {
if (writeToMapEntry(mMapFd, &key, &value, flags)) {
- return ErrnoErrorf("Write to map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::writeValue() failed");
}
return {};
}
Result<void> deleteValue(const Key& key) {
if (deleteMapEntry(mMapFd, &key)) {
- return ErrnoErrorf("Delete entry from map {} failed", mMapFd.get());
+ return ErrnoErrorf("BpfMap::deleteValue() failed");
}
return {};
}
@@ -280,7 +280,7 @@
if (map_flags & BPF_F_RDONLY) abort();
mMapFd.reset(createMap(map_type, sizeof(Key), sizeof(Value), max_entries,
map_flags));
- if (!mMapFd.ok()) return ErrnoErrorf("Unable to create map.");
+ if (!mMapFd.ok()) return ErrnoErrorf("BpfMap::resetMap() failed");
abortOnMismatch(/* writable */ true);
return {};
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
index dd1504c..d716358 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -19,6 +19,7 @@
#include <android-base/result.h>
#include <android-base/unique_fd.h>
#include <linux/bpf.h>
+#include <poll.h>
#include <sys/mman.h>
#include <utils/Log.h>
@@ -39,6 +40,11 @@
mProducerPos = nullptr;
}
+ bool isEmpty(void);
+
+ // returns !isEmpty() for convenience
+ bool wait(int timeout_ms = -1);
+
protected:
// Non-initializing constructor, used by Create.
BpfRingbufBase(size_t value_size) : mValueSize(value_size) {}
@@ -197,6 +203,22 @@
return {};
}
+inline bool BpfRingbufBase::isEmpty(void) {
+ uint32_t prod_pos = mProducerPos->load(std::memory_order_relaxed);
+ uint64_t cons_pos = mConsumerPos->load(std::memory_order_relaxed);
+ return (cons_pos & 0xFFFFFFFF) == prod_pos;
+}
+
+inline bool BpfRingbufBase::wait(int timeout_ms) {
+ // possible optimization: if (!isEmpty()) return true;
+ struct pollfd pfd = { // 1-element array
+ .fd = mRingFd.get(),
+ .events = POLLIN,
+ };
+ (void)poll(&pfd, 1, timeout_ms); // 'best effort' poll
+ return !isEmpty();
+}
+
inline base::Result<int> BpfRingbufBase::ConsumeAll(
const std::function<void(const void*)>& callback) {
int64_t count = 0;
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index 67ac0e4..baff09b 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -105,9 +105,19 @@
* implemented in the kernel sources.
*/
-#define KVER_NONE 0
-#define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
-#define KVER_INF 0xFFFFFFFFu
+struct kver_uint { unsigned int kver; };
+#define KVER_(v) ((struct kver_uint){ .kver = (v) })
+#define KVER(a, b, c) KVER_(((a) << 24) + ((b) << 16) + (c))
+#define KVER_NONE KVER_(0)
+#define KVER_4_14 KVER(4, 14, 0)
+#define KVER_4_19 KVER(4, 19, 0)
+#define KVER_5_4 KVER(5, 4, 0)
+#define KVER_5_8 KVER(5, 8, 0)
+#define KVER_5_9 KVER(5, 9, 0)
+#define KVER_5_15 KVER(5, 15, 0)
+#define KVER_INF KVER_(0xFFFFFFFFu)
+
+#define KVER_IS_AT_LEAST(kver, a, b, c) ((kver).kver >= KVER(a, b, c).kver)
/*
* BPFFS (ie. /sys/fs/bpf) labelling is as follows:
@@ -188,10 +198,12 @@
__attribute__ ((section(".maps." #name), used)) \
____btf_map_##name = { }
-#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
- _Static_assert( \
- (min_loader) >= BPFLOADER_IGNORED_ON_VERSION || \
- !((ignore_eng) || (ignore_user) || (ignore_userdebug)), \
+#define BPF_ASSERT_LOADER_VERSION(min_loader, ignore_eng, ignore_user, ignore_userdebug) \
+ _Static_assert( \
+ (min_loader) >= BPFLOADER_IGNORED_ON_VERSION || \
+ !((ignore_eng).ignore_on_eng || \
+ (ignore_user).ignore_on_user || \
+ (ignore_userdebug).ignore_on_userdebug), \
"bpfloader min version must be >= 0.33 in order to use ignored_on");
#define DEFINE_BPF_MAP_BASE(the_map, TYPE, keysize, valuesize, num_entries, \
@@ -209,14 +221,14 @@
.mode = (md), \
.bpfloader_min_ver = (minloader), \
.bpfloader_max_ver = (maxloader), \
- .min_kver = (minkver), \
- .max_kver = (maxkver), \
+ .min_kver = (minkver).kver, \
+ .max_kver = (maxkver).kver, \
.selinux_context = (selinux), \
.pin_subdir = (pindir), \
- .shared = (share), \
- .ignore_on_eng = (ignore_eng), \
- .ignore_on_user = (ignore_user), \
- .ignore_on_userdebug = (ignore_userdebug), \
+ .shared = (share).shared, \
+ .ignore_on_eng = (ignore_eng).ignore_on_eng, \
+ .ignore_on_user = (ignore_user).ignore_on_user, \
+ .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug, \
}; \
BPF_ASSERT_LOADER_VERSION(minloader, ignore_eng, ignore_user, ignore_userdebug);
@@ -230,7 +242,7 @@
selinux, pindir, share, min_loader, max_loader, \
ignore_eng, ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, RINGBUF, 0, 0, size_bytes, usr, grp, md, \
- selinux, pindir, share, KVER(5, 8, 0), KVER_INF, \
+ selinux, pindir, share, KVER_5_8, KVER_INF, \
min_loader, max_loader, ignore_eng, ignore_user, \
ignore_userdebug); \
\
@@ -312,11 +324,11 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false, \
- BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false, \
- /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, PRIVATE, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, LOAD_ON_ENG, \
+ LOAD_ON_USER, LOAD_ON_USERDEBUG)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -362,16 +374,16 @@
const struct bpf_prog_def SECTION("progs") the_prog##_def = { \
.uid = (prog_uid), \
.gid = (prog_gid), \
- .min_kver = (min_kv), \
- .max_kver = (max_kv), \
- .optional = (opt), \
+ .min_kver = (min_kv).kver, \
+ .max_kver = (max_kv).kver, \
+ .optional = (opt).optional, \
.bpfloader_min_ver = (min_loader), \
.bpfloader_max_ver = (max_loader), \
.selinux_context = (selinux), \
.pin_subdir = (pindir), \
- .ignore_on_eng = (ignore_eng), \
- .ignore_on_user = (ignore_user), \
- .ignore_on_userdebug = (ignore_userdebug), \
+ .ignore_on_eng = (ignore_eng).ignore_on_eng, \
+ .ignore_on_user = (ignore_user).ignore_on_user, \
+ .ignore_on_userdebug = (ignore_userdebug).ignore_on_userdebug, \
}; \
SECTION(SECTION_NAME) \
int the_prog
@@ -389,7 +401,7 @@
DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, opt, \
DEFAULT_BPF_PROG_SELINUX_CONTEXT, DEFAULT_BPF_PROG_PIN_SUBDIR, \
- false, false, false)
+ LOAD_ON_ENG, LOAD_ON_USER, LOAD_ON_USERDEBUG)
// Programs (here used in the sense of functions/sections) marked optional are allowed to fail
// to load (for example due to missing kernel patches).
@@ -405,21 +417,24 @@
// programs requiring a kernel version >= min_kv && < max_kv
#define DEFINE_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
- false)
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, \
max_kv) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, true)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
+ OPTIONAL)
// programs requiring a kernel version >= min_kv
#define DEFINE_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
- false)
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG_KVER(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv) \
DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, KVER_INF, \
- true)
+ OPTIONAL)
// programs with no kernel version requirements
#define DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, false)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+ MANDATORY)
#define DEFINE_OPTIONAL_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
- DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, 0, KVER_INF, true)
+ DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, KVER_NONE, KVER_INF, \
+ OPTIONAL)
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index e7428b6..ef03c4d 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -114,6 +114,31 @@
// BPF wants 8, but 32-bit x86 wants 4
//_Static_assert(_Alignof(unsigned long long) == 8, "_Alignof unsigned long long != 8");
+
+// for maps:
+struct shared_bool { bool shared; };
+#define PRIVATE ((struct shared_bool){ .shared = false })
+#define SHARED ((struct shared_bool){ .shared = true })
+
+// for programs:
+struct optional_bool { bool optional; };
+#define MANDATORY ((struct optional_bool){ .optional = false })
+#define OPTIONAL ((struct optional_bool){ .optional = true })
+
+// for both maps and programs:
+struct ignore_on_eng_bool { bool ignore_on_eng; };
+#define LOAD_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = false })
+#define IGNORE_ON_ENG ((struct ignore_on_eng_bool){ .ignore_on_eng = true })
+
+struct ignore_on_user_bool { bool ignore_on_user; };
+#define LOAD_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = false })
+#define IGNORE_ON_USER ((struct ignore_on_user_bool){ .ignore_on_user = true })
+
+struct ignore_on_userdebug_bool { bool ignore_on_userdebug; };
+#define LOAD_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = false })
+#define IGNORE_ON_USERDEBUG ((struct ignore_on_userdebug_bool){ .ignore_on_userdebug = true })
+
+
// Length of strings (incl. selinux_context and pin_subdir)
// in the bpf_map_def and bpf_prog_def structs.
//
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index 13f7cb3..9995cb9 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -44,6 +44,11 @@
return syscall(__NR_bpf, cmd, &attr, sizeof(attr));
}
+// this version is meant for use with cmd's which mutate the argument
+inline int bpf(enum bpf_cmd cmd, bpf_attr *attr) {
+ return syscall(__NR_bpf, cmd, attr, sizeof(*attr));
+}
+
inline int createMap(bpf_map_type map_type, uint32_t key_size, uint32_t value_size,
uint32_t max_entries, uint32_t map_flags) {
return bpf(BPF_MAP_CREATE, {
@@ -160,6 +165,27 @@
});
}
+inline int queryProgram(const BPF_FD_TYPE cg_fd,
+ enum bpf_attach_type attach_type,
+ __u32 query_flags = 0,
+ __u32 attach_flags = 0) {
+ int prog_id = -1; // equivalent to an array of one integer.
+ bpf_attr arg = {
+ .query = {
+ .target_fd = BPF_FD_TO_U32(cg_fd),
+ .attach_type = attach_type,
+ .query_flags = query_flags,
+ .attach_flags = attach_flags,
+ .prog_ids = ptr_to_u64(&prog_id), // pointer to output array
+ .prog_cnt = 1, // in: space - nr of ints in the array, out: used
+ }
+ };
+ int v = bpf(BPF_PROG_QUERY, &arg);
+ if (v) return v; // error case
+ if (!arg.query.prog_cnt) return 0; // no program, kernel never returns zero id
+ return prog_id; // return actual id
+}
+
inline int detachSingleProgram(bpf_attach_type type, const BPF_FD_TYPE prog_fd,
const BPF_FD_TYPE cg_fd) {
return bpf(BPF_PROG_DETACH, {
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index f93d6e1..b92f107 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -15,6 +15,8 @@
*/
#include <errno.h>
+#include <linux/pfkeyv2.h>
+#include <sys/socket.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>
#include <nativehelper/ScopedLocalRef.h>
@@ -117,6 +119,22 @@
return throwIfNotEnoent(env, "nativeFindMapEntry", ret, errno);
}
+static void com_android_net_module_util_BpfMap_nativeSynchronizeKernelRCU(JNIEnv *env,
+ jclass clazz) {
+ const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
+
+ if (pfSocket < 0) {
+ jniThrowErrnoException(env, "nativeSynchronizeKernelRCU:socket", errno);
+ return;
+ }
+
+ if (close(pfSocket)) {
+ jniThrowErrnoException(env, "nativeSynchronizeKernelRCU:close", errno);
+ return;
+ }
+ return;
+}
+
/*
* JNI registration.
*/
@@ -132,6 +150,8 @@
(void*) com_android_net_module_util_BpfMap_nativeGetNextMapKey },
{ "nativeFindMapEntry", "(I[B[B)Z",
(void*) com_android_net_module_util_BpfMap_nativeFindMapEntry },
+ { "nativeSynchronizeKernelRCU", "()V",
+ (void*) com_android_net_module_util_BpfMap_nativeSynchronizeKernelRCU },
};
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
index cb06afb..ab83da6 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_TcUtils.cpp
@@ -27,7 +27,7 @@
}
static jboolean com_android_net_module_util_TcUtils_isEthernet(JNIEnv *env,
- jobject clazz,
+ jclass clazz,
jstring iface) {
ScopedUtfChars interface(env, iface);
bool result = false;
@@ -43,7 +43,7 @@
// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned
// /sys/fs/bpf/... direct-action
static void com_android_net_module_util_TcUtils_tcFilterAddDevBpf(
- JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio,
+ JNIEnv *env, jclass clazz, jint ifIndex, jboolean ingress, jshort prio,
jshort proto, jstring bpfProgPath) {
ScopedUtfChars pathname(env, bpfProgPath);
int error = tcAddBpfFilter(ifIndex, ingress, prio, proto, pathname.c_str());
@@ -59,7 +59,7 @@
// action bpf object-pinned .. \
// drop
static void com_android_net_module_util_TcUtils_tcFilterAddDevIngressPolice(
- JNIEnv *env, jobject clazz, jint ifIndex, jshort prio, jshort proto,
+ JNIEnv *env, jclass clazz, jint ifIndex, jshort prio, jshort proto,
jint rateInBytesPerSec, jstring bpfProgPath) {
ScopedUtfChars pathname(env, bpfProgPath);
int error = tcAddIngressPoliceFilter(ifIndex, prio, proto, rateInBytesPerSec,
@@ -74,7 +74,7 @@
// tc filter del dev .. in/egress prio .. protocol ..
static void com_android_net_module_util_TcUtils_tcFilterDelDev(
- JNIEnv *env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio,
+ JNIEnv *env, jclass clazz, jint ifIndex, jboolean ingress, jshort prio,
jshort proto) {
int error = tcDeleteFilter(ifIndex, ingress, prio, proto);
if (error) {
@@ -86,7 +86,7 @@
// tc qdisc add dev .. clsact
static void com_android_net_module_util_TcUtils_tcQdiscAddDevClsact(JNIEnv *env,
- jobject clazz,
+ jclass clazz,
jint ifIndex) {
int error = tcAddQdiscClsact(ifIndex);
if (error) {
diff --git a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
index 0f2ebbd..bcc3ded 100644
--- a/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
+++ b/staticlibs/native/bpfutiljni/com_android_net_module_util_BpfUtils.cpp
@@ -30,9 +30,8 @@
using base::unique_fd;
-// If attach fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_attachProgramToCgroup(JNIEnv *env,
- jobject clazz, jint type, jstring bpfProgPath, jstring cgroupPath, jint flags) {
+static jint com_android_net_module_util_BpfUtil_getProgramIdFromCgroup(JNIEnv *env,
+ jclass clazz, jint type, jstring cgroupPath) {
ScopedUtfChars dirPath(env, cgroupPath);
unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
@@ -40,87 +39,27 @@
jniThrowExceptionFmt(env, "java/io/IOException",
"Failed to open the cgroup directory %s: %s",
dirPath.c_str(), strerror(errno));
- return false;
+ return -1;
}
- ScopedUtfChars bpfProg(env, bpfProgPath);
- unique_fd bpf_fd(bpf::retrieveProgram(bpfProg.c_str()));
- if (bpf_fd == -1) {
+ int id = bpf::queryProgram(cg_fd, (bpf_attach_type) type);
+ if (id < 0) {
jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to retrieve bpf program from %s: %s",
- bpfProg.c_str(), strerror(errno));
- return false;
+ "Failed to query bpf program %d at %s: %s",
+ type, dirPath.c_str(), strerror(errno));
+ return -1;
}
- if (bpf::attachProgram((bpf_attach_type) type, bpf_fd, cg_fd, flags)) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to attach bpf program %s to %s: %s",
- bpfProg.c_str(), dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
+ return id; // may return 0 meaning none
}
-// If detach fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_detachProgramFromCgroup(JNIEnv *env,
- jobject clazz, jint type, jstring cgroupPath) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- if (bpf::detachProgram((bpf_attach_type) type, cg_fd)) {
- jniThrowExceptionFmt(env, "Failed to detach bpf program from %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
-
-// If detach single program fails throw error and return false.
-static jboolean com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup(JNIEnv *env,
- jobject clazz, jint type, jstring bpfProgPath, jstring cgroupPath) {
-
- ScopedUtfChars dirPath(env, cgroupPath);
- unique_fd cg_fd(open(dirPath.c_str(), O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- if (cg_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to open the cgroup directory %s: %s",
- dirPath.c_str(), strerror(errno));
- return false;
- }
-
- ScopedUtfChars bpfProg(env, bpfProgPath);
- unique_fd bpf_fd(bpf::retrieveProgram(bpfProg.c_str()));
- if (bpf_fd == -1) {
- jniThrowExceptionFmt(env, "java/io/IOException",
- "Failed to retrieve bpf program from %s: %s",
- bpfProg.c_str(), strerror(errno));
- return false;
- }
- if (bpf::detachSingleProgram((bpf_attach_type) type, bpf_fd, cg_fd)) {
- jniThrowExceptionFmt(env, "Failed to detach bpf program %s from %s: %s",
- bpfProg.c_str(), dirPath.c_str(), strerror(errno));
- return false;
- }
- return true;
-}
/*
* JNI registration.
*/
static const JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
- { "native_attachProgramToCgroup", "(ILjava/lang/String;Ljava/lang/String;I)Z",
- (void*) com_android_net_module_util_BpfUtil_attachProgramToCgroup },
- { "native_detachProgramFromCgroup", "(ILjava/lang/String;)Z",
- (void*) com_android_net_module_util_BpfUtil_detachProgramFromCgroup },
- { "native_detachSingleProgramFromCgroup", "(ILjava/lang/String;Ljava/lang/String;)Z",
- (void*) com_android_net_module_util_BpfUtil_detachSingleProgramFromCgroup },
+ { "native_getProgramIdFromCgroup", "(ILjava/lang/String;)I",
+ (void*) com_android_net_module_util_BpfUtil_getProgramIdFromCgroup },
};
int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name) {
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index 65b3b09..637a938 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -21,7 +21,7 @@
sdk_version: "system_current",
min_sdk_version: "30",
static_libs: [
- "netd_aidl_interface-V13-java",
+ "netd_aidl_interface-V14-java",
],
apex_available: [
"//apex_available:platform", // used from services.net
@@ -44,7 +44,7 @@
cc_library_static {
name: "netd_aidl_interface-lateststable-ndk",
whole_static_libs: [
- "netd_aidl_interface-V13-ndk",
+ "netd_aidl_interface-V14-ndk",
],
apex_available: [
"com.android.resolv",
@@ -55,12 +55,12 @@
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_static",
- static_libs: ["netd_aidl_interface-V13-cpp"],
+ static_libs: ["netd_aidl_interface-V14-cpp"],
}
cc_defaults {
name: "netd_aidl_interface_lateststable_cpp_shared",
- shared_libs: ["netd_aidl_interface-V13-cpp"],
+ shared_libs: ["netd_aidl_interface-V14-cpp"],
}
aidl_interface {
@@ -162,8 +162,13 @@
version: "13",
imports: [],
},
+ {
+ version: "14",
+ imports: [],
+ },
],
+ frozen: true,
}
@@ -220,19 +225,6 @@
}
-java_library {
- name: "mdns_aidl_interface-lateststable-java",
- sdk_version: "module_current",
- min_sdk_version: "30",
- static_libs: [
- "mdns_aidl_interface-V1-java",
- ],
- apex_available: [
- "//apex_available:platform",
- "com.android.tethering",
- ],
-}
-
aidl_interface {
name: "mdns_aidl_interface",
local_include_dir: "binder",
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
new file mode 100644
index 0000000..0bf7bde
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/.hash
@@ -0,0 +1 @@
+50bce96bc8d5811ed952950df30ec503f8a561ed
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
new file mode 100644
index 0000000..8ccefb2
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetd.aidl
@@ -0,0 +1,259 @@
+/**
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetd {
+ boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
+ boolean bandwidthEnableDataSaver(boolean enable);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreatePhysical(int netId, int permission);
+ /**
+ * @deprecated use networkCreate() instead.
+ */
+ void networkCreateVpn(int netId, boolean secure);
+ void networkDestroy(int netId);
+ void networkAddInterface(int netId, in @utf8InCpp String iface);
+ void networkRemoveInterface(int netId, in @utf8InCpp String iface);
+ void networkAddUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRemoveUidRanges(int netId, in android.net.UidRangeParcel[] uidRanges);
+ void networkRejectNonSecureVpn(boolean add, in android.net.UidRangeParcel[] uidRanges);
+ void socketDestroy(in android.net.UidRangeParcel[] uidRanges, in int[] exemptUids);
+ boolean tetherApplyDnsInterfaces();
+ android.net.TetherStatsParcel[] tetherGetStats();
+ void interfaceAddAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ void interfaceDelAddress(in @utf8InCpp String ifName, in @utf8InCpp String addrString, int prefixLength);
+ @utf8InCpp String getProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter);
+ void setProcSysNet(int ipversion, int which, in @utf8InCpp String ifname, in @utf8InCpp String parameter, in @utf8InCpp String value);
+ void ipSecSetEncapSocketOwner(in ParcelFileDescriptor socket, int newUid);
+ int ipSecAllocateSpi(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecAddSecurityAssociation(int transformId, int mode, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int underlyingNetId, int spi, int markValue, int markMask, in @utf8InCpp String authAlgo, in byte[] authKey, in int authTruncBits, in @utf8InCpp String cryptAlgo, in byte[] cryptKey, in int cryptTruncBits, in @utf8InCpp String aeadAlgo, in byte[] aeadKey, in int aeadIcvBits, int encapType, int encapLocalPort, int encapRemotePort, int interfaceId);
+ void ipSecDeleteSecurityAssociation(int transformId, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecApplyTransportModeTransform(in ParcelFileDescriptor socket, int transformId, int direction, in @utf8InCpp String sourceAddress, in @utf8InCpp String destinationAddress, int spi);
+ void ipSecRemoveTransportModeTransform(in ParcelFileDescriptor socket);
+ void ipSecAddSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecUpdateSecurityPolicy(int transformId, int selAddrFamily, int direction, in @utf8InCpp String tmplSrcAddress, in @utf8InCpp String tmplDstAddress, int spi, int markValue, int markMask, int interfaceId);
+ void ipSecDeleteSecurityPolicy(int transformId, int selAddrFamily, int direction, int markValue, int markMask, int interfaceId);
+ void ipSecAddTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecUpdateTunnelInterface(in @utf8InCpp String deviceName, in @utf8InCpp String localAddress, in @utf8InCpp String remoteAddress, int iKey, int oKey, int interfaceId);
+ void ipSecRemoveTunnelInterface(in @utf8InCpp String deviceName);
+ void wakeupAddInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void wakeupDelInterface(in @utf8InCpp String ifName, in @utf8InCpp String prefix, int mark, int mask);
+ void setIPv6AddrGenMode(in @utf8InCpp String ifName, int mode);
+ void idletimerAddInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void idletimerRemoveInterface(in @utf8InCpp String ifName, int timeout, in @utf8InCpp String classLabel);
+ void strictUidCleartextPenalty(int uid, int policyPenalty);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ @utf8InCpp String clatdStart(in @utf8InCpp String ifName, in @utf8InCpp String nat64Prefix);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The clatd control plane moved to the mainline module starting in T. See ClatCoordinator.
+ */
+ void clatdStop(in @utf8InCpp String ifName);
+ boolean ipfwdEnabled();
+ @utf8InCpp String[] ipfwdGetRequesterList();
+ void ipfwdEnableForwarding(in @utf8InCpp String requester);
+ void ipfwdDisableForwarding(in @utf8InCpp String requester);
+ void ipfwdAddInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void ipfwdRemoveInterfaceForward(in @utf8InCpp String fromIface, in @utf8InCpp String toIface);
+ void bandwidthSetInterfaceQuota(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceQuota(in @utf8InCpp String ifName);
+ void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
+ void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
+ void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void bandwidthRemoveNiceApp(int uid);
+ void tetherStart(in @utf8InCpp String[] dhcpRanges);
+ void tetherStop();
+ boolean tetherIsEnabled();
+ void tetherInterfaceAdd(in @utf8InCpp String ifName);
+ void tetherInterfaceRemove(in @utf8InCpp String ifName);
+ @utf8InCpp String[] tetherInterfaceList();
+ void tetherDnsSet(int netId, in @utf8InCpp String[] dnsAddrs);
+ @utf8InCpp String[] tetherDnsList();
+ void networkAddRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkRemoveRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop);
+ void networkAddLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ void networkRemoveLegacyRoute(int netId, in @utf8InCpp String ifName, in @utf8InCpp String destination, in @utf8InCpp String nextHop, int uid);
+ int networkGetDefault();
+ void networkSetDefault(int netId);
+ void networkClearDefault();
+ void networkSetPermissionForNetwork(int netId, int permission);
+ void networkSetPermissionForUser(int permission, in int[] uids);
+ void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSetNetPermForUids(int permission, in int[] uids);
+ void networkSetProtectAllow(int uid);
+ void networkSetProtectDeny(int uid);
+ boolean networkCanProtect(int uid);
+ void firewallSetFirewallType(int firewalltype);
+ void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallEnableChildChain(int childChain, boolean enable);
+ @utf8InCpp String[] interfaceGetList();
+ android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
+ void interfaceSetCfg(in android.net.InterfaceConfigurationParcel cfg);
+ void interfaceSetIPv6PrivacyExtensions(in @utf8InCpp String ifName, boolean enable);
+ void interfaceClearAddrs(in @utf8InCpp String ifName);
+ void interfaceSetEnableIPv6(in @utf8InCpp String ifName, boolean enable);
+ void interfaceSetMtu(in @utf8InCpp String ifName, int mtu);
+ void tetherAddForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
+ void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
+ void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
+ void trafficSwapActiveStatsMap();
+ IBinder getOemNetd();
+ void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
+ android.net.MarkMaskParcel getFwmarkForNetwork(int netId);
+ void networkAddRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkUpdateRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ void networkRemoveRouteParcel(int netId, in android.net.RouteInfoParcel routeInfo);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleAdd(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadRuleRemove(in android.net.TetherOffloadRuleParcel rule);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel[] tetherOffloadGetStats();
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ void tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+ /**
+ * @deprecated This method has no effect and throws UnsupportedOperationException. The mainline module accesses the BPF map directly starting in S. See BpfCoordinator.
+ */
+ android.net.TetherStatsParcel tetherOffloadGetAndClearStats(int ifIndex);
+ void networkCreate(in android.net.NativeNetworkConfig config);
+ void networkAddUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void networkRemoveUidRangesParcel(in android.net.netd.aidl.NativeUidRangeConfig uidRangesConfig);
+ void ipSecMigrate(in android.net.IpSecMigrateInfoParcel migrateInfo);
+ void setNetworkAllowlist(in android.net.netd.aidl.NativeUidRangeConfig[] allowedNetworks);
+ const int IPV4 = 4;
+ const int IPV6 = 6;
+ const int CONF = 1;
+ const int NEIGH = 2;
+ const String IPSEC_INTERFACE_PREFIX = "ipsec";
+ const int IPV6_ADDR_GEN_MODE_EUI64 = 0;
+ const int IPV6_ADDR_GEN_MODE_NONE = 1;
+ const int IPV6_ADDR_GEN_MODE_STABLE_PRIVACY = 2;
+ const int IPV6_ADDR_GEN_MODE_RANDOM = 3;
+ const int IPV6_ADDR_GEN_MODE_DEFAULT = 0;
+ const int PENALTY_POLICY_ACCEPT = 1;
+ const int PENALTY_POLICY_LOG = 2;
+ const int PENALTY_POLICY_REJECT = 3;
+ const int CLAT_MARK = 0xdeadc1a7;
+ const int LOCAL_NET_ID = 99;
+ const int DUMMY_NET_ID = 51;
+ const int UNREACHABLE_NET_ID = 52;
+ const String NEXTHOP_NONE = "";
+ const String NEXTHOP_UNREACHABLE = "unreachable";
+ const String NEXTHOP_THROW = "throw";
+ const int PERMISSION_NONE = 0;
+ const int PERMISSION_NETWORK = 1;
+ const int PERMISSION_SYSTEM = 2;
+ const int NO_PERMISSIONS = 0;
+ const int PERMISSION_INTERNET = 4;
+ const int PERMISSION_UPDATE_DEVICE_STATS = 8;
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
+ /**
+ * @deprecated use FIREWALL_ALLOWLIST.
+ */
+ const int FIREWALL_WHITELIST = 0;
+ const int FIREWALL_ALLOWLIST = 0;
+ /**
+ * @deprecated use FIREWALL_DENYLIST.
+ */
+ const int FIREWALL_BLACKLIST = 1;
+ const int FIREWALL_DENYLIST = 1;
+ const int FIREWALL_RULE_ALLOW = 1;
+ const int FIREWALL_RULE_DENY = 2;
+ const int FIREWALL_CHAIN_NONE = 0;
+ const int FIREWALL_CHAIN_DOZABLE = 1;
+ const int FIREWALL_CHAIN_STANDBY = 2;
+ const int FIREWALL_CHAIN_POWERSAVE = 3;
+ const int FIREWALL_CHAIN_RESTRICTED = 4;
+ const String IF_STATE_UP = "up";
+ const String IF_STATE_DOWN = "down";
+ const String IF_FLAG_BROADCAST = "broadcast";
+ const String IF_FLAG_LOOPBACK = "loopback";
+ const String IF_FLAG_POINTOPOINT = "point-to-point";
+ const String IF_FLAG_RUNNING = "running";
+ const String IF_FLAG_MULTICAST = "multicast";
+ const int IPSEC_DIRECTION_IN = 0;
+ const int IPSEC_DIRECTION_OUT = 1;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
new file mode 100644
index 0000000..31775df
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/INetdUnsolicitedEventListener.aidl
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2018, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetdUnsolicitedEventListener {
+ oneway void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs, int uid);
+ oneway void onQuotaLimitReached(@utf8InCpp String alertName, @utf8InCpp String ifName);
+ oneway void onInterfaceDnsServerInfo(@utf8InCpp String ifName, long lifetimeS, in @utf8InCpp String[] servers);
+ oneway void onInterfaceAddressUpdated(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAddressRemoved(@utf8InCpp String addr, @utf8InCpp String ifName, int flags, int scope);
+ oneway void onInterfaceAdded(@utf8InCpp String ifName);
+ oneway void onInterfaceRemoved(@utf8InCpp String ifName);
+ oneway void onInterfaceChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onInterfaceLinkStateChanged(@utf8InCpp String ifName, boolean up);
+ oneway void onRouteChanged(boolean updated, @utf8InCpp String route, @utf8InCpp String gateway, @utf8InCpp String ifName);
+ oneway void onStrictCleartextDetected(int uid, @utf8InCpp String hex);
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
new file mode 100644
index 0000000..1869d8d
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/InterfaceConfigurationParcel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable InterfaceConfigurationParcel {
+ @utf8InCpp String ifName;
+ @utf8InCpp String hwAddr;
+ @utf8InCpp String ipv4Addr;
+ int prefixLength;
+ @utf8InCpp String[] flags;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl
new file mode 100644
index 0000000..975a261
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/IpSecMigrateInfoParcel.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaOnlyImmutable
+parcelable IpSecMigrateInfoParcel {
+ int requestId;
+ int selAddrFamily;
+ int direction;
+ @utf8InCpp String oldSourceAddress;
+ @utf8InCpp String oldDestinationAddress;
+ @utf8InCpp String newSourceAddress;
+ @utf8InCpp String newDestinationAddress;
+ int interfaceId;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
new file mode 100644
index 0000000..8ea20d1
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/MarkMaskParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable MarkMaskParcel {
+ int mark;
+ int mask;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
new file mode 100644
index 0000000..77d814b
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkConfig.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeNetworkConfig {
+ int netId;
+ android.net.NativeNetworkType networkType = android.net.NativeNetworkType.PHYSICAL;
+ int permission;
+ boolean secure;
+ android.net.NativeVpnType vpnType = android.net.NativeVpnType.PLATFORM;
+ boolean excludeLocalRoutes = false;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
new file mode 100644
index 0000000..e77a143
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeNetworkType.aidl
@@ -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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeNetworkType {
+ PHYSICAL = 0,
+ VIRTUAL = 1,
+ PHYSICAL_LOCAL = 2,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
new file mode 100644
index 0000000..8a8be83
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/NativeVpnType.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@Backing(type="int")
+enum NativeVpnType {
+ SERVICE = 1,
+ PLATFORM = 2,
+ LEGACY = 3,
+ OEM = 4,
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
new file mode 100644
index 0000000..5ef95e6
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/RouteInfoParcel.aidl
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+parcelable RouteInfoParcel {
+ @utf8InCpp String destination;
+ @utf8InCpp String ifName;
+ @utf8InCpp String nextHop;
+ int mtu;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
new file mode 100644
index 0000000..7b39c22
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherConfigParcel.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherConfigParcel {
+ boolean usingLegacyDnsProxy;
+ @utf8InCpp String[] dhcpRanges;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
new file mode 100644
index 0000000..983e986
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherOffloadRuleParcel.aidl
@@ -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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherOffloadRuleParcel {
+ int inputInterfaceIndex;
+ int outputInterfaceIndex;
+ byte[] destination;
+ int prefixLength;
+ byte[] srcL2Address;
+ byte[] dstL2Address;
+ int pmtu = 1500;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
new file mode 100644
index 0000000..5f1b722
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/TetherStatsParcel.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+parcelable TetherStatsParcel {
+ @utf8InCpp String iface;
+ long rxBytes;
+ long rxPackets;
+ long txBytes;
+ long txPackets;
+ int ifIndex = 0;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
new file mode 100644
index 0000000..72e987a
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/UidRangeParcel.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable UidRangeParcel {
+ int start;
+ int stop;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
new file mode 100644
index 0000000..9bb679f
--- /dev/null
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/14/android/net/netd/aidl/NativeUidRangeConfig.aidl
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+// the interface (from the latest frozen version), the build system will
+// prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.netd.aidl;
+/* @hide */
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable NativeUidRangeConfig {
+ int netId;
+ android.net.UidRangeParcel[] uidRanges;
+ int subPriority;
+}
diff --git a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
index 3507784..8ccefb2 100644
--- a/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
+++ b/staticlibs/netd/aidl_api/netd_aidl_interface/current/android/net/INetd.aidl
@@ -35,6 +35,9 @@
/* @hide */
interface INetd {
boolean isAlive();
+ /**
+ * @deprecated unimplemented on T+.
+ */
boolean firewallReplaceUidChain(in @utf8InCpp String chainName, boolean isAllowlist, in int[] uids);
boolean bandwidthEnableDataSaver(boolean enable);
/**
@@ -95,9 +98,21 @@
void bandwidthSetInterfaceAlert(in @utf8InCpp String ifName, long bytes);
void bandwidthRemoveInterfaceAlert(in @utf8InCpp String ifName);
void bandwidthSetGlobalAlert(long bytes);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthAddNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthRemoveNaughtyApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthAddNiceApp(int uid);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void bandwidthRemoveNiceApp(int uid);
void tetherStart(in @utf8InCpp String[] dhcpRanges);
void tetherStop();
@@ -117,13 +132,22 @@
void networkSetPermissionForNetwork(int netId, int permission);
void networkSetPermissionForUser(int permission, in int[] uids);
void networkClearPermissionForUser(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void trafficSetNetPermForUids(int permission, in int[] uids);
void networkSetProtectAllow(int uid);
void networkSetProtectDeny(int uid);
boolean networkCanProtect(int uid);
void firewallSetFirewallType(int firewalltype);
void firewallSetInterfaceRule(in @utf8InCpp String ifName, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallSetUidRule(int childChain, int uid, int firewallRule);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallEnableChildChain(int childChain, boolean enable);
@utf8InCpp String[] interfaceGetList();
android.net.InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName);
@@ -136,8 +160,17 @@
void tetherRemoveForward(in @utf8InCpp String intIface, in @utf8InCpp String extIface);
void setTcpRWmemorySize(in @utf8InCpp String rmemValues, in @utf8InCpp String wmemValues);
void registerUnsolicitedEventListener(android.net.INetdUnsolicitedEventListener listener);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void firewallRemoveUidInterfaceRules(in int[] uids);
+ /**
+ * @deprecated unimplemented on T+.
+ */
void trafficSwapActiveStatsMap();
IBinder getOemNetd();
void tetherStartWithConfiguration(in android.net.TetherConfigParcel config);
@@ -196,7 +229,7 @@
const int NO_PERMISSIONS = 0;
const int PERMISSION_INTERNET = 4;
const int PERMISSION_UPDATE_DEVICE_STATS = 8;
- const int PERMISSION_UNINSTALLED = (-1);
+ const int PERMISSION_UNINSTALLED = (-1) /* -1 */;
/**
* @deprecated use FIREWALL_ALLOWLIST.
*/
diff --git a/staticlibs/netd/binder/android/net/INetd.aidl b/staticlibs/netd/binder/android/net/INetd.aidl
index 27d9a03..ee27e84 100644
--- a/staticlibs/netd/binder/android/net/INetd.aidl
+++ b/staticlibs/netd/binder/android/net/INetd.aidl
@@ -47,6 +47,7 @@
* @param isAllowlist Whether this is an allowlist or denylist chain.
* @param uids The list of UIDs to allow/deny.
* @return true if the chain was successfully replaced, false otherwise.
+ * @deprecated unimplemented on T+.
*/
boolean firewallReplaceUidChain(in @utf8InCpp String chainName,
boolean isAllowlist,
@@ -683,6 +684,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthAddNaughtyApp(int uid);
@@ -692,6 +694,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthRemoveNaughtyApp(int uid);
@@ -701,6 +704,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthAddNiceApp(int uid);
@@ -710,6 +714,7 @@
* @param uid uid of target app
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void bandwidthRemoveNiceApp(int uid);
@@ -983,6 +988,7 @@
* PERMISSION_UPDATE_DEVICE_STATS. If the permission is NO_PERMISSIONS, then
* revoke all permissions for the uids.
* @param uids uid of users to grant permission
+ * @deprecated unimplemented on T+.
*/
void trafficSetNetPermForUids(int permission, in int[] uids);
@@ -1071,6 +1077,7 @@
* @param firewallRule either FIREWALL_RULE_ALLOW or FIREWALL_RULE_DENY
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallSetUidRule(int childChain, int uid, int firewallRule);
@@ -1081,6 +1088,7 @@
* @param enable whether to enable or disable child chain.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallEnableChildChain(int childChain, boolean enable);
@@ -1212,6 +1220,7 @@
* @param uids an array of UIDs which the filtering rules will be set
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallAddUidInterfaceRules(in @utf8InCpp String ifName, in int[] uids);
@@ -1224,6 +1233,7 @@
* @param uids an array of UIDs from which the filtering rules will be removed
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void firewallRemoveUidInterfaceRules(in int[] uids);
@@ -1231,6 +1241,7 @@
* Request netd to change the current active network stats map.
* @throws ServiceSpecificException in case of failure, with an error code indicating the
* cause of the failure.
+ * @deprecated unimplemented on T+.
*/
void trafficSwapActiveStatsMap();
diff --git a/staticlibs/netd/libnetdutils/Utils.cpp b/staticlibs/netd/libnetdutils/Utils.cpp
index 16ec882..9b0b3e0 100644
--- a/staticlibs/netd/libnetdutils/Utils.cpp
+++ b/staticlibs/netd/libnetdutils/Utils.cpp
@@ -16,6 +16,7 @@
*/
#include <map>
+#include <vector>
#include <net/if.h>
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 9e04676..06b3e2f 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -71,6 +71,10 @@
public class DeviceConfigUtilsTest {
private static final String TEST_NAME_SPACE = "connectivity";
private static final String TEST_EXPERIMENT_FLAG = "experiment_flag";
+ private static final String CORE_NETWORKING_TRUNK_STABLE_NAMESPACE = "android_core_networking";
+ private static final String TEST_TRUNK_STABLE_FLAG = "trunk_stable_feature";
+ private static final String TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY =
+ "com.android.net.flags.trunk_stable_feature";
private static final int TEST_FLAG_VALUE = 28;
private static final String TEST_FLAG_VALUE_STRING = "28";
private static final int TEST_DEFAULT_FLAG_VALUE = 0;
@@ -301,15 +305,12 @@
assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
- // Follow defaultEnabled if the flag is not set
+ // If the flag is not set feature is disabled
doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY,
TEST_EXPERIMENT_FLAG));
doReturn(null).when(() -> DeviceConfig.getProperty(NAMESPACE_TETHERING,
TEST_EXPERIMENT_FLAG));
- assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG,
- false /* defaultEnabled */));
- assertTrue(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG,
- true /* defaultEnabled */));
+ assertFalse(DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isTetheringFeatureEnabled(mContext, TEST_EXPERIMENT_FLAG));
}
@@ -506,4 +507,25 @@
verify(mContext, never()).getPackageName();
verify(mPm, never()).getPackageInfo(anyString(), anyInt());
}
+
+ @Test
+ public void testIsCoreNetworkingTrunkStableFeatureEnabled() {
+ doReturn(null).when(() -> DeviceConfig.getProperty(
+ CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+ TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
+ assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
+ TEST_TRUNK_STABLE_FLAG));
+
+ doReturn("false").when(() -> DeviceConfig.getProperty(
+ CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+ TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
+ assertFalse(DeviceConfigUtils.isTrunkStableFeatureEnabled(
+ TEST_TRUNK_STABLE_FLAG));
+
+ doReturn("true").when(() -> DeviceConfig.getProperty(
+ CORE_NETWORKING_TRUNK_STABLE_NAMESPACE,
+ TEST_CORE_NETWORKING_TRUNK_STABLE_FLAG_PROPERTY));
+ assertTrue(DeviceConfigUtils.isTrunkStableFeatureEnabled(
+ TEST_TRUNK_STABLE_FLAG));
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
index 28e183a..88d9e1e 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsPacketTest.java
@@ -203,6 +203,30 @@
"test.com", CLASS_IN, 0 /* ttl */, "example.com"));
}
+ /** Verifies that the type of implementation returned from DnsRecord#parse is correct */
+ @Test
+ public void testDnsRecordParse() throws IOException {
+ final byte[] svcbQuestionRecord = new byte[] {
+ 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
+ 0x00, 0x40, /* Type */
+ 0x00, 0x01, /* Class */
+ };
+ assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.QDSECTION,
+ ByteBuffer.wrap(svcbQuestionRecord)) instanceof DnsSvcbRecord);
+
+ final byte[] svcbAnswerRecord = new byte[] {
+ 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00, /* Name */
+ 0x00, 0x40, /* Type */
+ 0x00, 0x01, /* Class */
+ 0x00, 0x00, 0x01, 0x2b, /* TTL */
+ 0x00, 0x0b, /* Data length */
+ 0x00, 0x01, /* SvcPriority */
+ 0x03, 'd', 'o', 't', 0x03, 'c', 'o', 'm', 0x00, /* TargetName */
+ };
+ assertTrue(DnsPacket.DnsRecord.parse(DnsPacket.ANSECTION,
+ ByteBuffer.wrap(svcbAnswerRecord)) instanceof DnsSvcbRecord);
+ }
+
/**
* Verifies ttl/rData error handling when parsing
* {@link DnsPacket.DnsRecord} from bytes.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
new file mode 100644
index 0000000..d59795f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DnsSvcbPacketTest.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util;
+
+import static android.net.DnsResolver.CLASS_IN;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+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 android.annotation.NonNull;
+import android.net.InetAddresses;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class DnsSvcbPacketTest {
+ private static final short TEST_TRANSACTION_ID = 0x4321;
+ private static final byte[] TEST_DNS_RESPONSE_HEADER_FLAG = new byte[] { (byte) 0x81, 0x00 };
+
+ // A common DNS SVCB Question section with Name = "_dns.resolver.arpa".
+ private static final byte[] TEST_DNS_SVCB_QUESTION_SECTION = new byte[] {
+ 0x04, '_', 'd', 'n', 's', 0x08, 'r', 'e', 's', 'o', 'l', 'v', 'e', 'r',
+ 0x04, 'a', 'r', 'p', 'a', 0x00, 0x00, 0x40, 0x00, 0x01,
+ };
+
+ // mandatory=ipv4hint,alpn,key333
+ private static final byte[] TEST_SVC_PARAM_MANDATORY = new byte[] {
+ 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x01, 0x01, 0x4d,
+ };
+
+ // alpn=doq
+ private static final byte[] TEST_SVC_PARAM_ALPN_DOQ = new byte[] {
+ 0x00, 0x01, 0x00, 0x04, 0x03, 'd', 'o', 'q'
+ };
+
+ // alpn=h2,http/1.1
+ private static final byte[] TEST_SVC_PARAM_ALPN_HTTPS = new byte[] {
+ 0x00, 0x01, 0x00, 0x0c, 0x02, 'h', '2',
+ 0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
+ };
+
+ // no-default-alpn
+ private static final byte[] TEST_SVC_PARAM_NO_DEFAULT_ALPN = new byte[] {
+ 0x00, 0x02, 0x00, 0x00,
+ };
+
+ // port=5353
+ private static final byte[] TEST_SVC_PARAM_PORT = new byte[] {
+ 0x00, 0x03, 0x00, 0x02, 0x14, (byte) 0xe9,
+ };
+
+ // ipv4hint=1.2.3.4,6.7.8.9
+ private static final byte[] TEST_SVC_PARAM_IPV4HINT_1 = new byte[] {
+ 0x00, 0x04, 0x00, 0x08, 0x01, 0x02, 0x03, 0x04, 0x06, 0x07, 0x08, 0x09,
+ };
+
+ // ipv4hint=4.3.2.1
+ private static final byte[] TEST_SVC_PARAM_IPV4HINT_2 = new byte[] {
+ 0x00, 0x04, 0x00, 0x04, 0x04, 0x03, 0x02, 0x01,
+ };
+
+ // ech=aBcDe
+ private static final byte[] TEST_SVC_PARAM_ECH = new byte[] {
+ 0x00, 0x05, 0x00, 0x05, 'a', 'B', 'c', 'D', 'e',
+ };
+
+ // ipv6hint=2001:db8::1
+ private static final byte[] TEST_SVC_PARAM_IPV6HINT = new byte[] {
+ 0x00, 0x06, 0x00, 0x10, 0x20, 0x01, 0x0d, (byte) 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ };
+
+ // dohpath=/some-path{?dns}
+ private static final byte[] TEST_SVC_PARAM_DOHPATH = new byte[] {
+ 0x00, 0x07, 0x00, 0x10,
+ '/', 's', 'o', 'm', 'e', '-', 'p', 'a', 't', 'h', '{', '?', 'd', 'n', 's', '}',
+ };
+
+ // key12345=1A2B0C
+ private static final byte[] TEST_SVC_PARAM_GENERIC_WITH_VALUE = new byte[] {
+ 0x30, 0x39, 0x00, 0x03, 0x1a, 0x2b, 0x0c,
+ };
+
+ // key12346
+ private static final byte[] TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE = new byte[] {
+ 0x30, 0x3a, 0x00, 0x00,
+ };
+
+ private static byte[] makeDnsResponseHeaderAsByteArray(int qdcount, int ancount, int nscount,
+ int arcount) {
+ final ByteBuffer buffer = ByteBuffer.wrap(new byte[12]);
+ buffer.putShort(TEST_TRANSACTION_ID); /* Transaction ID */
+ buffer.put(TEST_DNS_RESPONSE_HEADER_FLAG); /* Flags */
+ buffer.putShort((short) qdcount);
+ buffer.putShort((short) ancount);
+ buffer.putShort((short) nscount);
+ buffer.putShort((short) arcount);
+ return buffer.array();
+ }
+
+ private static DnsSvcbRecord makeDnsSvcbRecordFromByteArray(@NonNull byte[] data)
+ throws IOException {
+ return new DnsSvcbRecord(DnsPacket.ANSECTION, ByteBuffer.wrap(data));
+ }
+
+ private static DnsSvcbRecord makeDnsSvcbRecordWithSingleSvcParam(@NonNull byte[] svcParam)
+ throws IOException {
+ return makeDnsSvcbRecordFromByteArray(new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .setTargetName("test.com")
+ .addRdata(svcParam)
+ .build());
+ }
+
+ // Converts a Short to a byte array in big endian.
+ private static byte[] shortToByteArray(short value) {
+ return new byte[] { (byte) (value >> 8), (byte) value };
+ }
+
+ private static byte[] getRemainingByteArray(@NonNull ByteBuffer buffer) {
+ final byte[] out = new byte[buffer.remaining()];
+ buffer.get(out);
+ return out;
+ }
+
+ // A utility to make a DNS record as byte array.
+ private static class TestDnsRecordByteArrayBuilder {
+ private static final byte[] NAME_COMPRESSION_POINTER = new byte[] { (byte) 0xc0, 0x0c };
+
+ private final String mRRName = "dns.com";
+ private short mRRType = 0;
+ private final short mRRClass = CLASS_IN;
+ private final int mRRTtl = 10;
+ private int mRdataLen = 0;
+ private final ArrayList<byte[]> mRdata = new ArrayList<>();
+ private String mTargetName = null;
+ private short mSvcPriority = 1;
+ private boolean mNameCompression = false;
+
+ TestDnsRecordByteArrayBuilder setNameCompression(boolean value) {
+ mNameCompression = value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder setRRType(int value) {
+ mRRType = (short) value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder setTargetName(@NonNull String value) throws IOException {
+ mTargetName = value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder setSvcPriority(int value) {
+ mSvcPriority = (short) value;
+ return this;
+ }
+
+ TestDnsRecordByteArrayBuilder addRdata(@NonNull byte[] value) {
+ mRdata.add(value);
+ mRdataLen += value.length;
+ return this;
+ }
+
+ byte[] build() throws IOException {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ final byte[] name = mNameCompression ? NAME_COMPRESSION_POINTER
+ : DnsPacketUtils.DnsRecordParser.domainNameToLabels(mRRName);
+ os.write(name);
+ os.write(shortToByteArray(mRRType));
+ os.write(shortToByteArray(mRRClass));
+ os.write(HexDump.toByteArray(mRRTtl));
+ if (mTargetName == null) {
+ os.write(shortToByteArray((short) mRdataLen));
+ } else {
+ final byte[] targetNameLabels =
+ DnsPacketUtils.DnsRecordParser.domainNameToLabels(mTargetName);
+ mRdataLen += (Short.BYTES + targetNameLabels.length);
+ os.write(shortToByteArray((short) mRdataLen));
+ os.write(shortToByteArray(mSvcPriority));
+ os.write(targetNameLabels);
+ }
+ for (byte[] data : mRdata) {
+ os.write(data);
+ }
+ return os.toByteArray();
+ }
+ }
+
+ @Test
+ public void testSliceAndAdvance() throws Exception {
+ final ByteBuffer buffer = ByteBuffer.wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9});
+ final ByteBuffer slice1 = DnsSvcbRecord.sliceAndAdvance(buffer, 3);
+ final ByteBuffer slice2 = DnsSvcbRecord.sliceAndAdvance(buffer, 4);
+ assertEquals(0, slice1.position());
+ assertEquals(3, slice1.capacity());
+ assertEquals(3, slice1.remaining());
+ assertTrue(slice1.isReadOnly());
+ assertArrayEquals(new byte[] {1, 2, 3}, getRemainingByteArray(slice1));
+ assertEquals(0, slice2.position());
+ assertEquals(4, slice2.capacity());
+ assertEquals(4, slice2.remaining());
+ assertTrue(slice2.isReadOnly());
+ assertArrayEquals(new byte[] {4, 5, 6, 7}, getRemainingByteArray(slice2));
+
+ // Nothing is read if out-of-bound access happens.
+ assertThrows(BufferUnderflowException.class,
+ () -> DnsSvcbRecord.sliceAndAdvance(buffer, 5));
+ assertEquals(7, buffer.position());
+ assertEquals(9, buffer.capacity());
+ assertEquals(2, buffer.remaining());
+ assertArrayEquals(new byte[] {8, 9}, getRemainingByteArray(buffer));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamMandatory() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_MANDATORY);
+ // Check the content returned from toString() for now because the getter function for
+ // this SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.isMandatory(String alpn) when needed.
+ assertTrue(record.toString().contains("ipv4hint"));
+ assertTrue(record.toString().contains("alpn"));
+ assertTrue(record.toString().contains("key333"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamAlpn() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ALPN_HTTPS);
+ assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamNoDefaultAlpn() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+ TEST_SVC_PARAM_NO_DEFAULT_ALPN);
+ // Check the content returned from toString() for now because the getter function for
+ // this SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.hasNoDefaultAlpn() when needed.
+ assertTrue(record.toString().contains("no-default-alpn"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamPort() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_PORT);
+ assertEquals(5353, record.getPort());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamIpv4Hint() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV4HINT_2);
+ assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("4.3.2.1")),
+ record.getAddresses());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamEch() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_ECH);
+ // Check the content returned from toString() for now because the getter function for
+ // this SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.getEch() when needed.
+ assertTrue(record.toString().contains("ech=6142634465"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamIpv6Hint() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_IPV6HINT);
+ assertEquals(Arrays.asList(InetAddresses.parseNumericAddress("2001:db8::1")),
+ record.getAddresses());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamDohPath() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(TEST_SVC_PARAM_DOHPATH);
+ assertEquals("/some-path{?dns}", record.getDohPath());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamGeneric_withValue() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+ TEST_SVC_PARAM_GENERIC_WITH_VALUE);
+ // Check the content returned from toString() for now because the getter function for
+ // generic SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
+ // when needed.
+ assertTrue(record.toString().contains("key12345=1A2B0C"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_svcParamGeneric_withoutValue() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordWithSingleSvcParam(
+ TEST_SVC_PARAM_GENERIC_WITHOUT_VALUE);
+ // Check the content returned from toString() for now because the getter function for
+ // generic SvcParam hasn't been implemented.
+ // TODO(b/240259333): Consider adding DnsSvcbRecord.getValueFromGenericSvcParam(int key)
+ // when needed.
+ assertTrue(record.toString().contains("key12346"));
+ }
+
+ @Test
+ public void testDnsSvcbRecord() throws Exception {
+ final DnsSvcbRecord record = makeDnsSvcbRecordFromByteArray(
+ new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .setTargetName("doh.dns.com")
+ .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_1)
+ .addRdata(TEST_SVC_PARAM_IPV6HINT)
+ .addRdata(TEST_SVC_PARAM_PORT)
+ .addRdata(TEST_SVC_PARAM_DOHPATH)
+ .build());
+ assertEquals("doh.dns.com", record.getTargetName());
+ assertEquals(Arrays.asList("h2", "http/1.1"), record.getAlpns());
+ assertEquals(5353, record.getPort());
+ assertEquals(Arrays.asList(
+ InetAddresses.parseNumericAddress("1.2.3.4"),
+ InetAddresses.parseNumericAddress("6.7.8.9"),
+ InetAddresses.parseNumericAddress("2001:db8::1")), record.getAddresses());
+ assertEquals("/some-path{?dns}", record.getDohPath());
+ }
+
+ @Test
+ public void testDnsSvcbRecord_createdFromNullObject() throws Exception {
+ assertThrows(NullPointerException.class, () -> makeDnsSvcbRecordFromByteArray(null));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_invalidDnsRecord() throws Exception {
+ // The type is not SVCB.
+ final byte[] bytes1 = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_A)
+ .addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
+ .build();
+ assertThrows(IllegalStateException.class, () -> makeDnsSvcbRecordFromByteArray(bytes1));
+
+ // TargetName is missing.
+ final byte[] bytes2 = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .addRdata(new byte[] { 0x01, 0x01 })
+ .build();
+ assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes2));
+
+ // Rdata is empty.
+ final byte[] bytes3 = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .build();
+ assertThrows(BufferUnderflowException.class, () -> makeDnsSvcbRecordFromByteArray(bytes3));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_repeatedKeyIsInvalid() throws Exception {
+ final byte[] bytes = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .build();
+ assertThrows(DnsPacket.ParseException.class, () -> makeDnsSvcbRecordFromByteArray(bytes));
+ }
+
+ @Test
+ public void testDnsSvcbRecord_invalidContent() throws Exception {
+ final List<byte[]> invalidContents = Arrays.asList(
+ // Invalid SvcParamValue for "mandatory":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue must be multiple of 2.
+ new byte[] { 0x00, 0x00, 0x00, 0x00},
+ new byte[] { 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06 },
+ new byte[] { 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, },
+ new byte[] { 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00 },
+
+ // Invalid SvcParamValue for "alpn":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - Alpn length is less than the actual data size.
+ // - Alpn length is more than the actual data size.
+ // - Alpn must be a non-empty string.
+ new byte[] { 0x00, 0x01, 0x00, 0x00},
+ new byte[] { 0x00, 0x01, 0x00, 0x02, 0x02, 'h', '2' },
+ new byte[] { 0x00, 0x01, 0x00, 0x05, 0x02, 'h', '2' },
+ new byte[] { 0x00, 0x01, 0x00, 0x04, 0x02, 'd', 'o', 't' },
+ new byte[] { 0x00, 0x01, 0x00, 0x04, 0x08, 'd', 'o', 't' },
+ new byte[] { 0x00, 0x01, 0x00, 0x08, 0x02, 'h', '2', 0x00 },
+
+ // Invalid SvcParamValue for "no-default-alpn":
+ // - SvcParamValue must be empty.
+ // - SvcParamValue length must be 0.
+ new byte[] { 0x00, 0x02, 0x00, 0x04, 'd', 'a', 't', 'a' },
+ new byte[] { 0x00, 0x02, 0x00, 0x04 },
+
+ // Invalid SvcParamValue for "port":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue length must be multiple of 2.
+ new byte[] { 0x00, 0x03, 0x00, 0x00 },
+ new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01 },
+ new byte[] { 0x00, 0x03, 0x00, 0x02, 0x01, 0x02, 0x03 },
+ new byte[] { 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x03 },
+
+ // Invalid SvcParamValue for "ipv4hint":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue must be multiple of 4.
+ new byte[] { 0x00, 0x04, 0x00, 0x00 },
+ new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08 },
+ new byte[] { 0x00, 0x04, 0x00, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08 },
+ new byte[] { 0x00, 0x04, 0x00, 0x05, 0x08, 0x08, 0x08, 0x08 },
+
+ // Invalid SvcParamValue for "ipv6hint":
+ // - SvcParamValue must not be empty.
+ // - SvcParamValue has less data than expected.
+ // - SvcParamValue has more data than expected.
+ // - SvcParamValue must be multiple of 16.
+ new byte[] { 0x00, 0x06, 0x00, 0x00 },
+ new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01 },
+ new byte[] { 0x00, 0x06, 0x00, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17 },
+ new byte[] { 0x00, 0x06, 0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 }
+ );
+
+ for (byte[] content : invalidContents) {
+ final byte[] bytes = new TestDnsRecordByteArrayBuilder()
+ .setRRType(TYPE_SVCB)
+ .addRdata(content)
+ .build();
+ assertThrows(DnsPacket.ParseException.class,
+ () -> makeDnsSvcbRecordFromByteArray(bytes));
+ }
+ }
+
+ @Test
+ public void testDnsSvcbPacket_createdFromNullObject() throws Exception {
+ assertThrows(DnsPacket.ParseException.class, () -> DnsSvcbPacket.fromResponse(null));
+ }
+
+ @Test
+ public void testDnsSvcbPacket() throws Exception {
+ final String dohTargetName = "https.dns.com";
+ final String doqTargetName = "doq.dns.com";
+ final InetAddress[] expectedIpAddressesForHttps = new InetAddress[] {
+ InetAddresses.parseNumericAddress("1.2.3.4"),
+ InetAddresses.parseNumericAddress("6.7.8.9"),
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ };
+ final InetAddress[] expectedIpAddressesForDoq = new InetAddress[] {
+ InetAddresses.parseNumericAddress("4.3.2.1"),
+ };
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 2 /* ancount */, 0 /* nscount */,
+ 0 /* arcount */));
+ os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+ // Add answer for alpn h2 and http/1.1.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName(dohTargetName)
+ .addRdata(TEST_SVC_PARAM_ALPN_HTTPS)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_1)
+ .addRdata(TEST_SVC_PARAM_IPV6HINT)
+ .addRdata(TEST_SVC_PARAM_PORT)
+ .addRdata(TEST_SVC_PARAM_DOHPATH)
+ .build());
+ // Add answer for alpn doq.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName(doqTargetName)
+ .setSvcPriority(2)
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_2)
+ .build());
+ final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+ assertTrue(pkt.isSupported("http/1.1"));
+ assertTrue(pkt.isSupported("h2"));
+ assertTrue(pkt.isSupported("doq"));
+ assertFalse(pkt.isSupported("http"));
+ assertFalse(pkt.isSupported("h3"));
+ assertFalse(pkt.isSupported(""));
+
+ assertEquals(dohTargetName, pkt.getTargetName("http/1.1"));
+ assertEquals(dohTargetName, pkt.getTargetName("h2"));
+ assertEquals(doqTargetName, pkt.getTargetName("doq"));
+ assertEquals(null, pkt.getTargetName("http"));
+ assertEquals(null, pkt.getTargetName("h3"));
+ assertEquals(null, pkt.getTargetName(""));
+
+ assertEquals(5353, pkt.getPort("http/1.1"));
+ assertEquals(5353, pkt.getPort("h2"));
+ assertEquals(-1, pkt.getPort("doq"));
+ assertEquals(-1, pkt.getPort("http"));
+ assertEquals(-1, pkt.getPort("h3"));
+ assertEquals(-1, pkt.getPort(""));
+
+ assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("http/1.1").toArray());
+ assertArrayEquals(expectedIpAddressesForHttps, pkt.getAddresses("h2").toArray());
+ assertArrayEquals(expectedIpAddressesForDoq, pkt.getAddresses("doq").toArray());
+ assertTrue(pkt.getAddresses("http").isEmpty());
+ assertTrue(pkt.getAddresses("h3").isEmpty());
+ assertTrue(pkt.getAddresses("").isEmpty());
+
+ assertEquals("/some-path{?dns}", pkt.getDohPath("http/1.1"));
+ assertEquals("/some-path{?dns}", pkt.getDohPath("h2"));
+ assertEquals("", pkt.getDohPath("doq"));
+ assertEquals(null, pkt.getDohPath("http"));
+ assertEquals(null, pkt.getDohPath("h3"));
+ assertEquals(null, pkt.getDohPath(""));
+ }
+
+ @Test
+ public void testDnsSvcbPacket_noIpHint() throws Exception {
+ final String targetName = "doq.dns.com";
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
+ 0 /* arcount */));
+ os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+ // Add answer for alpn doq.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName(targetName)
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .build());
+ final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+ assertTrue(pkt.isSupported("doq"));
+ assertEquals(targetName, pkt.getTargetName("doq"));
+ assertEquals(-1, pkt.getPort("doq"));
+ assertArrayEquals(new InetAddress[] {}, pkt.getAddresses("doq").toArray());
+ assertEquals("", pkt.getDohPath("doq"));
+ }
+
+ @Test
+ public void testDnsSvcbPacket_hasAnswerInAdditionalSection() throws Exception {
+ final InetAddress[] expectedIpAddresses = new InetAddress[] {
+ InetAddresses.parseNumericAddress("1.2.3.4"),
+ InetAddresses.parseNumericAddress("2001:db8::2"),
+ };
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.write(makeDnsResponseHeaderAsByteArray(1 /* qdcount */, 1 /* ancount */, 0 /* nscount */,
+ 2 /* arcount */));
+ os.write(TEST_DNS_SVCB_QUESTION_SECTION);
+ // Add SVCB record in the Answer section.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_SVCB)
+ .setTargetName("doq.dns.com")
+ .addRdata(TEST_SVC_PARAM_ALPN_DOQ)
+ .addRdata(TEST_SVC_PARAM_IPV4HINT_2)
+ .addRdata(TEST_SVC_PARAM_IPV6HINT)
+ .build());
+ // Add A/AAAA records in the Additional section.
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_A)
+ .addRdata(InetAddresses.parseNumericAddress("1.2.3.4").getAddress())
+ .build());
+ os.write(new TestDnsRecordByteArrayBuilder()
+ .setNameCompression(true)
+ .setRRType(TYPE_AAAA)
+ .addRdata(InetAddresses.parseNumericAddress("2001:db8::2").getAddress())
+ .build());
+ final DnsSvcbPacket pkt = DnsSvcbPacket.fromResponse(os.toByteArray());
+
+ // If there are A/AAAA records in the Additional section, getAddresses() returns the IP
+ // addresses in those records instead of the IP addresses in ipv4hint/ipv6hint.
+ assertArrayEquals(expectedIpAddresses, pkt.getAddresses("doq").toArray());
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
index bb2b933..66427fc 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -18,6 +18,7 @@
import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -30,6 +31,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -92,4 +94,17 @@
assertEquals(localAddrStr + "%" + scopeId, updatedLocalAddr.getHostAddress());
assertEquals(scopeId, updatedLocalAddr.getScopeId());
}
+
+ @Test
+ public void testV4MappedV6Address() throws Exception {
+ final Inet4Address v4Addr = (Inet4Address) InetAddress.getByName("192.0.2.1");
+ final Inet6Address v4MappedV6Address = InetAddressUtils.v4MappedV6Address(v4Addr);
+ final byte[] expectedAddrBytes = new byte[]{
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+ };
+ assertArrayEquals(expectedAddrBytes, v4MappedV6Address.getAddress());
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index b4da043..a39b7a3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -765,6 +765,14 @@
msg.writeToBytes(ByteOrder.BIG_ENDIAN));
}
+ @Test
+ public void testV4MappedV6Address() {
+ final IpAddressMessage msg = doParsingMessageTest("c0a86401"
+ + "00000000000000000000ffffc0a86401", IpAddressMessage.class, ByteOrder.BIG_ENDIAN);
+ assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
+ assertEquals(InetAddressUtils.v4MappedV6Address(TEST_IPV4_ADDRESS), msg.ipv6Address);
+ }
+
public static class WrongIpAddressType extends Struct {
@Field(order = 0, type = Type.Ipv4Address) public byte[] ipv4Address;
@Field(order = 1, type = Type.Ipv6Address) public byte[] ipv6Address;
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 5e9b004..5a231fc 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -68,7 +68,7 @@
final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
assertNotNull(fd);
- NetlinkUtils.connectSocketToNetlink(fd);
+ NetlinkUtils.connectToKernel(fd);
final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
assertNotNull(localAddr);
@@ -153,7 +153,7 @@
final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
assertNotNull(fd);
- NetlinkUtils.connectSocketToNetlink(fd);
+ NetlinkUtils.connectToKernel(fd);
final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
assertNotNull(localAddr);
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java
new file mode 100644
index 0000000..52fd591
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/xfrm/StructXfrmUsersaIdTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.netlink.xfrm;
+
+import static com.android.net.module.util.netlink.xfrm.XfrmNetlinkMessage.IPPROTO_ESP;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.net.InetAddresses;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructXfrmUsersaIdTest {
+ private static final String EXPECTED_HEX_STRING =
+ "C0000201000000000000000000000000" + "7768440002003200";
+ private static final byte[] EXPECTED_HEX = HexDump.hexStringToByteArray(EXPECTED_HEX_STRING);
+
+ private static final InetAddress DEST_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+ private static final long SPI = 0x77684400;
+ private static final int FAMILY = OsConstants.AF_INET;
+ private static final short PROTO = IPPROTO_ESP;
+
+ @Test
+ public void testEncode() throws Exception {
+ final StructXfrmUsersaId struct = new StructXfrmUsersaId(DEST_ADDRESS, SPI, FAMILY, PROTO);
+
+ ByteBuffer buffer = ByteBuffer.allocate(EXPECTED_HEX.length);
+ buffer.order(ByteOrder.nativeOrder());
+ struct.writeToByteBuffer(buffer);
+
+ assertArrayEquals(EXPECTED_HEX, buffer.array());
+ }
+
+ @Test
+ public void testDecode() throws Exception {
+ final ByteBuffer buffer = ByteBuffer.wrap(EXPECTED_HEX);
+ buffer.order(ByteOrder.nativeOrder());
+
+ final StructXfrmUsersaId struct =
+ StructXfrmUsersaId.parse(StructXfrmUsersaId.class, buffer);
+
+ assertEquals(DEST_ADDRESS, struct.getDestAddress());
+ assertEquals(SPI, struct.spi);
+ assertEquals(FAMILY, struct.family);
+ assertEquals(PROTO, struct.proto);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
index 0f6fa48..440b836 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -27,7 +27,7 @@
import org.junit.runners.JUnit4
private const val ATTEMPTS = 50 // Causes testWaitForIdle to take about 150ms on aosp_crosshatch-eng
-private const val TIMEOUT_MS = 200
+private const val TIMEOUT_MS = 1000
@RunWith(JUnit4::class)
class HandlerUtilsTest {
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 5fe7ac3..a5e5afb 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -38,6 +38,7 @@
"net-utils-device-common",
"net-utils-device-common-async",
"net-utils-device-common-netlink",
+ "net-utils-device-common-struct",
"net-utils-device-common-wear",
"modules-utils-build_system",
],
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index d75d9ca..df6067d 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -61,8 +61,8 @@
commonError)
}
assertTrue(tm.isDataConnectivityPossible,
- "The device is not setup with a SIM card that supports data connectivity. " +
- commonError)
+ "The device has a SIM card, but it does not supports data connectivity. " +
+ "Check the data plan, and verify that mobile data is working. " + commonError)
connectUtil.ensureCellularValidated()
}
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
index 35f22b9..46229b0 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRule.kt
@@ -27,6 +27,9 @@
@Deprecated("Use Build.VERSION_CODES", ReplaceWith("Build.VERSION_CODES.S_V2"))
const val SC_V2 = Build.VERSION_CODES.S_V2
+// TODO: Remove this when Build.VERSION_CODES.VANILLA_ICE_CREAM is available in all branches
+// where this code builds
+const val VANILLA_ICE_CREAM = 35 // Bui1ld.VERSION_CODES.VANILLA_ICE_CREAM
private val MAX_TARGET_SDK_ANNOTATION_RE = Pattern.compile("MaxTargetSdk([0-9]+)$")
private val targetSdk = InstrumentationRegistry.getContext().applicationInfo.targetSdkVersion
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
index 2d281fd..10accd4 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DevSdkIgnoreRunner.kt
@@ -17,6 +17,7 @@
package com.android.testutils
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import java.lang.reflect.Modifier
@@ -27,6 +28,7 @@
import org.junit.runner.manipulation.NoTestsRemainException
import org.junit.runner.manipulation.Sortable
import org.junit.runner.manipulation.Sorter
+import org.junit.runner.notification.Failure
import org.junit.runner.notification.RunNotifier
import org.junit.runners.Parameterized
@@ -52,6 +54,9 @@
* class MyTestClass { ... }
*/
class DevSdkIgnoreRunner(private val klass: Class<*>) : Runner(), Filterable, Sortable {
+ private val leakMonitorDesc = Description.createTestDescription(klass, "ThreadLeakMonitor")
+ private val shouldThreadLeakFailTest = klass.isAnnotationPresent(MonitorThreadLeak::class.java)
+
// Inference correctly infers Runner & Filterable & Sortable for |baseRunner|, but the
// Java bytecode doesn't have a way to express this. Give this type a name by wrapping it.
private class RunnerWrapper<T>(private val wrapped: T) :
@@ -61,6 +66,10 @@
override fun run(notifier: RunNotifier?) = wrapped.run(notifier)
}
+ // Annotation for test classes to indicate the test runner should monitor thread leak.
+ // TODO(b/307693729): Remove this annotation and monitor thread leak by default.
+ annotation class MonitorThreadLeak
+
private val baseRunner: RunnerWrapper<*>? = klass.let {
val ignoreAfter = it.getAnnotation(IgnoreAfter::class.java)
val ignoreUpTo = it.getAnnotation(IgnoreUpTo::class.java)
@@ -81,20 +90,65 @@
it.isAnnotationPresent(Parameterized.Parameters::class.java) }
override fun run(notifier: RunNotifier) {
- if (baseRunner != null) {
+ if (baseRunner == null) {
+ // Report a single, skipped placeholder test for this class, as the class is expected to
+ // report results when run. In practice runners that apply the Filterable implementation
+ // would see a NoTestsRemainException and not call the run method.
+ notifier.fireTestIgnored(
+ Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ return
+ }
+ if (!shouldThreadLeakFailTest) {
baseRunner.run(notifier)
return
}
- // Report a single, skipped placeholder test for this class, as the class is expected to
- // report results when run. In practice runners that apply the Filterable implementation
- // would see a NoTestsRemainException and not call the run method.
- notifier.fireTestIgnored(
- Description.createTestDescription(klass, "skippedClassForDevSdkMismatch"))
+ // Dump threads as a baseline to monitor thread leaks.
+ val threadCountsBeforeTest = getAllThreadNameCounts()
+
+ baseRunner.run(notifier)
+
+ notifier.fireTestStarted(leakMonitorDesc)
+ val threadCountsAfterTest = getAllThreadNameCounts()
+ // TODO : move CompareOrUpdateResult to its own util instead of LinkProperties.
+ val threadsDiff = CompareOrUpdateResult(
+ threadCountsBeforeTest.entries,
+ threadCountsAfterTest.entries
+ ) { it.key }
+ // Ignore removed threads, which typically are generated by previous tests.
+ // Because this is in the threadsDiff.updated member, for sure there is a
+ // corresponding key in threadCountsBeforeTest.
+ val increasedThreads = threadsDiff.updated
+ .filter { threadCountsBeforeTest[it.key]!! < it.value }
+ if (threadsDiff.added.isNotEmpty() || increasedThreads.isNotEmpty()) {
+ notifier.fireTestFailure(Failure(leakMonitorDesc,
+ IllegalStateException("Unexpected thread changes: $threadsDiff")))
+ }
+ notifier.fireTestFinished(leakMonitorDesc)
+ }
+
+ private fun getAllThreadNameCounts(): Map<String, Int> {
+ // Get the counts of threads in the group per name.
+ // Filter system thread groups.
+ // Also ignore threads with 1 count, this effectively filtered out threads created by the
+ // test runner or other system components. e.g. hwuiTask*, queued-work-looper,
+ // SurfaceSyncGroupTimer, RenderThread, Time-limited test, etc.
+ return Thread.getAllStackTraces().keys
+ .filter { it.threadGroup?.name != "system" }
+ .groupingBy { it.name }.eachCount()
+ .filter { it.value != 1 }
}
override fun getDescription(): Description {
- return baseRunner?.description ?: Description.createSuiteDescription(klass)
+ if (baseRunner == null) {
+ return Description.createSuiteDescription(klass)
+ }
+
+ return baseRunner.description.also {
+ if (shouldThreadLeakFailTest) {
+ it.addChild(leakMonitorDesc)
+ }
+ }
}
/**
@@ -102,7 +156,9 @@
*/
override fun testCount(): Int {
// When ignoring the tests, a skipped placeholder test is reported, so test count is 1.
- return baseRunner?.testCount() ?: 1
+ if (baseRunner == null) return 1
+
+ return baseRunner.testCount() + if (shouldThreadLeakFailTest) 1 else 0
}
@Throws(NoTestsRemainException::class)
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
index 733bd98..70f20d6 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestBpfMap.java
@@ -43,6 +43,8 @@
public class TestBpfMap<K extends Struct, V extends Struct> implements IBpfMap<K, V> {
private final ConcurrentHashMap<K, V> mMap = new ConcurrentHashMap<>();
+ public TestBpfMap() {}
+
// TODO: Remove this constructor
public TestBpfMap(final Class<K> key, final Class<V> value) {
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index df9c61a..05c0444 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -18,6 +18,7 @@
import android.net.ConnectivityManager.NetworkCallback
import android.net.LinkProperties
+import android.net.LocalNetworkInfo
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
@@ -28,6 +29,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatusInt
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
@@ -68,6 +70,10 @@
override val network: Network,
val lp: LinkProperties
) : CallbackEntry()
+ data class LocalInfoChanged(
+ override val network: Network,
+ val info: LocalNetworkInfo
+ ) : CallbackEntry()
data class Suspended(override val network: Network) : CallbackEntry()
data class Resumed(override val network: Network) : CallbackEntry()
data class Losing(override val network: Network, val maxMsToLive: Int) : CallbackEntry()
@@ -94,6 +100,8 @@
@JvmField
val LINK_PROPERTIES_CHANGED = LinkPropertiesChanged::class
@JvmField
+ val LOCAL_INFO_CHANGED = LocalInfoChanged::class
+ @JvmField
val SUSPENDED = Suspended::class
@JvmField
val RESUMED = Resumed::class
@@ -131,6 +139,11 @@
history.add(LinkPropertiesChanged(network, lp))
}
+ override fun onLocalNetworkInfoChanged(network: Network, info: LocalNetworkInfo) {
+ Log.d(TAG, "onLocalNetworkInfoChanged $network $info")
+ history.add(LocalInfoChanged(network, info))
+ }
+
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
Log.d(TAG, "onBlockedStatusChanged $network $blocked")
history.add(BlockedStatus(network, blocked))
@@ -430,37 +443,63 @@
suspended: Boolean = false,
validated: Boolean? = true,
blocked: Boolean = false,
+ upstream: Network? = null,
tmt: Long = defaultTimeoutMs
) {
- expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+ expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
}
+ // For backward compatibility, add a method that allows callers to specify a timeout but
+ // no upstream.
+ fun expectAvailableCallbacks(
+ net: Network,
+ suspended: Boolean = false,
+ validated: Boolean? = true,
+ blocked: Boolean = false,
+ tmt: Long = defaultTimeoutMs
+ ) = expectAvailableCallbacks(net, suspended, validated, blocked, upstream = null, tmt = tmt)
+
fun expectAvailableCallbacks(
net: Network,
suspended: Boolean,
validated: Boolean,
blockedReason: Int,
+ upstream: Network? = null,
tmt: Long
) {
- expectAvailableCallbacksCommon(net, suspended, validated, tmt)
+ expectAvailableCallbacksCommon(net, suspended, validated, upstream, tmt)
expect<BlockedStatusInt>(net) { it.reason == blockedReason }
}
+ // For backward compatibility, add a method that allows callers to specify a timeout but
+ // no upstream.
+ fun expectAvailableCallbacks(
+ net: Network,
+ suspended: Boolean = false,
+ validated: Boolean = true,
+ blockedReason: Int,
+ tmt: Long = defaultTimeoutMs
+ ) = expectAvailableCallbacks(net, suspended, validated, blockedReason, upstream = null, tmt)
+
private fun expectAvailableCallbacksCommon(
net: Network,
suspended: Boolean,
validated: Boolean?,
+ upstream: Network?,
tmt: Long
) {
expect<Available>(net, tmt)
if (suspended) {
expect<Suspended>(net, tmt)
}
- expect<CapabilitiesChanged>(net, tmt) {
+ val caps = expect<CapabilitiesChanged>(net, tmt) {
validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
- }
+ }.caps
expect<LinkPropertiesChanged>(net, tmt)
+ if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)) {
+ expect<LocalInfoChanged>(net, tmt) { it.info.upstreamNetwork == upstream }
+ }
}
// Backward compatibility for existing Java code. Use named arguments instead and remove all
@@ -507,13 +546,15 @@
val network: Network
}
+ @JvmOverloads
fun expectAvailableCallbacks(
n: HasNetwork,
suspended: Boolean,
validated: Boolean,
blocked: Boolean,
+ upstream: Network? = null,
timeoutMs: Long
- ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, timeoutMs)
+ ) = expectAvailableCallbacks(n.network, suspended, validated, blocked, upstream, timeoutMs)
fun expectAvailableAndSuspendedCallbacks(n: HasNetwork, expectValidated: Boolean) {
expectAvailableAndSuspendedCallbacks(n.network, expectValidated)
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
index eb94781..600a623 100644
--- a/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityTestTargetPreparer.kt
@@ -128,7 +128,7 @@
if (testInfo.device.getApiLevel() < 31) return
testInfo.exec("cmd connectivity set-chain3-enabled $enableChain")
enablePkgs.forEach { (pkg, allow) ->
- testInfo.exec("cmd connectivity set-package-networking-enabled $pkg $allow")
+ testInfo.exec("cmd connectivity set-package-networking-enabled $allow $pkg")
}
}
diff --git a/tests/benchmark/Android.bp b/tests/benchmark/Android.bp
index 77383ad..6ea5347 100644
--- a/tests/benchmark/Android.bp
+++ b/tests/benchmark/Android.bp
@@ -29,6 +29,7 @@
"src/**/*.kt",
"src/**/*.aidl",
],
+ asset_dirs: ["assets"],
static_libs: [
"androidx.test.rules",
"mockito-target-minus-junit4",
diff --git a/tests/benchmark/assets/dataset/A052701.zip b/tests/benchmark/assets/dataset/A052701.zip
new file mode 100644
index 0000000..fdde1ad
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052701.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052801.zip b/tests/benchmark/assets/dataset/A052801.zip
new file mode 100644
index 0000000..7f908b7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052801.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052802.zip b/tests/benchmark/assets/dataset/A052802.zip
new file mode 100644
index 0000000..180ad3e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052802.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052803.zip b/tests/benchmark/assets/dataset/A052803.zip
new file mode 100644
index 0000000..321a79b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052803.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052804.zip b/tests/benchmark/assets/dataset/A052804.zip
new file mode 100644
index 0000000..298ec04
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052804.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052901.zip b/tests/benchmark/assets/dataset/A052901.zip
new file mode 100644
index 0000000..0f49543
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052901.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A052902.zip b/tests/benchmark/assets/dataset/A052902.zip
new file mode 100644
index 0000000..ec22456
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A052902.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053001.zip b/tests/benchmark/assets/dataset/A053001.zip
new file mode 100644
index 0000000..ad5d82e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053002.zip b/tests/benchmark/assets/dataset/A053002.zip
new file mode 100644
index 0000000..8a4bb0c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053003.zip b/tests/benchmark/assets/dataset/A053003.zip
new file mode 100644
index 0000000..24d2057
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053003.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053004.zip b/tests/benchmark/assets/dataset/A053004.zip
new file mode 100644
index 0000000..352f93f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053004.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053005.zip b/tests/benchmark/assets/dataset/A053005.zip
new file mode 100644
index 0000000..2b49a1b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053005.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053006.zip b/tests/benchmark/assets/dataset/A053006.zip
new file mode 100644
index 0000000..a59f2ec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053006.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053007.zip b/tests/benchmark/assets/dataset/A053007.zip
new file mode 100644
index 0000000..df7ae74
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053007.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053101.zip b/tests/benchmark/assets/dataset/A053101.zip
new file mode 100644
index 0000000..c10ed64
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053102.zip b/tests/benchmark/assets/dataset/A053102.zip
new file mode 100644
index 0000000..8c9f9cf
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053103.zip b/tests/benchmark/assets/dataset/A053103.zip
new file mode 100644
index 0000000..9202c50
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A053104.zip b/tests/benchmark/assets/dataset/A053104.zip
new file mode 100644
index 0000000..3c77724
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A053104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060101.zip b/tests/benchmark/assets/dataset/A060101.zip
new file mode 100644
index 0000000..86443a7
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060102.zip b/tests/benchmark/assets/dataset/A060102.zip
new file mode 100644
index 0000000..4f2cf49
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060201.zip b/tests/benchmark/assets/dataset/A060201.zip
new file mode 100644
index 0000000..3c28bec
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/A060202.zip b/tests/benchmark/assets/dataset/A060202.zip
new file mode 100644
index 0000000..e39e493
--- /dev/null
+++ b/tests/benchmark/assets/dataset/A060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053001.zip b/tests/benchmark/assets/dataset/B053001.zip
new file mode 100644
index 0000000..8408744
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053001.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B053002.zip b/tests/benchmark/assets/dataset/B053002.zip
new file mode 100644
index 0000000..5245f70
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B053002.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060101.zip b/tests/benchmark/assets/dataset/B060101.zip
new file mode 100644
index 0000000..242c0d1
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060201.zip b/tests/benchmark/assets/dataset/B060201.zip
new file mode 100644
index 0000000..29df25a
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060201.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060202.zip b/tests/benchmark/assets/dataset/B060202.zip
new file mode 100644
index 0000000..bda9edd
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060202.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060203.zip b/tests/benchmark/assets/dataset/B060203.zip
new file mode 100644
index 0000000..b9fccfe
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060203.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060204.zip b/tests/benchmark/assets/dataset/B060204.zip
new file mode 100644
index 0000000..66227d2
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060204.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060205.zip b/tests/benchmark/assets/dataset/B060205.zip
new file mode 100644
index 0000000..6aaa06b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060205.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060206.zip b/tests/benchmark/assets/dataset/B060206.zip
new file mode 100644
index 0000000..18445b0
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060206.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/B060207.zip b/tests/benchmark/assets/dataset/B060207.zip
new file mode 100644
index 0000000..20f7c5b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/B060207.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060101.zip b/tests/benchmark/assets/dataset/C060101.zip
new file mode 100644
index 0000000..0b1c29f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060101.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060102.zip b/tests/benchmark/assets/dataset/C060102.zip
new file mode 100644
index 0000000..8064905
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060102.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060103.zip b/tests/benchmark/assets/dataset/C060103.zip
new file mode 100644
index 0000000..d0e819f
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060103.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060104.zip b/tests/benchmark/assets/dataset/C060104.zip
new file mode 100644
index 0000000..f87ca8d
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060104.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060105.zip b/tests/benchmark/assets/dataset/C060105.zip
new file mode 100644
index 0000000..e869895
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060105.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060106.zip b/tests/benchmark/assets/dataset/C060106.zip
new file mode 100644
index 0000000..6d25a98
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060106.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060107.zip b/tests/benchmark/assets/dataset/C060107.zip
new file mode 100644
index 0000000..a7cb31c
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060107.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060108.zip b/tests/benchmark/assets/dataset/C060108.zip
new file mode 100644
index 0000000..c1a5898
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060108.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060109.zip b/tests/benchmark/assets/dataset/C060109.zip
new file mode 100644
index 0000000..bb9116e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060109.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060110.zip b/tests/benchmark/assets/dataset/C060110.zip
new file mode 100644
index 0000000..5ca0f96
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060110.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060111.zip b/tests/benchmark/assets/dataset/C060111.zip
new file mode 100644
index 0000000..6a12d7e
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060111.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060112.zip b/tests/benchmark/assets/dataset/C060112.zip
new file mode 100644
index 0000000..fa2c30b
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060112.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060113.zip b/tests/benchmark/assets/dataset/C060113.zip
new file mode 100644
index 0000000..63a34ba
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060113.zip
Binary files differ
diff --git a/tests/benchmark/assets/dataset/C060114.zip b/tests/benchmark/assets/dataset/C060114.zip
new file mode 100644
index 0000000..bd60927
--- /dev/null
+++ b/tests/benchmark/assets/dataset/C060114.zip
Binary files differ
diff --git a/tests/benchmark/res/raw/netstats-many-uids-zip b/tests/benchmark/assets/dataset/netstats-many-uids.zip
similarity index 98%
rename from tests/benchmark/res/raw/netstats-many-uids-zip
rename to tests/benchmark/assets/dataset/netstats-many-uids.zip
index 22e8254..9554aaa 100644
--- a/tests/benchmark/res/raw/netstats-many-uids-zip
+++ b/tests/benchmark/assets/dataset/netstats-many-uids.zip
Binary files differ
diff --git a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
index e80548b..585157f 100644
--- a/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
+++ b/tests/benchmark/src/android/net/netstats/benchmarktests/NetworkStatsTest.kt
@@ -20,10 +20,9 @@
import android.net.NetworkStatsCollection
import android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID
import android.os.DropBoxManager
-import androidx.test.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.util.FileRotator
import com.android.internal.util.FileRotator.Reader
-import com.android.server.connectivity.benchmarktests.R
import com.android.server.net.NetworkStatsRecorder
import java.io.BufferedInputStream
import java.io.DataInputStream
@@ -44,23 +43,22 @@
companion object {
private val DEFAULT_BUFFER_SIZE = 8192
private val FILE_CACHE_WARM_UP_REPEAT_COUNT = 10
- private val TEST_REPEAT_COUNT = 10
private val UID_COLLECTION_BUCKET_DURATION_MS = TimeUnit.HOURS.toMillis(2)
private val UID_RECORDER_ROTATE_AGE_MS = TimeUnit.DAYS.toMillis(15)
private val UID_RECORDER_DELETE_AGE_MS = TimeUnit.DAYS.toMillis(90)
+ private val TEST_DATASET_SUBFOLDER = "dataset/"
- private val testFilesDir by lazy {
- // These file generated by using real user dataset which has many uid records
- // and agreed to share the dataset for testing purpose. These dataset can be
- // extracted from rooted devices by using
- // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
- val zipInputStream =
- ZipInputStream(getInputStreamForResource(R.raw.netstats_many_uids_zip))
- unzipToTempDir(zipInputStream)
- }
-
- private val uidTestFiles: List<File> by lazy {
- getSortedListForPrefix(testFilesDir, "uid")
+ // These files are generated by using real user dataset which has many uid records
+ // and agreed to share the dataset for testing purpose. These dataset can be
+ // extracted from rooted devices by using
+ // "adb pull /data/misc/apexdata/com.android.tethering/netstats" command.
+ private val testFilesAssets by lazy {
+ val zipFiles = context.assets.list(TEST_DATASET_SUBFOLDER)!!.asList()
+ zipFiles.map {
+ val zipInputStream =
+ ZipInputStream((TEST_DATASET_SUBFOLDER + it).toAssetInputStream())
+ File(unzipToTempDir(zipInputStream), "netstats")
+ }
}
// Test results shows the test cases who read the file first will take longer time to
@@ -72,24 +70,34 @@
@BeforeClass
fun setUpOnce() {
repeat(FILE_CACHE_WARM_UP_REPEAT_COUNT) {
- val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
- for (file in uidTestFiles) {
- readFile(file, collection)
+ testFilesAssets.forEach {
+ val uidTestFiles = getSortedListForPrefix(it, "uid")
+ val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
+ for (file in uidTestFiles) {
+ readFile(file, collection)
+ }
}
}
}
- private fun getInputStreamForResource(resourceId: Int): DataInputStream =
- DataInputStream(
- InstrumentationRegistry.getContext()
- .getResources().openRawResource(resourceId)
- )
+ val context get() = InstrumentationRegistry.getInstrumentation().getContext()
+ private fun String.toAssetInputStream() = DataInputStream(context.assets.open(this))
private fun unzipToTempDir(zis: ZipInputStream): File {
val statsDir =
Files.createTempDirectory(NetworkStatsTest::class.simpleName).toFile()
generateSequence { zis.nextEntry }.forEach { entry ->
- FileOutputStream(File(statsDir, entry.name)).use {
+ val entryFile = File(statsDir, entry.name)
+ if (entry.isDirectory) {
+ entryFile.mkdirs()
+ return@forEach
+ }
+
+ // Make sure all folders exists. There is no guarantee anywhere.
+ entryFile.parentFile!!.mkdirs()
+
+ // If the entry is a file extract it.
+ FileOutputStream(entryFile).use {
zis.copyTo(it, DEFAULT_BUFFER_SIZE)
}
}
@@ -99,7 +107,7 @@
// List [xt|uid|uid_tag].<start>-<end> files under the given directory.
private fun getSortedListForPrefix(statsDir: File, prefix: String): List<File> {
assertTrue(statsDir.exists())
- return statsDir.list() { dir, name -> name.startsWith("$prefix.") }
+ return statsDir.list { _, name -> name.startsWith("$prefix.") }
.orEmpty()
.map { it -> File(statsDir, it) }
.sorted()
@@ -115,7 +123,8 @@
fun testReadCollection_manyUids() {
// The file cache is warmed up by the @BeforeClass method, so now the test can repeat
// this a number of time to have a stable number.
- repeat(TEST_REPEAT_COUNT) {
+ testFilesAssets.forEach {
+ val uidTestFiles = getSortedListForPrefix(it, "uid")
val collection = NetworkStatsCollection(UID_COLLECTION_BUCKET_DURATION_MS)
for (file in uidTestFiles) {
readFile(file, collection)
@@ -127,10 +136,10 @@
fun testReadFromRecorder_manyUids() {
val mockObserver = mock<NonMonotonicObserver<String>>()
val mockDropBox = mock<DropBoxManager>()
- repeat(TEST_REPEAT_COUNT) {
+ testFilesAssets.forEach {
val recorder = NetworkStatsRecorder(
FileRotator(
- testFilesDir, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
+ it, PREFIX_UID, UID_RECORDER_ROTATE_AGE_MS, UID_RECORDER_DELETE_AGE_MS
),
mockObserver,
mockDropBox,
diff --git a/tests/common/java/android/net/CaptivePortalDataTest.kt b/tests/common/java/android/net/CaptivePortalDataTest.kt
index f927380..67a523c 100644
--- a/tests/common/java/android/net/CaptivePortalDataTest.kt
+++ b/tests/common/java/android/net/CaptivePortalDataTest.kt
@@ -19,21 +19,20 @@
import android.os.Build
import androidx.test.filters.SmallTest
import com.android.modules.utils.build.SdkLevel
-import com.android.testutils.assertParcelingIsLossless
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
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()
diff --git a/tests/common/java/android/net/KeepalivePacketDataTest.kt b/tests/common/java/android/net/KeepalivePacketDataTest.kt
index 403d6b5..97a45fc 100644
--- a/tests/common/java/android/net/KeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/KeepalivePacketDataTest.kt
@@ -17,27 +17,20 @@
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 com.android.testutils.NonNullTestUtils
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
@@ -60,7 +53,6 @@
NonNullTestUtils.nullUnsafe(dstAddress), dstPort, data)
@Test
- @IgnoreUpTo(Build.VERSION_CODES.Q)
fun testConstructor() {
try {
TestKeepalivePacketData(srcAddress = null)
@@ -99,22 +91,17 @@
}
@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))
}
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index d2e7c99..8f14572 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -134,13 +134,10 @@
assertFalse(lp.isIpv4Provisioned());
assertFalse(lp.isIpv6Provisioned());
assertFalse(lp.isPrivateDnsActive());
-
- if (SdkLevel.isAtLeastR()) {
- assertNull(lp.getDhcpServerAddress());
- assertFalse(lp.isWakeOnLanSupported());
- assertNull(lp.getCaptivePortalApiUrl());
- assertNull(lp.getCaptivePortalData());
- }
+ assertNull(lp.getDhcpServerAddress());
+ assertFalse(lp.isWakeOnLanSupported());
+ assertNull(lp.getCaptivePortalApiUrl());
+ assertNull(lp.getCaptivePortalData());
}
private LinkProperties makeTestObject() {
@@ -162,12 +159,10 @@
lp.setMtu(MTU);
lp.setTcpBufferSizes(TCP_BUFFER_SIZES);
lp.setNat64Prefix(new IpPrefix("2001:db8:0:64::/96"));
- if (SdkLevel.isAtLeastR()) {
- lp.setDhcpServerAddress(DHCPSERVER);
- lp.setWakeOnLanSupported(true);
- lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
- lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
- }
+ lp.setDhcpServerAddress(DHCPSERVER);
+ lp.setWakeOnLanSupported(true);
+ lp.setCaptivePortalApiUrl(CAPPORT_API_URL);
+ lp.setCaptivePortalData((CaptivePortalData) getCaptivePortalData());
return lp;
}
@@ -206,19 +201,17 @@
assertTrue(source.isIdenticalTcpBufferSizes(target));
assertTrue(target.isIdenticalTcpBufferSizes(source));
- if (SdkLevel.isAtLeastR()) {
- assertTrue(source.isIdenticalDhcpServerAddress(target));
- assertTrue(source.isIdenticalDhcpServerAddress(source));
+ assertTrue(source.isIdenticalDhcpServerAddress(target));
+ assertTrue(source.isIdenticalDhcpServerAddress(source));
- assertTrue(source.isIdenticalWakeOnLan(target));
- assertTrue(target.isIdenticalWakeOnLan(source));
+ assertTrue(source.isIdenticalWakeOnLan(target));
+ assertTrue(target.isIdenticalWakeOnLan(source));
- assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
- assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
+ assertTrue(source.isIdenticalCaptivePortalApiUrl(target));
+ assertTrue(target.isIdenticalCaptivePortalApiUrl(source));
- assertTrue(source.isIdenticalCaptivePortalData(target));
- assertTrue(target.isIdenticalCaptivePortalData(source));
- }
+ assertTrue(source.isIdenticalCaptivePortalData(target));
+ assertTrue(target.isIdenticalCaptivePortalData(source));
// Check result of equals().
assertTrue(source.equals(target));
@@ -1017,7 +1010,7 @@
assertParcelingIsLossless(source);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testLinkPropertiesParcelable() throws Exception {
final LinkProperties source = makeLinkPropertiesForParceling();
@@ -1035,7 +1028,7 @@
}
// Parceling of the scope was broken until Q-QPR2
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testLinkLocalDnsServerParceling() throws Exception {
final String strAddress = "fe80::1%lo";
final LinkProperties lp = new LinkProperties();
@@ -1158,7 +1151,7 @@
assertFalse(lp.isPrivateDnsActive());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testDhcpServerAddress() {
final LinkProperties lp = makeTestObject();
assertEquals(DHCPSERVER, lp.getDhcpServerAddress());
@@ -1167,7 +1160,7 @@
assertNull(lp.getDhcpServerAddress());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testWakeOnLanSupported() {
final LinkProperties lp = makeTestObject();
assertTrue(lp.isWakeOnLanSupported());
@@ -1176,7 +1169,7 @@
assertFalse(lp.isWakeOnLanSupported());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testCaptivePortalApiUrl() {
final LinkProperties lp = makeTestObject();
assertEquals(CAPPORT_API_URL, lp.getCaptivePortalApiUrl());
@@ -1185,7 +1178,7 @@
assertNull(lp.getCaptivePortalApiUrl());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testCaptivePortalData() {
final LinkProperties lp = makeTestObject();
assertEquals(getCaptivePortalData(), lp.getCaptivePortalData());
@@ -1238,7 +1231,7 @@
assertTrue(Ipv6.hasIpv6DnsServer());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testHasIpv4UnreachableDefaultRoute() {
final LinkProperties lp = makeTestObject();
assertFalse(lp.hasIpv4UnreachableDefaultRoute());
@@ -1249,7 +1242,7 @@
assertFalse(lp.hasIpv6UnreachableDefaultRoute());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testHasIpv6UnreachableDefaultRoute() {
final LinkProperties lp = makeTestObject();
assertFalse(lp.hasIpv6UnreachableDefaultRoute());
diff --git a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
index 4a4859d..70adbd7 100644
--- a/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
+++ b/tests/common/java/android/net/MatchAllNetworkSpecifierTest.kt
@@ -52,7 +52,6 @@
}
@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
diff --git a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
index e5806a6..1148eff 100644
--- a/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
+++ b/tests/common/java/android/net/NattKeepalivePacketDataTest.kt
@@ -82,7 +82,7 @@
dstPort: Int = NATT_PORT
) = NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort, dstAddress, dstPort)
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testConstructor() {
assertFailsWith<InvalidPacketException>(
"Dst port is not NATT port should cause exception") {
@@ -132,12 +132,12 @@
assertEquals(TEST_ADDRV6, packet2.dstAddress)
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testParcel() {
assertParcelingIsLossless(nattKeepalivePacket())
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testEquals() {
assertEqualBothWays(nattKeepalivePacket(), nattKeepalivePacket())
assertNotEquals(nattKeepalivePacket(dstAddress = TEST_SRC_ADDRV4), nattKeepalivePacket())
@@ -146,7 +146,7 @@
assertNotEquals(nattKeepalivePacket(srcPort = TEST_PORT2), nattKeepalivePacket())
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testHashCode() {
assertEquals(nattKeepalivePacket().hashCode(), nattKeepalivePacket().hashCode())
}
diff --git a/tests/common/java/android/net/NetworkAgentConfigTest.kt b/tests/common/java/android/net/NetworkAgentConfigTest.kt
index c05cdbd..d640a73 100644
--- a/tests/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/common/java/android/net/NetworkAgentConfigTest.kt
@@ -16,19 +16,15 @@
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.modules.utils.build.SdkLevel.isAtLeastT
import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.assertParcelingIsLossless
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
@@ -36,10 +32,7 @@
@SmallTest
@ConnectivityModuleTest
class NetworkAgentConfigTest {
- @Rule @JvmField
- val ignoreRule = DevSdkIgnoreRule()
-
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testParcelNetworkAgentConfig() {
val config = NetworkAgentConfig.Builder().apply {
setExplicitlySelected(true)
@@ -58,7 +51,7 @@
assertParcelingIsLossless(config)
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testBuilder() {
val testExtraInfo = "mylegacyExtraInfo"
val config = NetworkAgentConfig.Builder().apply {
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index aae3425..3a3459b 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -26,6 +26,7 @@
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_LOCAL_NETWORK;
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;
@@ -60,9 +61,9 @@
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.modules.utils.build.SdkLevel.isAtLeastT;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastV;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -369,6 +370,9 @@
.addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_EIMS)
.addCapability(NET_CAPABILITY_NOT_METERED);
+ if (isAtLeastV()) {
+ netCap.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ }
if (isAtLeastS()) {
final ArraySet<Integer> allowedUids = new ArraySet<>();
allowedUids.add(4);
@@ -377,10 +381,9 @@
netCap.setSubscriptionIds(Set.of(TEST_SUBID1, TEST_SUBID2));
netCap.setUids(uids);
}
- if (isAtLeastR()) {
- netCap.setOwnerUid(123);
- netCap.setAdministratorUids(new int[] {5, 11});
- }
+
+ netCap.setOwnerUid(123);
+ netCap.setAdministratorUids(new int[] {5, 11});
assertParcelingIsLossless(netCap);
netCap.setSSID(TEST_SSID);
testParcelSane(netCap);
@@ -392,10 +395,8 @@
.addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_EIMS)
.addCapability(NET_CAPABILITY_NOT_METERED);
- if (isAtLeastR()) {
- netCap.setRequestorPackageName("com.android.test");
- netCap.setRequestorUid(9304);
- }
+ netCap.setRequestorPackageName("com.android.test");
+ netCap.setRequestorUid(9304);
assertParcelingIsLossless(netCap);
netCap.setSSID(TEST_SSID);
testParcelSane(netCap);
@@ -815,16 +816,12 @@
assertTrue(nc2.hasForbiddenCapability(NET_CAPABILITY_NOT_ROAMING));
}
- if (isAtLeastR()) {
- assertTrue(TEST_SSID.equals(nc2.getSsid()));
- }
-
+ 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()));
- }
+ assertTrue(DIFFERENT_TEST_SSID.equals(nc2.getSsid()));
+
if (isAtLeastS()) {
nc1.setUids(uidRanges(10, 13));
} else {
diff --git a/tests/common/java/android/net/NetworkProviderTest.kt b/tests/common/java/android/net/NetworkProviderTest.kt
index c6a7346..0d35960 100644
--- a/tests/common/java/android/net/NetworkProviderTest.kt
+++ b/tests/common/java/android/net/NetworkProviderTest.kt
@@ -39,6 +39,12 @@
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.TestableNetworkOfferCallback
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.RejectedExecutionException
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -47,12 +53,6 @@
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.verifyNoMoreInteractions
-import java.util.UUID
-import java.util.concurrent.Executor
-import java.util.concurrent.RejectedExecutionException
-import kotlin.test.assertEquals
-import kotlin.test.assertNotEquals
-import kotlin.test.fail
private const val DEFAULT_TIMEOUT_MS = 5000L
private const val DEFAULT_NO_CALLBACK_TIMEOUT_MS = 200L
@@ -62,7 +62,6 @@
private val PROVIDER_NAME = "NetworkProviderTest"
@RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
@ConnectivityModuleTest
class NetworkProviderTest {
@Rule @JvmField
diff --git a/tests/common/java/android/net/NetworkSpecifierTest.kt b/tests/common/java/android/net/NetworkSpecifierTest.kt
index b960417..7edb474 100644
--- a/tests/common/java/android/net/NetworkSpecifierTest.kt
+++ b/tests/common/java/android/net/NetworkSpecifierTest.kt
@@ -15,21 +15,18 @@
*/
package android.net
-import android.os.Build
import androidx.test.filters.SmallTest
import com.android.testutils.ConnectivityModuleTest
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
@SmallTest
@RunWith(DevSdkIgnoreRunner::class)
-@IgnoreUpTo(Build.VERSION_CODES.Q)
@ConnectivityModuleTest
class NetworkSpecifierTest {
private class TestNetworkSpecifier(
diff --git a/tests/common/java/android/net/NetworkStackTest.java b/tests/common/java/android/net/NetworkStackTest.java
index f8f9c72..13550f9 100644
--- a/tests/common/java/android/net/NetworkStackTest.java
+++ b/tests/common/java/android/net/NetworkStackTest.java
@@ -17,16 +17,11 @@
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;
@@ -34,16 +29,13 @@
@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)
+ @Test
public void testGetService() {
NetworkStack.setServiceForTest(mConnectorBinder);
assertEquals(NetworkStack.getService(), mConnectorBinder);
diff --git a/tests/common/java/android/net/NetworkTest.java b/tests/common/java/android/net/NetworkTest.java
index c102cb3..86d2463 100644
--- a/tests/common/java/android/net/NetworkTest.java
+++ b/tests/common/java/android/net/NetworkTest.java
@@ -161,8 +161,7 @@
assertEquals(16290598925L, three.getNetworkHandle());
}
- // getNetId() did not exist in Q
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testGetNetId() {
assertEquals(1234, new Network(1234).getNetId());
assertEquals(2345, new Network(2345, true).getNetId());
diff --git a/tests/common/java/android/net/RouteInfoTest.java b/tests/common/java/android/net/RouteInfoTest.java
index 5b28b84..154dc4c 100644
--- a/tests/common/java/android/net/RouteInfoTest.java
+++ b/tests/common/java/android/net/RouteInfoTest.java
@@ -31,17 +31,11 @@
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.ConnectivityModuleTest;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -53,9 +47,6 @@
@SmallTest
@ConnectivityModuleTest
public class RouteInfoTest {
- @Rule
- public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
private static final int INVALID_ROUTE_TYPE = -1;
private InetAddress Address(String addr) {
@@ -66,11 +57,6 @@
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;
@@ -204,130 +190,108 @@
assertTrue(r.isDefaultRoute());
assertTrue(r.isIPv4Default());
assertFalse(r.isIPv6Default());
- if (isAtLeastR()) {
- assertFalse(r.isIPv4UnreachableDefault());
- assertFalse(r.isIPv6UnreachableDefault());
- }
+
+ 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());
- }
+ 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());
- }
+ 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());
- }
+
+ 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());
- }
+
+ 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());
- }
+ 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());
- }
+ 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());
- }
+ 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());
- }
+ 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());
- }
+ 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());
- }
+ 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());
- }
+ 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());
- }
+ assertFalse(r.isIPv4UnreachableDefault());
+ assertTrue(r.isIPv6UnreachableDefault());
}
@Test
@@ -376,14 +340,14 @@
assertParcelingIsLossless(r);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testMtuParceling() {
final RouteInfo r = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::"), "testiface",
RTN_UNREACHABLE, 1450 /* mtu */);
assertParcelingIsLossless(r);
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testMtu() {
RouteInfo r;
r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0",
@@ -394,7 +358,7 @@
assertEquals(0, r.getMtu());
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
public void testRouteKey() {
RouteInfo.RouteKey k1, k2;
// Only prefix, null gateway and null interface
diff --git a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
index c90b1aa..8cef6aa 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsApiTest.kt
@@ -28,25 +28,18 @@
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.assertNetworkStatsEquals
import com.android.testutils.assertParcelingIsLossless
+import kotlin.test.assertEquals
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-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
diff --git a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
index fd7bd74..1b55be9 100644
--- a/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkTemplateTest.kt
@@ -62,11 +62,6 @@
}
}
- // Verify hidden match rules cannot construct templates.
- assertFailsWith<IllegalArgumentException> {
- NetworkTemplate.Builder(MATCH_PROXY).build()
- }
-
// Verify template which matches metered cellular and carrier networks with
// the given IMSI. See buildTemplateMobileAll and buildTemplateCarrierMetered.
listOf(MATCH_MOBILE, MATCH_CARRIER).forEach { matchRule ->
@@ -170,9 +165,9 @@
assertEquals(expectedTemplate, it)
}
- // Verify template which matches ethernet and bluetooth networks.
+ // Verify template which matches ethernet, bluetooth and proxy networks.
// See buildTemplateEthernet and buildTemplateBluetooth.
- listOf(MATCH_ETHERNET, MATCH_BLUETOOTH).forEach { matchRule ->
+ listOf(MATCH_ETHERNET, MATCH_BLUETOOTH, MATCH_PROXY).forEach { matchRule ->
NetworkTemplate.Builder(matchRule).build().let {
val expectedTemplate = NetworkTemplate(matchRule,
emptyArray<String>() /*subscriberIds*/, emptyArray<String>(),
diff --git a/tests/common/java/android/net/util/SocketUtilsTest.kt b/tests/common/java/android/net/util/SocketUtilsTest.kt
index aaf97f3..520cf07 100644
--- a/tests/common/java/android/net/util/SocketUtilsTest.kt
+++ b/tests/common/java/android/net/util/SocketUtilsTest.kt
@@ -16,7 +16,6 @@
package android.net.util
-import android.os.Build
import android.system.NetlinkSocketAddress
import android.system.Os
import android.system.OsConstants.AF_INET
@@ -27,13 +26,10 @@
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
@@ -44,9 +40,6 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
class SocketUtilsTest {
- @Rule @JvmField
- val ignoreRule = DevSdkIgnoreRule()
-
@Test
fun testMakeNetlinkSocketAddress() {
val nlAddress = SocketUtils.makeNetlinkSocketAddress(TEST_PORT, RTMGRP_NEIGH)
@@ -67,7 +60,7 @@
assertTrue("Not PacketSocketAddress object", pkAddress2 is PacketSocketAddress)
}
- @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @Test
fun testMakePacketSocketAddress() {
val pkAddress = SocketUtils.makePacketSocketAddress(
ETH_P_ALL, TEST_INDEX, ByteArray(6) { FF_BYTE })
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index e83e36a..90b7875 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -33,6 +33,11 @@
<option name="teardown-command" value="cmd netpolicy stop-watching" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="force-skip-system-props" value="true" />
+ <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+ </target_preparer>
+
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
<option name="jar" value="CtsHostsideNetworkTests.jar" />
<option name="runtime-hint" value="3m56s" />
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 2245382..470bb17 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -38,7 +38,6 @@
srcs: ["src/**/*.java"],
// Tag this module as a cts test artifact
test_suites: [
- "cts",
"general-tests",
"sts",
],
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 89a55a7..198b009 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -37,6 +37,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.NotificationManager;
@@ -61,6 +62,9 @@
import android.util.Log;
import android.util.Pair;
+import androidx.annotation.Nullable;
+
+import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
@@ -198,7 +202,8 @@
protected void tearDown() throws Exception {
executeShellCommand("cmd netpolicy stop-watching");
mServiceClient.unbind();
- if (mLock.isHeld()) mLock.release();
+ final PowerManager.WakeLock lock = mLock;
+ if (null != lock && lock.isHeld()) lock.release();
}
protected int getUid(String packageName) throws Exception {
@@ -280,8 +285,30 @@
}
protected void assertBackgroundNetworkAccess(boolean expectAllowed) throws Exception {
+ assertBackgroundNetworkAccess(expectAllowed, null);
+ }
+
+ /**
+ * Asserts whether the active network is available or not for the background app. If the network
+ * is unavailable, also checks whether it is blocked by the expected error.
+ *
+ * @param expectAllowed expect background network access to be allowed or not.
+ * @param expectedUnavailableError the expected error when {@code expectAllowed} is false. It's
+ * meaningful only when the {@code expectAllowed} is 'false'.
+ * Throws an IllegalArgumentException when {@code expectAllowed}
+ * is true and this parameter is not null. When the
+ * {@code expectAllowed} is 'false' and this parameter is null,
+ * this function does not compare error type of the networking
+ * access failure.
+ */
+ protected void assertBackgroundNetworkAccess(boolean expectAllowed,
+ @Nullable final String expectedUnavailableError) throws Exception {
assertBackgroundState();
- assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */);
+ if (expectAllowed && expectedUnavailableError != null) {
+ throw new IllegalArgumentException("expectedUnavailableError is not null");
+ }
+ assertNetworkAccess(expectAllowed /* expectAvailable */, false /* needScreenOn */,
+ expectedUnavailableError);
}
protected void assertForegroundNetworkAccess() throws Exception {
@@ -404,12 +431,17 @@
*/
private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn)
throws Exception {
+ assertNetworkAccess(expectAvailable, needScreenOn, null);
+ }
+
+ private void assertNetworkAccess(boolean expectAvailable, boolean needScreenOn,
+ @Nullable final String expectedUnavailableError) throws Exception {
final int maxTries = 5;
String error = null;
int timeoutMs = 500;
for (int i = 1; i <= maxTries; i++) {
- error = checkNetworkAccess(expectAvailable);
+ error = checkNetworkAccess(expectAvailable, expectedUnavailableError);
if (error == null) return;
@@ -436,16 +468,55 @@
}
/**
+ * Asserts whether the network is blocked by accessing bpf maps if command-line tool supports.
+ */
+ void assertNetworkAccessBlockedByBpf(boolean expectBlocked, int uid, boolean metered) {
+ final String result;
+ try {
+ result = executeShellCommand(
+ "cmd network_stack is-uid-networking-blocked " + uid + " " + metered);
+ } catch (AssertionError e) {
+ // If NetworkStack is too old to support this command, ignore and continue
+ // this test to verify other parts.
+ if (e.getMessage().contains("No shell command implementation.")) {
+ return;
+ }
+ throw e;
+ }
+
+ // Tethering module is too old.
+ if (result.contains("API is unsupported")) {
+ return;
+ }
+
+ assertEquals(expectBlocked, parseBooleanOrThrow(result.trim()));
+ }
+
+ /**
+ * Similar to {@link Boolean#parseBoolean} but throws when the input
+ * is unexpected instead of returning false.
+ */
+ private static boolean parseBooleanOrThrow(@NonNull String s) {
+ // Don't use Boolean.parseBoolean
+ if ("true".equalsIgnoreCase(s)) return true;
+ if ("false".equalsIgnoreCase(s)) return false;
+ throw new IllegalArgumentException("Unexpected: " + s);
+ }
+
+ /**
* Checks whether the network is available as expected.
*
* @return error message with the mismatch (or empty if assertion passed).
*/
- private String checkNetworkAccess(boolean expectAvailable) throws Exception {
+ private String checkNetworkAccess(boolean expectAvailable,
+ @Nullable final String expectedUnavailableError) throws Exception {
final String resultData = mServiceClient.checkNetworkStatus();
- return checkForAvailabilityInResultData(resultData, expectAvailable);
+ return checkForAvailabilityInResultData(resultData, expectAvailable,
+ expectedUnavailableError);
}
- private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable) {
+ private String checkForAvailabilityInResultData(String resultData, boolean expectAvailable,
+ @Nullable final String expectedUnavailableError) {
if (resultData == null) {
assertNotNull("Network status from app2 is null", resultData);
}
@@ -477,6 +548,10 @@
if (expectedState != state || expectedDetailedState != detailedState) {
errors.append(String.format("Connection state mismatch: expected %s/%s, got %s/%s\n",
expectedState, expectedDetailedState, state, detailedState));
+ } else if (!expectAvailable && (expectedUnavailableError != null)
+ && !connectionCheckDetails.contains(expectedUnavailableError)) {
+ errors.append("Connection unavailable reason mismatch: expected "
+ + expectedUnavailableError + "\n");
}
if (errors.length() > 0) {
@@ -719,10 +794,12 @@
Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
if (enabled) {
turnBatteryOn();
+ AmUtils.waitForBroadcastBarrier();
executeSilentShellCommand("cmd power set-mode 1");
} else {
executeSilentShellCommand("cmd power set-mode 0");
turnBatteryOff();
+ AmUtils.waitForBroadcastBarrier();
}
}
@@ -748,27 +825,24 @@
assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
}
- protected void setAppIdle(boolean enabled) throws Exception {
+ protected void setAppIdle(boolean isIdle) throws Exception {
+ setAppIdleNoAssert(isIdle);
+ assertAppIdle(isIdle);
+ }
+
+ protected void setAppIdleNoAssert(boolean isIdle) throws Exception {
if (!isAppStandbySupported()) {
return;
}
- Log.i(TAG, "Setting app idle to " + enabled);
- executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
- assertAppIdle(enabled);
+ Log.i(TAG, "Setting app idle to " + isIdle);
+ final String bucketName = isIdle ? "rare" : "active";
+ executeSilentShellCommand("am set-standby-bucket " + TEST_APP2_PKG + " " + bucketName);
}
- protected void setAppIdleNoAssert(boolean enabled) throws Exception {
- if (!isAppStandbySupported()) {
- return;
- }
- Log.i(TAG, "Setting app idle to " + enabled);
- executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
- }
-
- protected void assertAppIdle(boolean enabled) throws Exception {
+ protected void assertAppIdle(boolean isIdle) throws Exception {
try {
assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
- 30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
+ 30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + isIdle);
} catch (Throwable e) {
throw e;
}
@@ -876,7 +950,7 @@
final String resultData = result.get(0).second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable);
+ resultData, expectAvailable, null /* expectedUnavailableError */);
if (error != null) {
fail("Network is not available for activity in app2 (" + mUid + "): "
+ error);
@@ -911,7 +985,7 @@
final String resultData = result.get(0).second;
if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) {
final String error = checkForAvailabilityInResultData(
- resultData, expectAvailable);
+ resultData, expectAvailable, null /* expectedUnavailableError */);
if (error != null) {
Log.d(TAG, "Network state is unexpected, checking again. " + error);
// Right now we could end up in an unexpected state if expedited job
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 0610774..93cc911 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -27,7 +27,7 @@
import android.os.RemoteException;
public class MyServiceClient {
- private static final int TIMEOUT_MS = 5000;
+ private static final int TIMEOUT_MS = 20_000;
private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
private static final String APP2_PACKAGE = PACKAGE + ".app2";
private static final String SERVICE_NAME = APP2_PACKAGE + ".MyService";
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 0715e32..82f4a65 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -32,8 +32,11 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
+import android.net.cts.util.CtsNetUtils;
import android.util.Log;
+import com.android.modules.utils.build.SdkLevel;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -46,6 +49,9 @@
public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCase {
private Network mNetwork;
private final TestNetworkCallback mTestNetworkCallback = new TestNetworkCallback();
+ private CtsNetUtils mCtsNetUtils;
+ private static final String GOOGLE_PRIVATE_DNS_SERVER = "dns.google";
+
@Rule
public final MeterednessConfigurationRule mMeterednessConfiguration
= new MeterednessConfigurationRule();
@@ -218,6 +224,26 @@
mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
false /* hasCapability */, NET_CAPABILITY_NOT_METERED);
mTestNetworkCallback.expectBlockedStatusCallback(mNetwork, false);
+
+ // Before Android T, DNS queries over private DNS should be but are not restricted by Power
+ // Saver or Data Saver. The issue is fixed in mainline update and apps can no longer request
+ // DNS queries when its network is restricted by Power Saver. The fix takes effect backwards
+ // starting from Android T. But for Data Saver, the fix is not backward compatible since
+ // there are some platform changes involved. It is only available on devices that a specific
+ // trunk flag is enabled.
+ //
+ // This test can not only verify that the network traffic from apps is blocked at the right
+ // time, but also verify whether it is correctly blocked at the DNS stage, or at a later
+ // socket connection stage.
+ if (SdkLevel.isAtLeastT()) {
+ // Enable private DNS
+ mCtsNetUtils = new CtsNetUtils(mContext);
+ mCtsNetUtils.storePrivateDnsSetting();
+ mCtsNetUtils.setPrivateDnsStrictMode(GOOGLE_PRIVATE_DNS_SERVER);
+ mCtsNetUtils.awaitPrivateDnsSetting(
+ "NetworkCallbackTest wait private DNS setting timeout", mNetwork,
+ GOOGLE_PRIVATE_DNS_SERVER, true);
+ }
}
@After
@@ -227,6 +253,10 @@
setRestrictBackground(false);
setBatterySaverMode(false);
unregisterNetworkCallback();
+
+ if (SdkLevel.isAtLeastT() && (mCtsNetUtils != null)) {
+ mCtsNetUtils.restorePrivateDnsSetting();
+ }
}
@RequiredProperties({DATA_SAVER_MODE})
@@ -235,17 +265,23 @@
try {
// Enable restrict background
setRestrictBackground(true);
+ // TODO: Verify expectedUnavailableError when aconfig support mainline.
+ // (see go/aconfig-in-mainline-problems)
assertBackgroundNetworkAccess(false);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
// Add to whitelist
addRestrictBackgroundWhitelist(mUid);
assertBackgroundNetworkAccess(true);
+ assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
// Remove from whitelist
removeRestrictBackgroundWhitelist(mUid);
+ // TODO: Verify expectedUnavailableError when aconfig support mainline.
assertBackgroundNetworkAccess(false);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
} finally {
mMeterednessConfiguration.resetNetworkMeteredness();
@@ -257,11 +293,13 @@
true /* hasCapability */, NET_CAPABILITY_NOT_METERED);
try {
assertBackgroundNetworkAccess(true);
+ assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
// Disable restrict background, should not trigger callback
setRestrictBackground(false);
assertBackgroundNetworkAccess(true);
+ assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
} finally {
mMeterednessConfiguration.resetNetworkMeteredness();
}
@@ -273,13 +311,19 @@
try {
// Enable Power Saver
setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
+ if (SdkLevel.isAtLeastT()) {
+ assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ } else {
+ assertBackgroundNetworkAccess(false);
+ }
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ assertNetworkAccessBlockedByBpf(true, mUid, true /* metered */);
// Disable Power Saver
setBatterySaverMode(false);
assertBackgroundNetworkAccess(true);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ assertNetworkAccessBlockedByBpf(false, mUid, true /* metered */);
} finally {
mMeterednessConfiguration.resetNetworkMeteredness();
}
@@ -291,13 +335,19 @@
try {
// Enable Power Saver
setBatterySaverMode(true);
- assertBackgroundNetworkAccess(false);
+ if (SdkLevel.isAtLeastT()) {
+ assertBackgroundNetworkAccess(false, "java.net.UnknownHostException");
+ } else {
+ assertBackgroundNetworkAccess(false);
+ }
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, true);
+ assertNetworkAccessBlockedByBpf(true, mUid, false /* metered */);
// Disable Power Saver
setBatterySaverMode(false);
assertBackgroundNetworkAccess(true);
mTestNetworkCallback.expectBlockedStatusCallbackEventually(mNetwork, false);
+ assertNetworkAccessBlockedByBpf(false, mUid, false /* metered */);
} finally {
mMeterednessConfiguration.resetNetworkMeteredness();
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 8c38b44..5331601 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -449,13 +449,19 @@
// this function and using PollingCheck to try to make sure the uid has updated and reduce the
// flaky rate.
public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
- boolean expectedResult) throws Exception {
- PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)));
+ boolean expectedResult) {
+ final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
+ + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
+ PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
+ errMsg);
}
- public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult)
- throws Exception {
- PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)));
+ public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
+ final String errMsg = String.format(
+ "Unexpected result from isUidRestrictedOnMeteredNetworks; "
+ + "uid= " + uid + ", expected=" + expectedResult);
+ PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
+ errMsg);
}
public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index ff7240d..2c2d957 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -20,6 +20,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -45,7 +46,11 @@
<service android:name=".MyService"
android:exported="true"/>
<service android:name=".MyForegroundService"
- android:exported="true"/>
+ android:foregroundServiceType="specialUse"
+ android:exported="true">
+ <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="Connectivity" />
+ </service>
<service android:name=".RemoteSocketFactoryService"
android:exported="true"/>
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 6de663a..9310888 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -55,17 +55,20 @@
"junit-params",
"modules-utils-build",
"net-utils-framework-common",
- "truth-prebuilt",
+ "truth",
"TetheringIntegrationTestsBaseLib",
],
// uncomment when b/13249961 is fixed
// sdk_version: "current",
platform_apis: true,
- data: [":ConnectivityTestPreparer"],
per_testcase_directory: true,
host_required: ["net-tests-utils-host-common"],
test_config_template: "AndroidTestTemplate.xml",
+ data: [
+ ":ConnectivityTestPreparer",
+ ":CtsCarrierServicePackage",
+ ]
}
// Networking CTS tests for development and release. These tests always target the platform SDK
@@ -74,7 +77,10 @@
// devices.
android_test {
name: "CtsNetTestCases",
- defaults: ["CtsNetTestCasesDefaults", "ConnectivityNextEnableDefaults"],
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "ConnectivityNextEnableDefaults",
+ ],
static_libs: [
"DhcpPacketLib",
"NetworkStackApiCurrentShims",
@@ -129,7 +135,7 @@
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk33", // Must match CtsNetTestCasesMaxTargetSdk33 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "33",
package_name: "android.net.cts.maxtargetsdk33",
@@ -137,17 +143,31 @@
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "31",
- package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
+ package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
instrumentation_target_package: "android.net.cts.maxtargetsdk31",
}
android_test {
- name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
+ name: "CtsNetTestCasesMaxTargetSdk30", // Must match CtsNetTestCasesMaxTargetSdk30 annotation.
defaults: ["CtsNetTestCasesMaxTargetSdkDefaults"],
target_sdk_version: "30",
- package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
+ package_name: "android.net.cts.maxtargetsdk30", // CTS package names must be unique.
instrumentation_target_package: "android.net.cts.maxtargetsdk30",
}
+
+android_test_helper_app {
+ name: "CtsCarrierServicePackage",
+ defaults: ["cts_defaults"],
+ package_name: "android.net.cts.carrierservicepackage",
+ manifest: "carrierservicepackage/AndroidManifest.xml",
+ srcs: ["carrierservicepackage/src/**/*.java"],
+ min_sdk_version: "30",
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "cts",
+ "general-tests",
+ ],
+}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 68e36ff..098cc0a 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -36,6 +36,7 @@
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- This test also uses signature permissions through adopting the shell identity.
The permissions acquired that way include (probably not exhaustive) :
@@ -46,6 +47,7 @@
android:usesCleartextTraffic="true">
<uses-library android:name="android.test.runner" />
<uses-library android:name="org.apache.http.legacy" android:required="false" />
+
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
@@ -54,4 +56,3 @@
</instrumentation>
</manifest>
-
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 8efa99f..38f26d8 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -27,6 +27,7 @@
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="{MODULE}.apk" />
+ <option name="test-file-name" value="CtsCarrierServicePackage.apk" />
</target_preparer>
<target_preparer class="com.android.testutils.ConnectivityTestTargetPreparer">
</target_preparer>
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index 9b81a56..1f1dd5d 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -41,7 +41,7 @@
"mockwebserver",
"junit",
"junit-params",
- "truth-prebuilt",
+ "truth",
],
platform_apis: true,
diff --git a/tests/cts/net/carrierservicepackage/AndroidManifest.xml b/tests/cts/net/carrierservicepackage/AndroidManifest.xml
new file mode 100644
index 0000000..c2a45eb
--- /dev/null
+++ b/tests/cts/net/carrierservicepackage/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ android:versionCode="1"
+ android:versionName="1.0.0"
+ package="android.net.cts.carrierservicepackage">
+ <uses-sdk android:minSdkVersion="30"
+ android:targetSdkVersion="33" />
+ <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+ <application android:allowBackup="false"
+ android:directBootAware="true">
+ <service android:name=".DummyCarrierConfigService"
+ android:permission="android.permission.BIND_CARRIER_SERVICES"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.service.carrier.CarrierService"/>
+ </intent-filter>
+ </service>
+ </application>
+
+</manifest>
diff --git a/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java b/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java
new file mode 100644
index 0000000..ca2015b
--- /dev/null
+++ b/tests/cts/net/carrierservicepackage/src/android/net/cts/carrierservicepackage/DummyCarrierConfigService.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts.carrierservicepackage;
+
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.service.carrier.CarrierIdentifier;
+import android.service.carrier.CarrierService;
+
+public class DummyCarrierConfigService extends CarrierService {
+ private static final String TAG = "DummyCarrierConfigService";
+
+ public DummyCarrierConfigService() {}
+
+ @Override
+ public PersistableBundle onLoadConfig(CarrierIdentifier id) {
+ return new PersistableBundle(); // Do nothing
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return super.onBind(intent);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ return super.onUnbind(intent);
+ }
+}
+
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index d2c9481..58f6d58 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -38,6 +38,7 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.ConnectivityManager.EXTRA_NETWORK;
import static android.net.ConnectivityManager.EXTRA_NETWORK_REQUEST;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
@@ -47,6 +48,7 @@
import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
+import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
@@ -276,10 +278,8 @@
private static final int MIN_KEEPALIVE_INTERVAL = 10;
private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
- // Timeout for waiting network to be validated. Set the timeout to 30s, which is more than
- // DNS timeout.
- // TODO(b/252972908): reset the original timer when aosp/2188755 is ramped up.
- private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_000;
+ // Timeout for waiting network to be validated.
+ private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 5_000;
private static final int NO_CALLBACK_TIMEOUT_MS = 100;
private static final int NETWORK_REQUEST_TIMEOUT_MS = 3000;
private static final int SOCKET_TIMEOUT_MS = 100;
@@ -794,14 +794,15 @@
// Make sure that the NC is null if the package doesn't hold ACCESS_NETWORK_STATE.
assertNull(redactNc(nc, groundedUid, groundedPkg));
- // Uids, ssid, underlying networks & subscriptionIds will be redacted if the given uid
+ // Uids, ssid & underlying networks will be redacted if the given uid
// doesn't hold the associated permissions. The wifi transport info is also suitably
// redacted.
final NetworkCapabilities redactedNormal = redactNc(nc, normalUid, normalPkg);
assertNull(redactedNormal.getUids());
assertNull(redactedNormal.getSsid());
assertNull(redactedNormal.getUnderlyingNetworks());
- assertEquals(0, redactedNormal.getSubscriptionIds().size());
+ // Owner UID is allowed to see the subscription IDs.
+ assertEquals(2, redactedNormal.getSubscriptionIds().size());
assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());
@@ -3537,6 +3538,12 @@
doTestFirewallBlocking(FIREWALL_CHAIN_DOZABLE, ALLOWLIST);
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @ConnectivityModuleTest
+ @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ public void testFirewallBlockingBackground() {
+ doTestFirewallBlocking(FIREWALL_CHAIN_BACKGROUND, ALLOWLIST);
+ }
+
@Test @IgnoreUpTo(SC_V2) @ConnectivityModuleTest
@AppModeFull(reason = "Socket cannot bind in instant app mode")
public void testFirewallBlockingPowersave() {
@@ -3591,6 +3598,15 @@
}
}
+ private void setUidFirewallRule(final int chain, final int uid, final int rule) {
+ try {
+ mCm.setUidFirewallRule(chain, uid, rule);
+ } catch (IllegalStateException ignored) {
+ // Removing match causes an exception when the rule entry for the uid does
+ // not exist. But this is fine and can be ignored.
+ }
+ }
+
private static final boolean EXPECT_OPEN = false;
private static final boolean EXPECT_CLOSE = true;
@@ -3599,6 +3615,8 @@
runWithShellPermissionIdentity(() -> {
// Firewall chain status will be restored after the test.
final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+ final int myUid = Process.myUid();
+ final int previousMyUidFirewallRule = mCm.getUidFirewallRule(chain, myUid);
final int previousUidFirewallRule = mCm.getUidFirewallRule(chain, targetUid);
final Socket socket = new Socket(TEST_HOST, HTTP_PORT);
socket.setSoTimeout(NETWORK_REQUEST_TIMEOUT_MS);
@@ -3606,12 +3624,12 @@
mCm.setFirewallChainEnabled(chain, false /* enable */);
assertSocketOpen(socket);
- try {
- mCm.setUidFirewallRule(chain, targetUid, rule);
- } catch (IllegalStateException ignored) {
- // Removing match causes an exception when the rule entry for the uid does
- // not exist. But this is fine and can be ignored.
+ setUidFirewallRule(chain, targetUid, rule);
+ if (targetUid != myUid) {
+ // If this test does not set rule on myUid, remove existing rule on myUid
+ setUidFirewallRule(chain, myUid, FIREWALL_RULE_DEFAULT);
}
+
mCm.setFirewallChainEnabled(chain, true /* enable */);
if (expectClose) {
@@ -3624,11 +3642,9 @@
mCm.setFirewallChainEnabled(chain, wasChainEnabled);
}, /* cleanup */ () -> {
// Restore the uid firewall rule status
- try {
- mCm.setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
- } catch (IllegalStateException ignored) {
- // Removing match causes an exception when the rule entry for the uid does
- // not exist. But this is fine and can be ignored.
+ setUidFirewallRule(chain, targetUid, previousUidFirewallRule);
+ if (targetUid != myUid) {
+ setUidFirewallRule(chain, myUid, previousMyUidFirewallRule);
}
}, /* cleanup */ () -> {
socket.close();
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 805dd65..6b7954a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -22,6 +22,7 @@
import static android.net.cts.util.CtsNetUtils.TestNetworkCallback;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
@@ -34,12 +35,14 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
+import static org.junit.Assume.assumeFalse;
import android.Manifest;
import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Ikev2VpnProfile;
import android.net.IpSecAlgorithm;
@@ -60,11 +63,7 @@
import com.android.internal.util.HexDump;
import com.android.networkstack.apishim.ConstantsShim;
-import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
-import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
import com.android.networkstack.apishim.VpnManagerShimImpl;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
import com.android.networkstack.apishim.common.VpnManagerShim;
import com.android.networkstack.apishim.common.VpnProfileStateShim;
import com.android.testutils.DevSdkIgnoreRule;
@@ -75,6 +74,7 @@
import org.bouncycastle.x509.X509V1CertificateGenerator;
import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -203,6 +203,12 @@
mUserCertKey = generateRandomCertAndKeyPair();
}
+ @Before
+ public void setUp() {
+ assumeFalse("Skipping test because watches don't support VPN",
+ sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
+ }
+
@After
public void tearDown() {
for (TestableNetworkCallback callback : mCallbacksToUnregister) {
@@ -210,6 +216,15 @@
}
setAppop(AppOpsManager.OP_ACTIVATE_VPN, false);
setAppop(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, false);
+
+ // Make sure the VpnProfile is not provisioned already.
+ sVpnMgr.stopProvisionedVpnProfile();
+
+ try {
+ sVpnMgr.startProvisionedVpnProfile();
+ fail("Expected SecurityException for missing consent");
+ } catch (SecurityException expected) {
+ }
}
/**
@@ -227,28 +242,25 @@
}
private Ikev2VpnProfile buildIkev2VpnProfileCommon(
- @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+ @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
boolean requiresValidation, boolean automaticIpVersionSelectionEnabled,
boolean automaticNattKeepaliveTimerEnabled) throws Exception {
- builderShim.setBypassable(true)
+ builder.setBypassable(true)
.setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
.setProxy(TEST_PROXY_INFO)
.setMaxMtu(TEST_MTU)
.setMetered(false);
- if (TestUtils.shouldTestTApis()) {
- builderShim.setRequiresInternetValidation(requiresValidation);
+ if (isAtLeastT()) {
+ builder.setRequiresInternetValidation(requiresValidation);
}
- if (TestUtils.shouldTestUApis()) {
- builderShim.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
- builderShim.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
+ if (isAtLeastU()) {
+ builder.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
+ builder.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
}
- // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
- // method and is not defined in shims.
// TODO: replace it in alternative way to remove the hidden method usage
- final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
if (isRestrictedToTestNetworks) {
builder.restrictToTestNetworks();
}
@@ -264,16 +276,14 @@
? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
IkeSessionTestUtils.CHILD_PARAMS);
- final Ikev2VpnProfileBuilderShim builderShim =
- Ikev2VpnProfileBuilderShimImpl.newInstance(params)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(params)
.setRequiresInternetValidation(requiresValidation)
.setProxy(TEST_PROXY_INFO)
.setMaxMtu(TEST_MTU)
.setMetered(false);
- // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
- // method and is not defined in shims.
+
// TODO: replace it in alternative way to remove the hidden method usage
- final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
if (isRestrictedToTestNetworks) {
builder.restrictToTestNetworks();
}
@@ -283,8 +293,8 @@
private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
boolean isRestrictedToTestNetworks, boolean requiresValidation)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY)
.setAuthPsk(TEST_PSK);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
requiresValidation, false /* automaticIpVersionSelectionEnabled */,
@@ -293,8 +303,8 @@
private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
false /* requiresValidation */, false /* automaticIpVersionSelectionEnabled */,
@@ -303,8 +313,8 @@
private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
throws Exception {
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthDigitalSignature(
mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
@@ -347,7 +357,6 @@
@Test
public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception {
assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature());
- assumeTrue(TestUtils.shouldTestTApis());
final IkeTunnelConnectionParams expectedParams = new IkeTunnelConnectionParams(
IkeSessionTestUtils.IKE_PARAMS_V6, IkeSessionTestUtils.CHILD_PARAMS);
@@ -567,7 +576,7 @@
// regardless of its value. However, there is a race in Vpn(see b/228574221) that VPN may
// misuse VPN network itself as the underlying network. The fix is not available without
// SDK > T platform. Thus, verify this only on T+ platform.
- if (!requiresValidation && TestUtils.shouldTestTApis()) {
+ if (!requiresValidation && isAtLeastT()) {
cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, TIMEOUT_MS,
entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
.hasCapability(NET_CAPABILITY_VALIDATED));
@@ -647,7 +656,6 @@
@Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV4WithValidation() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
@@ -660,35 +668,30 @@
@Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV6WithValidation() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
@Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileIkeTunConnParamsV4() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, true /* testIkeTunConnParams */);
}
@Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileIkeTunConnParamsV4WithValidation() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(false /* testIpv6Only */, true /* requiresValidation */,
false /* testSessionKey */, true /* testIkeTunConnParams */);
}
@Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileIkeTunConnParamsV6() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, true /* testIkeTunConnParams */);
}
@Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileIkeTunConnParamsV6WithValidation() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(true /* testIpv6Only */, true /* requiresValidation */,
false /* testSessionKey */, true /* testIkeTunConnParams */);
}
@@ -696,7 +699,6 @@
@IgnoreUpTo(SC_V2)
@Test
public void testStartProvisionedVpnV4ProfileSession() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
true /* testSessionKey */, false /* testIkeTunConnParams */);
}
@@ -704,59 +706,44 @@
@IgnoreUpTo(SC_V2)
@Test
public void testStartProvisionedVpnV6ProfileSession() throws Exception {
- assumeTrue(TestUtils.shouldTestTApis());
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
true /* testSessionKey */, false /* testIkeTunConnParams */);
}
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@Test
public void testBuildIkev2VpnProfileWithAutomaticNattKeepaliveTimerEnabled() throws Exception {
- // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
- // 34 shims, and @IgnoreUpTo does not check that.
- assumeTrue(TestUtils.shouldTestUApis());
-
final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
- final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
- Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
- assertFalse(shimWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
+ assertFalse(profileWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthPsk(TEST_PSK);
final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
false /* isRestrictedToTestNetworks */,
false /* requiresValidation */,
false /* automaticIpVersionSelectionEnabled */,
true /* automaticNattKeepaliveTimerEnabled */);
- final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
- Ikev2VpnProfileShimImpl.newInstance(profile);
- assertTrue(shim.isAutomaticNattKeepaliveTimerEnabled());
+ assertTrue(profile.isAutomaticNattKeepaliveTimerEnabled());
}
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@Test
public void testBuildIkev2VpnProfileWithAutomaticIpVersionSelectionEnabled() throws Exception {
- // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
- // 34 shims, and @IgnoreUpTo does not check that.
- assumeTrue(TestUtils.shouldTestUApis());
-
final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
- final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
- Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
- assertFalse(shimWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
+ assertFalse(profileWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
- final Ikev2VpnProfileBuilderShim builder =
- Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+ final Ikev2VpnProfile.Builder builder =
+ new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
.setAuthPsk(TEST_PSK);
final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
false /* isRestrictedToTestNetworks */,
false /* requiresValidation */,
true /* automaticIpVersionSelectionEnabled */,
false /* automaticNattKeepaliveTimerEnabled */);
- final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
- Ikev2VpnProfileShimImpl.newInstance(profile);
- assertTrue(shim.isAutomaticIpVersionSelectionEnabled());
+ assertTrue(profile.isAutomaticIpVersionSelectionEnabled());
}
private static class CertificateAndKey {
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 5937655..225408c 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -15,9 +15,12 @@
*/
package android.net.cts
+import android.Manifest.permission.MODIFY_PHONE_STATE
import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
import android.app.Instrumentation
import android.content.Context
+import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.EthernetNetworkSpecifier
import android.net.INetworkAgent
@@ -44,7 +47,9 @@
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+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
@@ -53,6 +58,7 @@
import android.net.NetworkReleasedException
import android.net.NetworkRequest
import android.net.NetworkScore
+import android.net.NetworkSpecifier
import android.net.QosCallback
import android.net.QosCallback.QosCallbackRegistrationException
import android.net.QosCallbackException
@@ -61,6 +67,7 @@
import android.net.QosSocketInfo
import android.net.RouteInfo
import android.net.SocketKeepalive
+import android.net.TelephonyNetworkSpecifier
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
import android.net.Uri
@@ -69,21 +76,31 @@
import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnError
import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnQosSessionAvailable
import android.net.cts.NetworkAgentTest.TestableQosCallback.CallbackEntry.OnQosSessionLost
+import android.net.wifi.WifiInfo
import android.os.Build
+import android.os.ConditionVariable
import android.os.Handler
import android.os.HandlerThread
import android.os.Message
+import android.os.PersistableBundle
import android.os.Process
import android.os.SystemClock
import android.platform.test.annotations.AppModeFull
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.CarrierPrivilegesCallback
import android.telephony.data.EpsBearerQosSessionAttributes
+import android.util.ArraySet
import android.util.DebugUtils.valueToString
+import android.util.Log
import androidx.test.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import com.android.compatibility.common.util.ThrowingSupplier
+import com.android.compatibility.common.util.UiccUtil
import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
import com.android.testutils.CompatUtil
@@ -112,23 +129,8 @@
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStatus
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertThrows
-import java.io.Closeable
-import java.io.IOException
-import java.net.DatagramSocket
-import java.net.InetAddress
-import java.net.InetSocketAddress
-import java.net.Socket
-import java.time.Duration
-import java.util.Arrays
-import java.util.UUID
-import java.util.concurrent.Executors
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertFalse
-import kotlin.test.assertNotNull
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
-import kotlin.test.fail
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -140,7 +142,26 @@
import org.mockito.Mockito.mock
import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify
+import java.io.Closeable
+import java.io.IOException
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.Socket
+import java.security.MessageDigest
+import java.time.Duration
+import java.util.Arrays
+import java.util.UUID
+import java.util.concurrent.Executors
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+private const val TAG = "NetworkAgentTest"
// This test doesn't really have a constraint on how fast the methods should return. If it's
// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
// without affecting the run time of successful runs. Thus, set a very high timeout.
@@ -261,17 +282,18 @@
callbacksToCleanUp.add(callback)
}
- private fun makeTestNetworkRequest(specifier: String? = null): NetworkRequest {
- return NetworkRequest.Builder()
- .clearCapabilities()
- .addTransportType(TRANSPORT_TEST)
- .also {
- if (specifier != null) {
- it.setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(specifier))
- }
- }
- .build()
- }
+ private fun String?.asEthSpecifier(): NetworkSpecifier? =
+ if (null == this) null else CompatUtil.makeEthernetNetworkSpecifier(this)
+ private fun makeTestNetworkRequest(specifier: NetworkSpecifier? = null) =
+ NetworkRequest.Builder().run {
+ clearCapabilities()
+ addTransportType(TRANSPORT_TEST)
+ if (specifier != null) setNetworkSpecifier(specifier)
+ build()
+ }
+
+ private fun makeTestNetworkRequest(specifier: String?) =
+ makeTestNetworkRequest(specifier.asEthSpecifier())
private fun makeTestNetworkCapabilities(
specifier: String? = null,
@@ -322,7 +344,7 @@
): Pair<TestableNetworkAgent, TestableNetworkCallback> {
val callback = TestableNetworkCallback()
// Ensure this NetworkAgent is never unneeded by filing a request with its specifier.
- requestNetwork(makeTestNetworkRequest(specifier = specifier), callback)
+ requestNetwork(makeTestNetworkRequest(specifier), callback)
val nc = makeTestNetworkCapabilities(specifier, transports)
val agent = createNetworkAgent(context, initialConfig = initialConfig, initialNc = nc)
agent.setTeardownDelayMillis(0)
@@ -543,6 +565,231 @@
.addTransportType(TRANSPORT_TEST)
.setAllowedUids(uids.toSet()).build()
+ /**
+ * Get the single element from this ArraySet, or fail() if doesn't contain exactly 1 element.
+ */
+ fun <T> ArraySet<T>.getSingleElement(): T {
+ if (size != 1) fail("Expected exactly one element, contained $size")
+ return iterator().next()
+ }
+
+ private fun doTestAllowedUids(
+ subId: Int,
+ transport: Int,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ doTestAllowedUids(subId, intArrayOf(transport), uid, expectUidsPresent)
+ }
+
+ private fun doTestAllowedUids(
+ subId: Int,
+ transports: IntArray,
+ uid: Int,
+ expectUidsPresent: Boolean
+ ) {
+ val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
+ val specifier = when {
+ transports.size != 1 -> null
+ TRANSPORT_ETHERNET in transports -> EthernetNetworkSpecifier("testInterface")
+ TRANSPORT_CELLULAR in transports -> TelephonyNetworkSpecifier(subId)
+ else -> null
+ }
+ val agent = createNetworkAgent(initialNc = NetworkCapabilities.Builder().run {
+ addTransportType(TRANSPORT_TEST)
+ transports.forEach { addTransportType(it) }
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ setNetworkSpecifier(specifier)
+ if (TRANSPORT_WIFI in transports && SdkLevel.isAtLeastV()) {
+ // setSubscriptionId only exists in V+
+ setTransportInfo(WifiInfo.Builder().setSubscriptionId(subId).build())
+ }
+ setAllowedUids(setOf(uid))
+ setOwnerUid(Process.myUid())
+ setAdministratorUids(intArrayOf(Process.myUid()))
+ build()
+ })
+ runWithShellPermissionIdentity {
+ agent.register()
+ }
+ agent.markConnected()
+
+ registerNetworkCallback(makeTestNetworkRequest(specifier), callback)
+ callback.expect<Available>(agent.network!!)
+ callback.expect<CapabilitiesChanged>(agent.network!!) {
+ if (expectUidsPresent) {
+ it.caps.allowedUidsNoCopy.getSingleElement() == uid
+ } else {
+ it.caps.allowedUidsNoCopy.isEmpty()
+ }
+ }
+ agent.unregister()
+ // callback will be unregistered in tearDown()
+ }
+
+ private fun setHoldCarrierPrivilege(hold: Boolean, subId: Int) {
+ fun getCertHash(): String {
+ val pkgInfo = realContext.packageManager.getPackageInfo(realContext.opPackageName,
+ PackageManager.GET_SIGNATURES)
+ val digest = MessageDigest.getInstance("SHA-256")
+ val certHash = digest.digest(pkgInfo.signatures!![0]!!.toByteArray())
+ return UiccUtil.bytesToHexString(certHash)!!
+ }
+
+ val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+ val ccm = realContext.getSystemService(CarrierConfigManager::class.java)!!
+
+ val cv = ConditionVariable()
+ val cpb = PrivilegeWaiterCallback(cv)
+ tryTest {
+ val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
+ }
+ // Wait for the callback to be registered
+ assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't register CarrierPrivilegesCallback")
+ if (cpb.hasPrivilege == hold) {
+ if (hold) {
+ Log.w(TAG, "Package ${realContext.opPackageName} already is privileged")
+ } else {
+ Log.w(TAG, "Package ${realContext.opPackageName} already isn't privileged")
+ }
+ return@tryTest
+ }
+ cv.close()
+ runAsShell(MODIFY_PHONE_STATE) {
+ val carrierConfigs = if (hold) {
+ PersistableBundle().also {
+ it.putStringArray(CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY,
+ arrayOf(getCertHash()))
+ }
+ } else {
+ null
+ }
+ ccm.overrideConfig(subId, carrierConfigs)
+ }
+ assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't change carrier privilege")
+ } cleanup {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ tm.unregisterCarrierPrivilegesCallback(cpb)
+ }
+ }
+ }
+
+ private fun acquireCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(true, subId)
+ private fun dropCarrierPrivilege(subId: Int) = setHoldCarrierPrivilege(false, subId)
+
+ private fun setCarrierServicePackageOverride(subId: Int, pkg: String?) {
+ val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+
+ val cv = ConditionVariable()
+ val cpb = CarrierServiceChangedWaiterCallback(cv)
+ tryTest {
+ val slotIndex = SubscriptionManager.getSlotIndex(subId)!!
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ tm.registerCarrierPrivilegesCallback(slotIndex, { it.run() }, cpb)
+ }
+ // Wait for the callback to be registered
+ assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't register CarrierPrivilegesCallback")
+ if (cpb.pkgName == pkg) {
+ Log.w(TAG, "Carrier service package was already $pkg")
+ return@tryTest
+ }
+ cv.close()
+ runAsShell(MODIFY_PHONE_STATE) {
+ if (null == pkg) {
+ // There is a bug is clear-carrier-service-package-override where not adding
+ // the -s argument will use the wrong slot index : b/299604822
+ runShellCommand("cmd phone clear-carrier-service-package-override" +
+ " -s $subId")
+ } else {
+ // -s could set the subId, but this test works with the default subId.
+ runShellCommand("cmd phone set-carrier-service-package-override $pkg")
+ }
+ }
+ assertTrue(cv.block(DEFAULT_TIMEOUT_MS), "Can't modify carrier service package")
+ } cleanup {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ tm.unregisterCarrierPrivilegesCallback(cpb)
+ }
+ }
+ }
+
+ private fun String.execute() = runShellCommand(this).trim()
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S)
+ fun testAllowedUids() {
+ // Use a different package than this one to make sure that a package that doesn't hold
+ // carrier service permission can be set as an allowed UID.
+ val servicePackage = "android.net.cts.carrierservicepackage"
+ val uid = try {
+ realContext.packageManager.getApplicationInfo(servicePackage, 0).uid
+ } catch (e: PackageManager.NameNotFoundException) {
+ fail("$servicePackage could not be installed, please check the SuiteApkInstaller" +
+ " installed CtsCarrierServicePackage.apk", e)
+ }
+
+ val tm = realContext.getSystemService(TelephonyManager::class.java)!!
+ val defaultSubId = SubscriptionManager.getDefaultSubscriptionId()
+ tryTest {
+ // This process is not the carrier service UID, so allowedUids should be ignored in all
+ // the following cases.
+ doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = false)
+ doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = false)
+ doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
+
+ // The tools to set the carrier service package override do not exist before U,
+ // so there is no way to test the rest of this test on < U.
+ if (!SdkLevel.isAtLeastU()) return@tryTest
+ // Acquiring carrier privilege is necessary to override the carrier service package.
+ val defaultSlotIndex = SubscriptionManager.getSlotIndex(defaultSubId)
+ acquireCarrierPrivilege(defaultSubId)
+ setCarrierServicePackageOverride(defaultSubId, servicePackage)
+ val actualServicePackage: String? = runAsShell(READ_PRIVILEGED_PHONE_STATE) {
+ tm.getCarrierServicePackageNameForLogicalSlot(defaultSlotIndex)
+ }
+ assertEquals(servicePackage, actualServicePackage)
+
+ // Wait for CarrierServiceAuthenticator to have seen the update of the service package
+ val timeout = SystemClock.elapsedRealtime() + DEFAULT_TIMEOUT_MS
+ while (true) {
+ if (SystemClock.elapsedRealtime() > timeout) {
+ fail("Couldn't make $servicePackage the service package for $defaultSubId: " +
+ "dumpsys connectivity".execute().split("\n")
+ .filter { it.contains("Logical slot = $defaultSlotIndex.*") })
+ }
+ if ("dumpsys connectivity"
+ .execute()
+ .split("\n")
+ .filter { it.contains("Logical slot = $defaultSlotIndex : uid = $uid") }
+ .isNotEmpty()) {
+ // Found the configuration
+ break
+ }
+ Thread.sleep(500)
+ }
+
+ // Cell and WiFi are allowed to set UIDs, but not Bluetooth or agents with multiple
+ // transports.
+ doTestAllowedUids(defaultSubId, TRANSPORT_CELLULAR, uid, expectUidsPresent = true)
+ if (SdkLevel.isAtLeastV()) {
+ // Cannot be tested before V because WifiInfo.Builder#setSubscriptionId doesn't
+ // exist
+ doTestAllowedUids(defaultSubId, TRANSPORT_WIFI, uid, expectUidsPresent = true)
+ }
+ doTestAllowedUids(defaultSubId, TRANSPORT_BLUETOOTH, uid, expectUidsPresent = false)
+ doTestAllowedUids(defaultSubId, intArrayOf(TRANSPORT_CELLULAR, TRANSPORT_WIFI), uid,
+ expectUidsPresent = false)
+ } cleanupStep {
+ if (SdkLevel.isAtLeastU()) setCarrierServicePackageOverride(defaultSubId, null)
+ } cleanup {
+ if (SdkLevel.isAtLeastU()) dropCarrierPrivilege(defaultSubId)
+ }
+ }
+
@Test
fun testRejectedUpdates() {
val callback = TestableNetworkCallback(DEFAULT_TIMEOUT_MS)
@@ -704,6 +951,7 @@
argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
any(LinkProperties::class.java),
any(NetworkCapabilities::class.java),
+ any(), // LocalNetworkConfig TODO : specify when it's public
any(NetworkScore::class.java),
any(NetworkAgentConfig::class.java),
eq(NetworkProvider.ID_NONE))
@@ -1496,3 +1744,25 @@
doTestNativeNetworkCreation(expectCreatedImmediately = true, intArrayOf(TRANSPORT_VPN))
}
}
+
+// Subclasses of CarrierPrivilegesCallback can't be inline, or they'll be compiled as
+// inner classes of the test class and will fail resolution on R as the test harness
+// uses reflection to list all methods and classes
+class PrivilegeWaiterCallback(private val cv: ConditionVariable) :
+ CarrierPrivilegesCallback {
+ var hasPrivilege = false
+ override fun onCarrierPrivilegesChanged(p: MutableSet<String>, uids: MutableSet<Int>) {
+ hasPrivilege = uids.contains(Process.myUid())
+ cv.open()
+ }
+}
+
+class CarrierServiceChangedWaiterCallback(private val cv: ConditionVariable) :
+ CarrierPrivilegesCallback {
+ var pkgName: String? = null
+ override fun onCarrierPrivilegesChanged(p: MutableSet<String>, u: MutableSet<Int>) {}
+ override fun onCarrierServiceChanged(pkgName: String?, uid: Int) {
+ this.pkgName = pkgName
+ cv.open()
+ }
+}
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index 637ed26..594f3fb 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -19,8 +19,10 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
@@ -28,6 +30,8 @@
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.testutils.DevSdkIgnoreRuleKt.VANILLA_ICE_CREAM;
+
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertArrayEquals;
@@ -104,6 +108,23 @@
verifyNoCapabilities(nr);
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testForbiddenCapabilities() {
+ final NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+ assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ builder.removeForbiddenCapability(NET_CAPABILITY_MMS);
+ assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.addCapability(NET_CAPABILITY_MMS);
+ assertFalse(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ assertTrue(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.addForbiddenCapability(NET_CAPABILITY_MMS);
+ assertTrue(builder.build().hasForbiddenCapability(NET_CAPABILITY_MMS));
+ assertFalse(builder.build().hasCapability(NET_CAPABILITY_MMS));
+ builder.clearCapabilities();
+ verifyNoCapabilities(builder.build());
+ }
+
@Test @IgnoreUpTo(Build.VERSION_CODES.Q)
public void testTemporarilyNotMeteredCapability() {
assertTrue(new NetworkRequest.Builder()
@@ -472,6 +493,32 @@
assertArrayEquals(netCapabilities, nr.getCapabilities());
}
+ @Test @IgnoreUpTo(VANILLA_ICE_CREAM)
+ public void testDefaultCapabilities() {
+ final NetworkRequest defaultNR = new NetworkRequest.Builder().build();
+ assertTrue(defaultNR.hasForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK));
+ assertFalse(defaultNR.hasCapability(NET_CAPABILITY_LOCAL_NETWORK));
+ assertTrue(defaultNR.hasCapability(NET_CAPABILITY_NOT_VPN));
+
+ final NetworkCapabilities emptyNC =
+ NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
+ assertFalse(defaultNR.canBeSatisfiedBy(emptyNC));
+
+ // defaultNC represent the capabilities of a network agent, so they must not contain
+ // forbidden capabilities by default.
+ final NetworkCapabilities defaultNC = new NetworkCapabilities.Builder().build();
+ assertArrayEquals(new int[0], defaultNC.getForbiddenCapabilities());
+ // A default NR can be satisfied by default NC.
+ assertTrue(defaultNR.canBeSatisfiedBy(defaultNC));
+
+ // Conversely, network requests have forbidden capabilities by default to manage
+ // backward compatibility, so test that these forbidden capabilities are in place.
+ // Starting in V, NET_CAPABILITY_LOCAL_NETWORK is introduced but is not seen by
+ // default, thanks to a default forbidden capability in NetworkRequest.
+ defaultNC.addCapability(NET_CAPABILITY_LOCAL_NETWORK);
+ assertFalse(defaultNR.canBeSatisfiedBy(defaultNC));
+ }
+
@Test
public void testBuildRequestFromExistingRequestWithBuilder() {
assumeTrue(TestUtils.shouldTestSApis());
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 9c44a3e..e1ea2b9 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -80,6 +80,7 @@
import com.android.net.module.util.PacketBuilder
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
@@ -843,6 +844,8 @@
checkConnectSocketToMdnsd(shouldFail = false)
}
+ // Native mdns powered by Netd is removed after U.
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test @CtsNetTestCasesMaxTargetSdk30("Socket is started with the service up to target SDK 30")
fun testManagerCreatesLegacySocket() {
nsdManager // Ensure the lazy-init member is initialized, so NsdManager is created
diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
index 5c7b5ca..f343e83 100644
--- a/tests/cts/net/src/android/net/cts/VpnServiceTest.java
+++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
@@ -15,12 +15,28 @@
*/
package android.net.cts;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.net.VpnService;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.AppModeFull;
import android.test.AndroidTestCase;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
import java.io.File;
import java.net.DatagramSocket;
import java.net.Socket;
@@ -30,12 +46,21 @@
* blocks us from writing tests for positive cases. For now we only test for
* negative cases, and we will try to cover the rest in the future.
*/
-public class VpnServiceTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class VpnServiceTest {
private static final String TAG = VpnServiceTest.class.getSimpleName();
+ private final Context mContext = InstrumentationRegistry.getContext();
private VpnService mVpnService = new VpnService();
+ @Before
+ public void setUp() {
+ assumeFalse("Skipping test because watches don't support VPN",
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH));
+ }
+
+ @Test
@AppModeFull(reason = "PackageManager#queryIntentActivities cannot access in instant app mode")
public void testPrepare() throws Exception {
// Should never return null since we are not prepared.
@@ -47,6 +72,7 @@
assertEquals(1, count);
}
+ @Test
@AppModeFull(reason = "establish() requires prepare(), which requires PackageManager access")
public void testEstablish() throws Exception {
ParcelFileDescriptor descriptor = null;
@@ -63,6 +89,7 @@
}
}
+ @Test
@AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
public void testProtect_DatagramSocket() throws Exception {
DatagramSocket socket = new DatagramSocket();
@@ -78,6 +105,7 @@
}
}
+ @Test
@AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
public void testProtect_Socket() throws Exception {
Socket socket = new Socket();
@@ -93,6 +121,7 @@
}
}
+ @Test
@AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
public void testProtect_int() throws Exception {
DatagramSocket socket = new DatagramSocket();
@@ -114,6 +143,7 @@
}
}
+ @Test
public void testTunDevice() throws Exception {
File file = new File("/dev/tun");
assertTrue(file.exists());
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 12919ae..f705e34 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -45,6 +45,7 @@
// order-dependent setup.
"NetworkStackApiStableLib",
"androidx.test.ext.junit",
+ "compatibility-device-util-axt",
"frameworks-net-integration-testutils",
"kotlin-reflect",
"mockito-target-extended-minus-junit4",
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
index 50f02d3..cea83c7 100644
--- a/tests/integration/AndroidManifest.xml
+++ b/tests/integration/AndroidManifest.xml
@@ -40,6 +40,8 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<!-- Querying the resources package -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+ <!-- Register UidFrozenStateChangedCallback -->
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
<application android:debuggable="true">
<uses-library android:name="android.test.runner"/>
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index e264b55..9b082a4 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -40,11 +40,14 @@
import android.os.IBinder
import android.os.SystemConfigManager
import android.os.UserHandle
+import android.os.VintfRuntimeInfo
import android.testing.TestableContext
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil
import com.android.connectivity.resources.R
+import com.android.net.module.util.BpfUtils
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper
@@ -53,6 +56,7 @@
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.ProxyTracker
+import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
import kotlin.test.assertEquals
@@ -60,6 +64,7 @@
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.After
+import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
@@ -302,4 +307,25 @@
!it.hasCapability(NET_CAPABILITY_VALIDATED)
}
}
+
+ private fun isBpfGetCgroupProgramIdSupportedByKernel(): Boolean {
+ val kVersionString = VintfRuntimeInfo.getKernelVersion()
+ return DeviceInfoUtils.compareMajorMinorVersion(kVersionString, "4.19") >= 0
+ }
+
+ @Test
+ fun testBpfProgramAttachStatus() {
+ Assume.assumeTrue(isBpfGetCgroupProgramIdSupportedByKernel())
+
+ listOf(
+ BpfUtils.BPF_CGROUP_INET_INGRESS,
+ BpfUtils.BPF_CGROUP_INET_EGRESS,
+ BpfUtils.BPF_CGROUP_INET_SOCK_CREATE
+ ).forEach {
+ val ret = SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(),
+ "cmd connectivity bpf-get-cgroup-program-id $it").trim()
+
+ assertTrue(Integer.parseInt(ret) > 0, "Unexpected output $ret for type $it")
+ }
+ }
}
diff --git a/tests/mts/bpf_existence_test.cpp b/tests/mts/bpf_existence_test.cpp
index 0c424e9..51a4eca 100644
--- a/tests/mts/bpf_existence_test.cpp
+++ b/tests/mts/bpf_existence_test.cpp
@@ -40,6 +40,7 @@
#define TETHERING "/sys/fs/bpf/tethering/"
#define PRIVATE "/sys/fs/bpf/net_private/"
#define SHARED "/sys/fs/bpf/net_shared/"
+#define NETD_RO "/sys/fs/bpf/netd_readonly/"
#define NETD "/sys/fs/bpf/netd_shared/"
class BpfExistenceTest : public ::testing::Test {
@@ -93,6 +94,7 @@
NETD "map_netd_app_uid_stats_map",
NETD "map_netd_configuration_map",
NETD "map_netd_cookie_tag_map",
+ NETD "map_netd_data_saver_enabled_map",
NETD "map_netd_iface_index_name_map",
NETD "map_netd_iface_stats_map",
NETD "map_netd_ingress_discard_map",
@@ -119,9 +121,9 @@
};
// Provided by *current* mainline module for T+ devices with 5.4+ kernels
-static const set<string> MAINLINE_FOR_T_5_4_PLUS = {
- SHARED "prog_block_bind4_block_port",
- SHARED "prog_block_bind6_block_port",
+static const set<string> MAINLINE_FOR_T_4_19_PLUS = {
+ NETD_RO "prog_block_bind4_block_port",
+ NETD_RO "prog_block_bind6_block_port",
};
// Provided by *current* mainline module for T+ devices with 5.15+ kernels
@@ -176,7 +178,7 @@
// T still only requires Linux Kernel 4.9+.
DO_EXPECT(IsAtLeastT(), MAINLINE_FOR_T_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 14, 0), MAINLINE_FOR_T_4_14_PLUS);
- DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 4, 0), MAINLINE_FOR_T_5_4_PLUS);
+ DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(4, 19, 0), MAINLINE_FOR_T_4_19_PLUS);
DO_EXPECT(IsAtLeastT() && isAtLeastKernelVersion(5, 15, 0), MAINLINE_FOR_T_5_15_PLUS);
// U requires Linux Kernel 4.14+, but nothing (as yet) added or removed in U.
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index e4669cb..669b76a 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -27,6 +27,12 @@
result = mUidOwnerMap.init(UID_OWNER_MAP_PATH);
EXPECT_RESULT_OK(result) << "init mUidOwnerMap failed";
+
+ // Do not check whether DATA_SAVER_ENABLED_MAP_PATH init succeeded or failed since the map is
+ // defined in tethering module, but the user of this class may be in other modules. For example,
+ // DNS resolver tests statically link to this class. But when running MTS, the test infra
+ // installs only DNS resolver module without installing tethering module together.
+ mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH);
}
Firewall* Firewall::getInstance() {
@@ -53,9 +59,11 @@
Result<void> Firewall::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
// iif should be non-zero if and only if match == MATCH_IIF
if (match == IIF_MATCH && iif == 0) {
- return Errorf("Interface match {} must have nonzero interface index", match);
+ return Errorf("Interface match {} must have nonzero interface index",
+ static_cast<int>(match));
} else if (match != IIF_MATCH && iif != 0) {
- return Errorf("Non-interface match {} must have zero interface index", match);
+ return Errorf("Non-interface match {} must have zero interface index",
+ static_cast<int>(match));
}
std::lock_guard guard(mMutex);
@@ -116,3 +124,28 @@
}
return {};
}
+
+Result<bool> Firewall::getDataSaverSetting() {
+ std::lock_guard guard(mMutex);
+ if (!mDataSaverEnabledMap.isValid()) {
+ return Errorf("init mDataSaverEnabledMap failed");
+ }
+
+ auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
+ if (!dataSaverSetting.ok()) {
+ return Errorf("Cannot read the data saver setting: {}", dataSaverSetting.error().message());
+ }
+ return dataSaverSetting;
+}
+
+Result<void> Firewall::setDataSaver(bool enabled) {
+ std::lock_guard guard(mMutex);
+ if (!mDataSaverEnabledMap.isValid()) {
+ return Errorf("init mDataSaverEnabledMap failed");
+ }
+
+ auto res = mDataSaverEnabledMap.writeValue(DATA_SAVER_ENABLED_KEY, enabled, BPF_EXIST);
+ if (!res.ok()) return Errorf("Failed to set data saver: {}", res.error().message());
+
+ return {};
+}
diff --git a/tests/native/utilities/firewall.h b/tests/native/utilities/firewall.h
index 1e7e987..b3d69bf 100644
--- a/tests/native/utilities/firewall.h
+++ b/tests/native/utilities/firewall.h
@@ -33,9 +33,11 @@
Result<void> removeRule(uint32_t uid, UidOwnerMatchType match) EXCLUDES(mMutex);
Result<void> addUidInterfaceRules(const std::string& ifName, const std::vector<int32_t>& uids);
Result<void> removeUidInterfaceRules(const std::vector<int32_t>& uids);
-
+ Result<bool> getDataSaverSetting();
+ Result<void> setDataSaver(bool enabled);
private:
BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
+ BpfMap<uint32_t, bool> mDataSaverEnabledMap GUARDED_BY(mMutex);
std::mutex mMutex;
};
diff --git a/tests/unit/java/android/net/BpfNetMapsReaderTest.kt b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
new file mode 100644
index 0000000..9de7f4d
--- /dev/null
+++ b/tests/unit/java/android/net/BpfNetMapsReaderTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED
+import android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY
+import android.net.BpfNetMapsConstants.DOZABLE_MATCH
+import android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH
+import android.net.BpfNetMapsConstants.PENALTY_BOX_MATCH
+import android.net.BpfNetMapsConstants.STANDBY_MATCH
+import android.net.BpfNetMapsConstants.UID_RULES_CONFIGURATION_KEY
+import android.net.BpfNetMapsUtils.getMatchByFirewallChain
+import android.os.Build.VERSION_CODES
+import com.android.net.module.util.IBpfMap
+import com.android.net.module.util.Struct.S32
+import com.android.net.module.util.Struct.U32
+import com.android.net.module.util.Struct.U8
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.TestBpfMap
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TEST_UID1 = 1234
+private const val TEST_UID2 = TEST_UID1 + 1
+private const val TEST_UID3 = TEST_UID2 + 1
+private const val NO_IIF = 0
+
+// pre-T devices does not support Bpf.
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(VERSION_CODES.S_V2)
+class BpfNetMapsReaderTest {
+ @Rule
+ @JvmField
+ val ignoreRule = DevSdkIgnoreRule()
+
+ private val testConfigurationMap: IBpfMap<S32, U32> = TestBpfMap()
+ private val testUidOwnerMap: IBpfMap<S32, UidOwnerValue> = TestBpfMap()
+ private val testDataSaverEnabledMap: IBpfMap<S32, U8> = TestBpfMap()
+ private val bpfNetMapsReader = BpfNetMapsReader(
+ TestDependencies(testConfigurationMap, testUidOwnerMap, testDataSaverEnabledMap))
+
+ class TestDependencies(
+ private val configMap: IBpfMap<S32, U32>,
+ private val uidOwnerMap: IBpfMap<S32, UidOwnerValue>,
+ private val dataSaverEnabledMap: IBpfMap<S32, U8>
+ ) : BpfNetMapsReader.Dependencies() {
+ override fun getConfigurationMap() = configMap
+ override fun getUidOwnerMap() = uidOwnerMap
+ override fun getDataSaverEnabledMap() = dataSaverEnabledMap
+ }
+
+ private fun doTestIsChainEnabled(chain: Int) {
+ testConfigurationMap.updateEntry(
+ UID_RULES_CONFIGURATION_KEY,
+ U32(getMatchByFirewallChain(chain))
+ )
+ assertTrue(bpfNetMapsReader.isChainEnabled(chain))
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ assertFalse(bpfNetMapsReader.isChainEnabled(chain))
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testIsChainEnabled() {
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_RESTRICTED)
+ doTestIsChainEnabled(ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY)
+ }
+
+ @Test
+ fun testFirewallChainList() {
+ // Verify that when a firewall chain constant is added, it should also be included in
+ // firewall chain list.
+ val declaredChains = ConnectivityManager::class.java.declaredFields.filter {
+ Modifier.isStatic(it.modifiers) && it.name.startsWith("FIREWALL_CHAIN_")
+ }
+ // Verify the size matches, this also verifies no common item in allow and deny chains.
+ assertEquals(BpfNetMapsConstants.ALLOW_CHAINS.size +
+ BpfNetMapsConstants.DENY_CHAINS.size, declaredChains.size)
+ declaredChains.forEach {
+ assertTrue(BpfNetMapsConstants.ALLOW_CHAINS.contains(it.get(null)) ||
+ BpfNetMapsConstants.DENY_CHAINS.contains(it.get(null)))
+ }
+ }
+
+ private fun mockChainEnabled(chain: Int, enabled: Boolean) {
+ val config = testConfigurationMap.getValue(UID_RULES_CONFIGURATION_KEY).`val`
+ val newConfig = if (enabled) {
+ config or getMatchByFirewallChain(chain)
+ } else {
+ config and getMatchByFirewallChain(chain).inv()
+ }
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(newConfig))
+ }
+
+ fun isUidNetworkingBlocked(uid: Int, metered: Boolean = false, dataSaver: Boolean = false) =
+ bpfNetMapsReader.isUidNetworkingBlocked(uid, metered, dataSaver)
+
+ @Test
+ fun testIsUidNetworkingBlockedByFirewallChains_allowChain() {
+ // With everything disabled by default, verify the return value is false.
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ assertFalse(isUidNetworkingBlocked(TEST_UID1))
+
+ // Enable dozable chain but does not provide allowed list. Verify the network is blocked
+ // for all uids.
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
+ assertTrue(isUidNetworkingBlocked(TEST_UID1))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2))
+
+ // Add uid1 to dozable allowed list. Verify the network is not blocked for uid1, while
+ // uid2 is blocked.
+ testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, DOZABLE_MATCH))
+ assertFalse(isUidNetworkingBlocked(TEST_UID1))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2))
+ }
+
+ @Test
+ fun testIsUidNetworkingBlockedByFirewallChains_denyChain() {
+ // Enable standby chain but does not provide denied list. Verify the network is allowed
+ // for all uids.
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
+ assertFalse(isUidNetworkingBlocked(TEST_UID1))
+ assertFalse(isUidNetworkingBlocked(TEST_UID2))
+
+ // Add uid1 to standby allowed list. Verify the network is blocked for uid1, while
+ // uid2 is not blocked.
+ testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, STANDBY_MATCH))
+ assertTrue(isUidNetworkingBlocked(TEST_UID1))
+ assertFalse(isUidNetworkingBlocked(TEST_UID2))
+ }
+
+ @Test
+ fun testIsUidNetworkingBlockedByFirewallChains_blockedWithAllowed() {
+ // Uids blocked by powersave chain but allowed by standby chain, verify the blocking
+ // takes higher priority.
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_POWERSAVE, true)
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_STANDBY, true)
+ assertTrue(isUidNetworkingBlocked(TEST_UID1))
+ }
+
+ @IgnoreUpTo(VERSION_CODES.S_V2)
+ @Test
+ fun testIsUidNetworkingBlockedByDataSaver() {
+ // With everything disabled by default, verify the return value is false.
+ testConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, U32(0))
+ assertFalse(isUidNetworkingBlocked(TEST_UID1, metered = true))
+
+ // Add uid1 to penalty box, verify the network is blocked for uid1, while uid2 is not
+ // affected.
+ testUidOwnerMap.updateEntry(S32(TEST_UID1), UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH))
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+
+ // Enable data saver, verify the network is blocked for uid1, uid2, but uid3 in happy box
+ // is not affected.
+ testUidOwnerMap.updateEntry(S32(TEST_UID3), UidOwnerValue(NO_IIF, HAPPY_BOX_MATCH))
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+
+ // Add uid1 to happy box as well, verify nothing is changed because penalty box has higher
+ // priority.
+ testUidOwnerMap.updateEntry(
+ S32(TEST_UID1),
+ UidOwnerValue(NO_IIF, PENALTY_BOX_MATCH or HAPPY_BOX_MATCH)
+ )
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+
+ // Enable doze mode, verify uid3 is blocked even if it is in happy box.
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, true)
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID2, metered = true, dataSaver = true))
+ assertTrue(isUidNetworkingBlocked(TEST_UID3, metered = true, dataSaver = true))
+
+ // Disable doze mode and data saver, only uid1 which is in penalty box is blocked.
+ mockChainEnabled(ConnectivityManager.FIREWALL_CHAIN_DOZABLE, false)
+ assertTrue(isUidNetworkingBlocked(TEST_UID1, metered = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID2, metered = true))
+ assertFalse(isUidNetworkingBlocked(TEST_UID3, metered = true))
+
+ // Make the network non-metered, nothing is blocked.
+ assertFalse(isUidNetworkingBlocked(TEST_UID1))
+ assertFalse(isUidNetworkingBlocked(TEST_UID2))
+ assertFalse(isUidNetworkingBlocked(TEST_UID3))
+ }
+
+ @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testGetDataSaverEnabled() {
+ testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_DISABLED))
+ assertFalse(bpfNetMapsReader.dataSaverEnabled)
+ testDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, U8(DATA_SAVER_ENABLED))
+ assertTrue(bpfNetMapsReader.dataSaverEnabled)
+ }
+}
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 45a9dbc..0082af2 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,6 +16,13 @@
package android.net;
+import static android.content.Context.RECEIVER_NOT_EXPORTED;
+import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
+import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -39,6 +46,7 @@
import static com.android.testutils.MiscAsserts.assertThrows;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -51,6 +59,7 @@
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -61,7 +70,10 @@
import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
+import android.net.ConnectivityManager.DataSaverStatusTracker;
import android.net.ConnectivityManager.NetworkCallback;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
@@ -78,6 +90,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -90,11 +103,14 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(VERSION_CODES.R)
public class ConnectivityManagerTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private static final int TIMEOUT_MS = 30_000;
private static final int SHORT_TIMEOUT_MS = 150;
@Mock Context mCtx;
@Mock IConnectivityManager mService;
+ @Mock NetworkPolicyManager mNpm;
@Before
public void setUp() {
@@ -510,4 +526,55 @@
assertNull("ConnectivityManager weak reference still not null after " + attempts
+ " attempts", ref.get());
}
+
+ @DevSdkIgnoreRule.IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ public void testDataSaverStatusTracker() {
+ mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
+ // Mock proper application info.
+ doReturn(mCtx).when(mCtx).getApplicationContext();
+ final ApplicationInfo mockAppInfo = new ApplicationInfo();
+ mockAppInfo.flags = FLAG_PERSISTENT | FLAG_SYSTEM;
+ doReturn(mockAppInfo).when(mCtx).getApplicationInfo();
+ // Enable data saver.
+ doReturn(RESTRICT_BACKGROUND_STATUS_ENABLED).when(mNpm)
+ .getRestrictBackgroundStatus(anyInt());
+
+ final DataSaverStatusTracker tracker = new DataSaverStatusTracker(mCtx);
+ // Verify the data saver status is correct right after initialization.
+ assertTrue(tracker.getDataSaverEnabled());
+
+ // Verify the tracker register receiver with expected intent filter.
+ final ArgumentCaptor<IntentFilter> intentFilterCaptor =
+ ArgumentCaptor.forClass(IntentFilter.class);
+ verify(mCtx).registerReceiver(
+ any(), intentFilterCaptor.capture(), eq(RECEIVER_NOT_EXPORTED));
+ assertEquals(ACTION_RESTRICT_BACKGROUND_CHANGED,
+ intentFilterCaptor.getValue().getAction(0));
+
+ // Mock data saver status changed event and verify the tracker tracks the
+ // status accordingly.
+ doReturn(RESTRICT_BACKGROUND_STATUS_DISABLED).when(mNpm)
+ .getRestrictBackgroundStatus(anyInt());
+ tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
+ assertFalse(tracker.getDataSaverEnabled());
+
+ doReturn(RESTRICT_BACKGROUND_STATUS_WHITELISTED).when(mNpm)
+ .getRestrictBackgroundStatus(anyInt());
+ tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
+ assertTrue(tracker.getDataSaverEnabled());
+ }
+
+ private <T> void mockService(Class<T> clazz, String name, T service) {
+ doReturn(service).when(mCtx).getSystemService(name);
+ doReturn(name).when(mCtx).getSystemServiceName(clazz);
+
+ // If the test suite uses the inline mock maker library, such as for coverage tests,
+ // then the final version of getSystemService must also be mocked, as the real
+ // method will not be called by the test and null object is returned since no mock.
+ // Otherwise, mocking a final method will fail the test.
+ if (mCtx.getSystemService(clazz) == null) {
+ doReturn(service).when(mCtx).getSystemService(clazz);
+ }
+ }
}
diff --git a/tests/unit/java/android/net/MulticastRoutingConfigTest.kt b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
new file mode 100644
index 0000000..f01057b
--- /dev/null
+++ b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet6Address
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+import android.net.MulticastRoutingConfig.Builder
+import android.net.MulticastRoutingConfig.FORWARD_NONE
+import android.net.MulticastRoutingConfig.FORWARD_SELECTED
+import android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingConfigTest {
+
+ val address1 = Inet6Address.getByName("2000::8888") as Inet6Address
+ val address2 = Inet6Address.getByName("2000::9999") as Inet6Address
+
+ private fun configNone() = Builder(FORWARD_NONE).build()
+ private fun configMinScope(scope: Int) = Builder(FORWARD_WITH_MIN_SCOPE, scope).build()
+ private fun configSelected() = Builder(FORWARD_SELECTED).build()
+ private fun configSelectedWithAddress1AndAddress2() =
+ Builder(FORWARD_SELECTED).addListeningAddress(address1)
+ .addListeningAddress(address2).build()
+ private fun configSelectedWithAddress2AndAddress1() =
+ Builder(FORWARD_SELECTED).addListeningAddress(address2)
+ .addListeningAddress(address1).build()
+
+ @Test
+ fun equalityTests() {
+
+ assertTrue(configNone().equals(configNone()))
+
+ assertTrue(configSelected().equals(configSelected()))
+
+ assertTrue(configMinScope(4).equals(configMinScope(4)))
+
+ assertTrue(configSelectedWithAddress1AndAddress2()
+ .equals(configSelectedWithAddress2AndAddress1()))
+ }
+
+ @Test
+ fun inequalityTests() {
+
+ assertFalse(configNone().equals(configSelected()))
+
+ assertFalse(configNone().equals(configMinScope(4)))
+
+ assertFalse(configSelected().equals(configMinScope(4)))
+
+ assertFalse(configMinScope(4).equals(configMinScope(5)))
+
+ assertFalse(configSelected().equals(configSelectedWithAddress1AndAddress2()))
+ }
+
+ @Test
+ fun toString_equalObjects_returnsEqualStrings() {
+ val config1 = configSelectedWithAddress1AndAddress2()
+ val config2 = configSelectedWithAddress2AndAddress1()
+
+ val str1 = config1.toString()
+ val str2 = config2.toString()
+
+ assertTrue(str1.equals(str2))
+ }
+
+ @Test
+ fun toString_unequalObjects_returnsUnequalStrings() {
+ val config1 = configSelected()
+ val config2 = configSelectedWithAddress1AndAddress2()
+
+ val str1 = config1.toString()
+ val str2 = config2.toString()
+
+ assertFalse(str1.equals(str2))
+ }
+}
diff --git a/tests/unit/java/android/net/NetworkStatsHistoryTest.java b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
index 2170882..1e1fd35 100644
--- a/tests/unit/java/android/net/NetworkStatsHistoryTest.java
+++ b/tests/unit/java/android/net/NetworkStatsHistoryTest.java
@@ -54,6 +54,7 @@
import com.android.frameworks.tests.net.R;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.SkipPresubmit;
import org.junit.After;
import org.junit.Test;
@@ -343,6 +344,7 @@
}
+ @SkipPresubmit(reason = "Flaky: b/302325928; add to presubmit after fixing")
@Test
public void testFuzzing() throws Exception {
try {
diff --git a/tests/unit/java/android/net/VpnManagerTest.java b/tests/unit/java/android/net/VpnManagerTest.java
index 532081a..2ab4e45 100644
--- a/tests/unit/java/android/net/VpnManagerTest.java
+++ b/tests/unit/java/android/net/VpnManagerTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeFalse;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
@@ -27,11 +28,13 @@
import android.content.ComponentName;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.test.mock.MockContext;
import android.util.SparseArray;
import androidx.test.filters.SmallTest;
+import androidx.test.InstrumentationRegistry;
import com.android.internal.net.VpnProfile;
import com.android.internal.util.MessageUtils;
@@ -47,6 +50,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class VpnManagerTest {
+
private static final String PKG_NAME = "fooPackage";
private static final String SESSION_NAME_STRING = "testSession";
@@ -66,6 +70,9 @@
@Before
public void setUp() throws Exception {
+ assumeFalse("Skipping test because watches don't support VPN",
+ InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_WATCH));
mMockService = mock(IVpnManager.class);
mVpnManager = new VpnManager(mMockContext, mMockService);
}
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 0965193..550a9ee 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -51,6 +51,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
diff --git a/tests/unit/java/com/android/internal/net/VpnProfileTest.java b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
index b2dff2e..acae7d2 100644
--- a/tests/unit/java/com/android/internal/net/VpnProfileTest.java
+++ b/tests/unit/java/com/android/internal/net/VpnProfileTest.java
@@ -26,6 +26,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -311,4 +312,12 @@
decoded.password = profile.password;
assertEquals(profile, decoded);
}
+
+ @Test
+ public void testClone() {
+ final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
+ final VpnProfile clone = profile.clone();
+ assertEquals(profile, clone);
+ assertNotSame(profile, clone);
+ }
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index 5f280c6..ea905d5 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -16,7 +16,12 @@
package com.android.server;
+import static android.net.BpfNetMapsConstants.ALLOW_CHAINS;
import static android.net.BpfNetMapsConstants.CURRENT_STATS_MAP_CONFIGURATION_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED_KEY;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_DISABLED;
+import static android.net.BpfNetMapsConstants.DATA_SAVER_ENABLED;
+import static android.net.BpfNetMapsConstants.DENY_CHAINS;
import static android.net.BpfNetMapsConstants.DOZABLE_MATCH;
import static android.net.BpfNetMapsConstants.HAPPY_BOX_MATCH;
import static android.net.BpfNetMapsConstants.IIF_MATCH;
@@ -66,6 +71,8 @@
import android.content.Context;
import android.net.BpfNetMapsUtils;
import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.UidOwnerValue;
import android.os.Build;
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
@@ -81,6 +88,8 @@
import com.android.net.module.util.Struct.U8;
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
+import com.android.net.module.util.bpf.IngressDiscardKey;
+import com.android.net.module.util.bpf.IngressDiscardValue;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -96,6 +105,8 @@
import java.io.FileDescriptor;
import java.io.StringWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.util.ArrayList;
import java.util.List;
@@ -114,21 +125,21 @@
private static final int TEST_IF_INDEX = 7;
private static final int NO_IIF = 0;
private static final int NULL_IIF = 0;
+ private static final Inet4Address TEST_V4_ADDRESS =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.0.2.1");
+ private static final Inet6Address TEST_V6_ADDRESS =
+ (Inet6Address) InetAddresses.parseNumericAddress("2001:db8::1");
private static final String CHAINNAME = "fw_dozable";
- private static final List<Integer> FIREWALL_CHAINS = List.of(
- FIREWALL_CHAIN_DOZABLE,
- FIREWALL_CHAIN_STANDBY,
- FIREWALL_CHAIN_POWERSAVE,
- FIREWALL_CHAIN_RESTRICTED,
- FIREWALL_CHAIN_LOW_POWER_STANDBY,
- FIREWALL_CHAIN_OEM_DENY_1,
- FIREWALL_CHAIN_OEM_DENY_2,
- FIREWALL_CHAIN_OEM_DENY_3
- );
private static final long STATS_SELECT_MAP_A = 0;
private static final long STATS_SELECT_MAP_B = 1;
+ private static final List<Integer> FIREWALL_CHAINS = new ArrayList<>();
+ static {
+ FIREWALL_CHAINS.addAll(ALLOW_CHAINS);
+ FIREWALL_CHAINS.addAll(DENY_CHAINS);
+ }
+
private BpfNetMaps mBpfNetMaps;
@Mock INetd mNetd;
@@ -140,13 +151,16 @@
private final IBpfMap<S32, U8> mUidPermissionMap = new TestBpfMap<>(S32.class, U8.class);
private final IBpfMap<CookieTagMapKey, CookieTagMapValue> mCookieTagMap =
spy(new TestBpfMap<>(CookieTagMapKey.class, CookieTagMapValue.class));
+ private final IBpfMap<S32, U8> mDataSaverEnabledMap = new TestBpfMap<>(S32.class, U8.class);
+ private final IBpfMap<IngressDiscardKey, IngressDiscardValue> mIngressDiscardMap =
+ new TestBpfMap<>(IngressDiscardKey.class, IngressDiscardValue.class);
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
doReturn(TEST_IF_INDEX).when(mDeps).getIfIndex(TEST_IF_NAME);
+ doReturn(TEST_IF_NAME).when(mDeps).getIfName(TEST_IF_INDEX);
doReturn(0).when(mDeps).synchronizeKernelRCU();
- BpfNetMaps.setEnableJavaBpfMapForTest(true /* enable */);
BpfNetMaps.setConfigurationMapForTest(mConfigurationMap);
mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(0));
mConfigurationMap.updateEntry(
@@ -154,6 +168,9 @@
BpfNetMaps.setUidOwnerMapForTest(mUidOwnerMap);
BpfNetMaps.setUidPermissionMapForTest(mUidPermissionMap);
BpfNetMaps.setCookieTagMapForTest(mCookieTagMap);
+ BpfNetMaps.setDataSaverEnabledMapForTest(mDataSaverEnabledMap);
+ mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(DATA_SAVER_DISABLED));
+ BpfNetMaps.setIngressDiscardMapForTest(mIngressDiscardMap);
mBpfNetMaps = new BpfNetMaps(mContext, mNetd, mDeps);
}
@@ -610,7 +627,7 @@
mUidOwnerMap.updateEntry(new S32(TEST_UID), new UidOwnerValue(TEST_IF_INDEX, IIF_MATCH));
for (final int chain: testChains) {
- final int ruleToAddMatch = mBpfNetMaps.isFirewallAllowList(chain)
+ final int ruleToAddMatch = BpfNetMapsUtils.isFirewallAllowList(chain)
? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
mBpfNetMaps.setUidRule(chain, TEST_UID, ruleToAddMatch);
}
@@ -618,7 +635,7 @@
checkUidOwnerValue(TEST_UID, TEST_IF_INDEX, IIF_MATCH | getMatch(testChains));
for (final int chain: testChains) {
- final int ruleToRemoveMatch = mBpfNetMaps.isFirewallAllowList(chain)
+ final int ruleToRemoveMatch = BpfNetMapsUtils.isFirewallAllowList(chain)
? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
mBpfNetMaps.setUidRule(chain, TEST_UID, ruleToRemoveMatch);
}
@@ -698,11 +715,11 @@
for (final int chain: FIREWALL_CHAINS) {
final String testCase = "EnabledChains: " + enableChains + " CheckedChain: " + chain;
if (enableChains.contains(chain)) {
- final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+ final int expectedRule = BpfNetMapsUtils.isFirewallAllowList(chain)
? FIREWALL_RULE_ALLOW : FIREWALL_RULE_DENY;
assertEquals(testCase, expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
} else {
- final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+ final int expectedRule = BpfNetMapsUtils.isFirewallAllowList(chain)
? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
assertEquals(testCase, expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
}
@@ -745,7 +762,7 @@
public void testGetUidRuleNoEntry() throws Exception {
mUidOwnerMap.clear();
for (final int chain: FIREWALL_CHAINS) {
- final int expectedRule = mBpfNetMaps.isFirewallAllowList(chain)
+ final int expectedRule = BpfNetMapsUtils.isFirewallAllowList(chain)
? FIREWALL_RULE_DENY : FIREWALL_RULE_ALLOW;
assertEquals(expectedRule, mBpfNetMaps.getUidRule(chain, TEST_UID));
}
@@ -1154,6 +1171,21 @@
assertDumpContains(getDump(), "cookie=123 tag=0x789 uid=456");
}
+ private void doTestDumpDataSaverConfig(final short value, final boolean expected)
+ throws Exception {
+ mDataSaverEnabledMap.updateEntry(DATA_SAVER_ENABLED_KEY, new U8(value));
+ assertDumpContains(getDump(),
+ "sDataSaverEnabledMap: " + expected);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDumpDataSaverConfig() throws Exception {
+ doTestDumpDataSaverConfig(DATA_SAVER_DISABLED, false);
+ doTestDumpDataSaverConfig(DATA_SAVER_ENABLED, true);
+ doTestDumpDataSaverConfig((short) 2, true);
+ }
+
@Test
public void testGetUids() throws ErrnoException {
final int uid0 = TEST_UIDS[0];
@@ -1182,4 +1214,73 @@
assertThrows(expected,
() -> mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_OEM_DENY_1));
}
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.S_V2)
+ public void testSetDataSaverEnabledBeforeT() {
+ for (boolean enable : new boolean[]{true, false}) {
+ assertThrows(UnsupportedOperationException.class,
+ () -> mBpfNetMaps.setDataSaverEnabled(enable));
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetDataSaverEnabled() throws Exception {
+ for (boolean enable : new boolean[]{true, false}) {
+ mBpfNetMaps.setDataSaverEnabled(enable);
+ assertEquals(enable ? DATA_SAVER_ENABLED : DATA_SAVER_DISABLED,
+ mDataSaverEnabledMap.getValue(DATA_SAVER_ENABLED_KEY).val);
+ }
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetIngressDiscardRule_V4address() throws Exception {
+ mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+ final IngressDiscardValue val = mIngressDiscardMap.getValue(new IngressDiscardKey(
+ TEST_V4_ADDRESS));
+ assertEquals(TEST_IF_INDEX, val.iif1);
+ assertEquals(TEST_IF_INDEX, val.iif2);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testSetIngressDiscardRule_V6address() throws Exception {
+ mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+ final IngressDiscardValue val =
+ mIngressDiscardMap.getValue(new IngressDiscardKey(TEST_V6_ADDRESS));
+ assertEquals(TEST_IF_INDEX, val.iif1);
+ assertEquals(TEST_IF_INDEX, val.iif2);
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testRemoveIngressDiscardRule() throws Exception {
+ mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+ mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+ final IngressDiscardKey v4Key = new IngressDiscardKey(TEST_V4_ADDRESS);
+ final IngressDiscardKey v6Key = new IngressDiscardKey(TEST_V6_ADDRESS);
+ assertTrue(mIngressDiscardMap.containsKey(v4Key));
+ assertTrue(mIngressDiscardMap.containsKey(v6Key));
+
+ mBpfNetMaps.removeIngressDiscardRule(TEST_V4_ADDRESS);
+ assertFalse(mIngressDiscardMap.containsKey(v4Key));
+ assertTrue(mIngressDiscardMap.containsKey(v6Key));
+
+ mBpfNetMaps.removeIngressDiscardRule(TEST_V6_ADDRESS);
+ assertFalse(mIngressDiscardMap.containsKey(v4Key));
+ assertFalse(mIngressDiscardMap.containsKey(v6Key));
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ public void testDumpIngressDiscardRule() throws Exception {
+ mBpfNetMaps.setIngressDiscardRule(TEST_V4_ADDRESS, TEST_IF_NAME);
+ mBpfNetMaps.setIngressDiscardRule(TEST_V6_ADDRESS, TEST_IF_NAME);
+ final String dump = getDump();
+ assertDumpContains(dump, TEST_V4_ADDRESS.getHostAddress());
+ assertDumpContains(dump, TEST_V6_ADDRESS.getHostAddress());
+ assertDumpContains(dump, TEST_IF_INDEX + "(" + TEST_IF_NAME + ")");
+ }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index eb03157..9fad766 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -31,6 +31,7 @@
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.STATUS_BAR_SERVICE;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -59,6 +60,7 @@
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
import static android.net.ConnectivityManager.EXTRA_REALTIME_NS;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
@@ -152,9 +154,12 @@
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 android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+import static android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
import static com.android.server.ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
+import static com.android.server.ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS;
import static com.android.server.ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION;
import static com.android.server.ConnectivityService.MAX_NETWORK_REQUESTS_PER_SYSTEM_UID;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_MOBILE_DATA_PREFERERRED;
@@ -416,9 +421,7 @@
import com.android.server.connectivity.QosCallbackTracker;
import com.android.server.connectivity.TcpKeepaliveController;
import com.android.server.connectivity.UidRangeUtils;
-import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.VpnProfileStore;
-import com.android.server.net.LockdownVpnTracker;
import com.android.server.net.NetworkPinner;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -470,6 +473,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -637,8 +641,8 @@
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
- final BatteryStatsManager mBatteryStatsManager =
- new BatteryStatsManager(mock(IBatteryStats.class));
+ final IBatteryStats mIBatteryStats = mock(IBatteryStats.class);
+ final BatteryStatsManager mBatteryStatsManager = new BatteryStatsManager(mIBatteryStats);
private ArgumentCaptor<ResolverParamsParcel> mResolverParamsParcelCaptor =
ArgumentCaptor.forClass(ResolverParamsParcel.class);
@@ -934,24 +938,39 @@
return appUid + (firstSdkSandboxUid - Process.FIRST_APPLICATION_UID);
}
- // This function assumes the UID range for user 0 ([1, 99999])
- private static UidRangeParcel[] uidRangeParcelsExcludingUids(Integer... excludedUids) {
- int start = 1;
- Arrays.sort(excludedUids);
- List<UidRangeParcel> parcels = new ArrayList<UidRangeParcel>();
+ // Create the list of ranges for the primary user (User 0), excluding excludedUids.
+ private static List<Range<Integer>> intRangesPrimaryExcludingUids(List<Integer> excludedUids) {
+ final List<Integer> excludedUidsList = new ArrayList<>(excludedUids);
+ // Uid 0 is always excluded
+ if (!excludedUidsList.contains(0)) {
+ excludedUidsList.add(0);
+ }
+ return intRangesExcludingUids(PRIMARY_USER, excludedUidsList);
+ }
+
+ private static List<Range<Integer>> intRangesExcludingUids(int userId,
+ List<Integer> excludedAppIds) {
+ final List<Integer> excludedUids = CollectionUtils.map(excludedAppIds,
+ appId -> UserHandle.getUid(userId, appId));
+ final int userBase = userId * UserHandle.PER_USER_RANGE;
+ final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
+
+ int start = userBase;
+ Collections.sort(excludedUids);
+ final List<Range<Integer>> ranges = new ArrayList<>();
for (int excludedUid : excludedUids) {
if (excludedUid == start) {
start++;
} else {
- parcels.add(new UidRangeParcel(start, excludedUid - 1));
+ ranges.add(new Range<>(start, excludedUid - 1));
start = excludedUid + 1;
}
}
- if (start <= 99999) {
- parcels.add(new UidRangeParcel(start, 99999));
+ if (start <= maxUid) {
+ ranges.add(new Range<>(start, maxUid));
}
- return parcels.toArray(new UidRangeParcel[0]);
+ return ranges;
}
private void waitForIdle() {
@@ -1481,13 +1500,8 @@
return uidRangesForUids(CollectionUtils.toIntArray(uids));
}
- private static Looper startHandlerThreadAndReturnLooper() {
- final HandlerThread handlerThread = new HandlerThread("MockVpnThread");
- handlerThread.start();
- return handlerThread.getLooper();
- }
-
- private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork {
+ // Helper class to mock vpn interaction.
+ private class MockVpn implements TestableNetworkCallback.HasNetwork {
// Note : Please do not add any new instrumentation here. If you need new instrumentation,
// please add it in CSTest and use subclasses of CSTest instead of adding more
// tools in ConnectivityServiceTest.
@@ -1495,52 +1509,22 @@
// Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
// not inherit from NetworkAgent.
private TestNetworkAgentWrapper mMockNetworkAgent;
+ // Initialize a stored NetworkCapabilities following the defaults of VPN. The TransportInfo
+ // should at least be updated to a valid VPN type before usage, see registerAgent(...).
+ private NetworkCapabilities mNetworkCapabilities = new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .setTransportInfo(new VpnTransportInfo(
+ VpnManager.TYPE_VPN_NONE,
+ null /* sessionId */,
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */))
+ .build();
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,
- new SystemServices(mServiceContext) {
- @Override
- public String settingsSecureGetStringForUser(String key, int userId) {
- switch (key) {
- // Settings keys not marked as @Readable are not readable from
- // non-privileged apps, unless marked as testOnly=true
- // (atest refuses to install testOnly=true apps), even if mocked
- // in the content provider, because
- // Settings.Secure.NameValueCache#getStringForUser checks the key
- // before querying the mock settings provider.
- case Settings.Secure.ALWAYS_ON_VPN_APP:
- return null;
- default:
- return super.settingsSecureGetStringForUser(key, userId);
- }
- }
- }, new Ikev2SessionCreator());
- }
+ private String mSessionKey;
public void setUids(Set<UidRange> uids) {
mNetworkCapabilities.setUids(UidRange.toIntRanges(uids));
@@ -1553,7 +1537,6 @@
mVpnType = vpnType;
}
- @Override
public Network getNetwork() {
return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
}
@@ -1562,7 +1545,6 @@
return null == mMockNetworkAgent ? null : mMockNetworkAgent.getNetworkAgentConfig();
}
- @Override
public int getActiveVpnType() {
return mVpnType;
}
@@ -1576,14 +1558,11 @@
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";
+ final String session = "MySession12345";
setUids(uids);
if (!isAlwaysMetered) mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
- mInterface = VPN_IFNAME;
mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(getActiveVpnType(),
- mConfig.session));
+ session));
mMockNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp,
mNetworkCapabilities);
mMockNetworkAgent.waitForIdle(TIMEOUT_MS);
@@ -1597,9 +1576,7 @@
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 {
@@ -1659,57 +1636,62 @@
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();
+ private void startLegacyVpn() {
+ // Do nothing.
}
- 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();
+ // Mock the interaction of IkeV2VpnRunner start. In the context of ConnectivityService,
+ // setVpnDefaultForUids() is the main interaction and a sessionKey is stored.
+ private void startPlatformVpn() {
+ mSessionKey = UUID.randomUUID().toString();
+ // Assuming no disallowed applications
+ final Set<Range<Integer>> ranges = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ mCm.setVpnDefaultForUids(mSessionKey, ranges);
+ // Wait for vpn network preference updates.
+ waitForIdle();
}
- @Override
- public void stopVpnRunnerPrivileged() {
- if (mVpnRunner != null) {
- super.stopVpnRunnerPrivileged();
- disconnect();
- mStartLegacyVpnCv = new ConditionVariable();
+ public void startLegacyVpnPrivileged(VpnProfile profile) {
+ switch (profile.type) {
+ case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
+ case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
+ case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
+ case VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS:
+ startPlatformVpn();
+ break;
+ case VpnProfile.TYPE_L2TP_IPSEC_PSK:
+ case VpnProfile.TYPE_L2TP_IPSEC_RSA:
+ case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
+ case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
+ case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
+ startLegacyVpn();
+ break;
+ default:
+ fail("Unknown VPN profile type");
}
- mVpnRunner = null;
- mStopVpnRunnerCv.open();
}
- public void expectStopVpnRunnerPrivileged() {
- assertTrue("stopVpnRunnerPrivileged not called after " + TIMEOUT_MS + " ms",
- mStopVpnRunnerCv.block(TIMEOUT_MS));
+ public void stopVpnRunnerPrivileged() {
+ if (mSessionKey != null) {
+ // Clear vpn network preference.
+ mCm.setVpnDefaultForUids(mSessionKey, Collections.EMPTY_LIST);
+ mSessionKey = null;
+ }
+ disconnect();
}
- @Override
- public synchronized UnderlyingNetworkInfo getUnderlyingNetworkInfo() {
- if (mUnderlyingNetworkInfo != null) return mUnderlyingNetworkInfo;
-
- return super.getUnderlyingNetworkInfo();
- }
-
- private synchronized void setUnderlyingNetworkInfo(
- UnderlyingNetworkInfo underlyingNetworkInfo) {
- mUnderlyingNetworkInfo = underlyingNetworkInfo;
+ public boolean setUnderlyingNetworks(@Nullable Network[] networks) {
+ if (!mAgentRegistered) return false;
+ mMockNetworkAgent.setUnderlyingNetworks(
+ (networks == null) ? null : Arrays.asList(networks));
+ return true;
}
}
@@ -1722,6 +1704,12 @@
return ranges.stream().map(r -> new UidRangeParcel(r, r)).toArray(UidRangeParcel[]::new);
}
+ private static UidRangeParcel[] intToUidRangeStableParcels(
+ final @NonNull List<Range<Integer>> ranges) {
+ return ranges.stream().map(
+ r -> new UidRangeParcel(r.getLower(), r.getUpper())).toArray(UidRangeParcel[]::new);
+ }
+
private void assertVpnTransportInfo(NetworkCapabilities nc, int type) {
assertNotNull(nc);
final TransportInfo ti = nc.getTransportInfo();
@@ -1736,11 +1724,6 @@
waitForIdle();
}
- private void mockVpn(int uid) {
- int userId = UserHandle.getUserId(uid);
- mMockVpn = new MockVpn(userId);
- }
-
private void mockUidNetworkingBlocked() {
doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
@@ -1854,6 +1837,8 @@
private static final UserHandle TERTIARY_USER_HANDLE = new UserHandle(TERTIARY_USER);
private static final int RESTRICTED_USER = 1;
+ private static final UidRange RESTRICTED_USER_UIDRANGE =
+ UidRange.createForUser(UserHandle.of(RESTRICTED_USER));
private static final UserInfo RESTRICTED_USER_INFO = new UserInfo(RESTRICTED_USER, "",
UserInfo.FLAG_RESTRICTED);
static {
@@ -1950,7 +1935,7 @@
mService.systemReadyInternal();
verify(mMockDnsResolver).registerUnsolicitedEventListener(any());
- mockVpn(Process.myUid());
+ mMockVpn = new MockVpn();
mCm.bindProcessToNetwork(null);
mQosCallbackTracker = mock(QosCallbackTracker.class);
@@ -2005,7 +1990,7 @@
}
@Override
- public HandlerThread makeHandlerThread() {
+ public HandlerThread makeHandlerThread(@NonNull final String tag) {
return mCsHandlerThread;
}
@@ -2167,6 +2152,16 @@
}
}
+ @Override
+ public boolean isFeatureNotChickenedOut(Context context, String name) {
+ switch (name) {
+ case ALLOW_SYSUI_CONNECTIVITY_REPORTS:
+ return true;
+ default:
+ return super.isFeatureNotChickenedOut(context, name);
+ }
+ }
+
public void setChangeIdEnabled(final boolean enabled, final long changeId, final int uid) {
final Pair<Long, Integer> data = new Pair<>(changeId, uid);
// mEnabledChangeIds is read on the handler thread and maybe the test thread, so
@@ -2259,6 +2254,11 @@
}
@Override
+ public int getBpfProgramId(final int attachType) {
+ return 0;
+ }
+
+ @Override
public BroadcastOptionsShim makeBroadcastOptionsShim(BroadcastOptions options) {
reset(mBroadcastOptionsShim);
return mBroadcastOptionsShim;
@@ -4301,7 +4301,9 @@
testFactory.terminate();
testFactory.assertNoRequestChanged();
if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback);
- handlerThread.quit();
+
+ handlerThread.quitSafely();
+ handlerThread.join();
}
@Test
@@ -4362,6 +4364,8 @@
expectNoRequestChanged(testFactoryAll); // still seeing the request
mWiFiAgent.disconnect();
+ handlerThread.quitSafely();
+ handlerThread.join();
}
@Test
@@ -4395,7 +4399,8 @@
}
}
}
- handlerThread.quit();
+ handlerThread.quitSafely();
+ handlerThread.join();
}
@Test
@@ -6016,7 +6021,8 @@
testFactory.assertNoRequestChanged();
} finally {
mCm.unregisterNetworkCallback(cellNetworkCallback);
- handlerThread.quit();
+ handlerThread.quitSafely();
+ handlerThread.join();
}
}
@@ -6606,7 +6612,8 @@
}
} finally {
testFactory.terminate();
- handlerThread.quit();
+ handlerThread.quitSafely();
+ handlerThread.join();
}
}
@@ -9404,11 +9411,11 @@
&& c.hasTransport(TRANSPORT_WIFI));
callback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
- doReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)).when(mPackageManager)
- .getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER);
-
- // New user added
- mMockVpn.onUserAdded(RESTRICTED_USER);
+ // New user added, this updates the Vpn uids, coverage in VpnTest.
+ // This is equivalent to `mMockVpn.onUserAdded(RESTRICTED_USER);`
+ final Set<UidRange> ranges = uidRangesForUids(uid);
+ ranges.add(RESTRICTED_USER_UIDRANGE);
+ mMockVpn.setUids(ranges);
// Expect that the VPN UID ranges contain both |uid| and the UID range for the newly-added
// restricted user.
@@ -9433,7 +9440,9 @@
&& !c.hasTransport(TRANSPORT_WIFI));
// User removed and expect to lose the UID range for the restricted user.
- mMockVpn.onUserRemoved(RESTRICTED_USER);
+ // This updates the Vpn uids, coverage in VpnTest.
+ // This is equivalent to `mMockVpn.onUserRemoved(RESTRICTED_USER);`
+ mMockVpn.setUids(uidRangesForUids(uid));
// 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.
@@ -9474,8 +9483,16 @@
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<>();
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ // Coverage in VpnTest.
+ final List<Integer> excludedUids = new ArrayList<>();
+ excludedUids.add(VPN_UID);
+ if (mDeps.isAtLeastT()) {
+ // On T onwards, the corresponding SDK sandbox UID should also be excluded
+ excludedUids.add(toSdkSandboxUid(VPN_UID));
+ }
+ final List<Range<Integer>> primaryRanges = intRangesPrimaryExcludingUids(excludedUids);
+ mCm.setRequireVpnForUids(true, primaryRanges);
+
waitForIdle();
assertNull(mCm.getActiveNetworkForUid(uid));
// This is arguably overspecified: a UID that is not running doesn't have an active network.
@@ -9484,32 +9501,28 @@
assertNotNull(mCm.getActiveNetworkForUid(restrictedUid));
// Start the restricted profile, and check that the UID within it loses network access.
- doReturn(UserHandle.getUid(RESTRICTED_USER, VPN_UID)).when(mPackageManager)
- .getPackageUidAsUser(ALWAYS_ON_PACKAGE, RESTRICTED_USER);
- doReturn(asList(PRIMARY_USER_INFO, RESTRICTED_USER_INFO)).when(mUserManager)
- .getAliveUsers();
// TODO: check that VPN app within restricted profile still has access, etc.
- mMockVpn.onUserAdded(RESTRICTED_USER);
- 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);
+ // Add a restricted user.
+ // This is equivalent to `mMockVpn.onUserAdded(RESTRICTED_USER);`, coverage in VpnTest.
+ final List<Range<Integer>> restrictedRanges =
+ intRangesExcludingUids(RESTRICTED_USER, excludedUids);
+ mCm.setRequireVpnForUids(true, restrictedRanges);
+ waitForIdle();
+
assertNull(mCm.getActiveNetworkForUid(uid));
assertNull(mCm.getActiveNetworkForUid(restrictedUid));
// Stop the restricted profile, and check that the UID within it has network access again.
- doReturn(asList(PRIMARY_USER_INFO)).when(mUserManager).getAliveUsers();
+ // Remove the restricted user.
+ // This is equivalent to `mMockVpn.onUserRemoved(RESTRICTED_USER);`, coverage in VpnTest.
+ mCm.setRequireVpnForUids(false, restrictedRanges);
+ waitForIdle();
- // Send a USER_REMOVED broadcast and expect to lose the UID range for the restricted user.
- mMockVpn.onUserRemoved(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));
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(false, primaryRanges);
+
waitForIdle();
}
@@ -9962,18 +9975,20 @@
new Handler(ConnectivityThread.getInstanceLooper()));
final int uid = Process.myUid();
- final ArrayList<String> allowList = new ArrayList<>();
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
- waitForIdle();
- final Set<Integer> excludedUids = new ArraySet<Integer>();
+ // Enable always-on VPN lockdown, coverage in VpnTest.
+ final List<Integer> excludedUids = new ArrayList<Integer>();
excludedUids.add(VPN_UID);
if (mDeps.isAtLeastT()) {
// On T onwards, the corresponding SDK sandbox UID should also be excluded
excludedUids.add(toSdkSandboxUid(VPN_UID));
}
- final UidRangeParcel[] uidRangeParcels = uidRangeParcelsExcludingUids(
- excludedUids.toArray(new Integer[0]));
+
+ final List<Range<Integer>> primaryRanges = intRangesPrimaryExcludingUids(excludedUids);
+ mCm.setRequireVpnForUids(true, primaryRanges);
+ waitForIdle();
+
+ final UidRangeParcel[] uidRangeParcels = intToUidRangeStableParcels(primaryRanges);
InOrder inOrder = inOrder(mMockNetd);
expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
@@ -9993,7 +10008,8 @@
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
// Disable lockdown, expect to see the network unblocked.
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(false, primaryRanges);
+ waitForIdle();
callback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
vpnUidCallback.assertNoCallback();
@@ -10006,22 +10022,25 @@
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);
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ // Add our UID to the allowlist, expect network is not blocked. Coverage in VpnTest.
+ excludedUids.add(uid);
+ if (mDeps.isAtLeastT()) {
+ // On T onwards, the corresponding SDK sandbox UID should also be excluded
+ excludedUids.add(toSdkSandboxUid(uid));
+ }
+ final List<Range<Integer>> primaryRangesExcludingUid =
+ intRangesPrimaryExcludingUids(excludedUids);
+ mCm.setRequireVpnForUids(true, primaryRangesExcludingUid);
+ waitForIdle();
+
callback.assertNoCallback();
defaultCallback.assertNoCallback();
vpnUidCallback.assertNoCallback();
vpnUidDefaultCallback.assertNoCallback();
vpnDefaultCallbackAsUid.assertNoCallback();
- excludedUids.add(uid);
- if (mDeps.isAtLeastT()) {
- // On T onwards, the corresponding SDK sandbox UID should also be excluded
- excludedUids.add(toSdkSandboxUid(uid));
- }
- final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs = uidRangeParcelsExcludingUids(
- excludedUids.toArray(new Integer[0]));
+ final UidRangeParcel[] uidRangeParcelsAlsoExcludingUs =
+ intToUidRangeStableParcels(primaryRangesExcludingUid);
expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcelsAlsoExcludingUs);
assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
@@ -10044,15 +10063,15 @@
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.
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ // Disable lockdown
+ mCm.setRequireVpnForUids(false, primaryRangesExcludingUid);
waitForIdle();
expectNetworkRejectNonSecureVpn(inOrder, false, uidRangeParcelsAlsoExcludingUs);
- allowList.clear();
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ // Remove our UID from the allowlist, and re-enable lockdown.
+ mCm.setRequireVpnForUids(true, primaryRanges);
waitForIdle();
expectNetworkRejectNonSecureVpn(inOrder, true, uidRangeParcels);
+ // Everything should now be blocked.
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> cb.getBlocked());
assertBlockedCallbackInAnyOrder(callback, true, mWiFiAgent, mCellAgent);
vpnUidCallback.assertNoCallback();
@@ -10065,7 +10084,7 @@
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
// Disable lockdown. Everything is unblocked.
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(false, primaryRanges);
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> !cb.getBlocked());
assertBlockedCallbackInAnyOrder(callback, false, mWiFiAgent, mCellAgent);
vpnUidCallback.assertNoCallback();
@@ -10077,36 +10096,8 @@
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);
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, false /* lockdown */, allowList);
- inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
- callback.assertNoCallback();
- defaultCallback.assertNoCallback();
- vpnUidCallback.assertNoCallback();
- vpnUidDefaultCallback.assertNoCallback();
- vpnDefaultCallbackAsUid.assertNoCallback();
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetwork());
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
- assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
- assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
-
- mMockVpn.setAlwaysOnPackage(null, false /* lockdown */, allowList);
- inOrder.verify(mMockNetd, never()).networkRejectNonSecureVpn(anyBoolean(), any());
- callback.assertNoCallback();
- defaultCallback.assertNoCallback();
- vpnUidCallback.assertNoCallback();
- vpnUidDefaultCallback.assertNoCallback();
- vpnDefaultCallbackAsUid.assertNoCallback();
- assertEquals(mWiFiAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
- assertEquals(mWiFiAgent.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.
- mMockVpn.setAlwaysOnPackage(ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
+ mCm.setRequireVpnForUids(true, primaryRanges);
defaultCallback.expect(BLOCKED_STATUS, mWiFiAgent, cb -> cb.getBlocked());
assertBlockedCallbackInAnyOrder(callback, true, mWiFiAgent, mCellAgent);
vpnUidCallback.assertNoCallback();
@@ -10192,7 +10183,7 @@
doAsUid(Process.SYSTEM_UID, () -> mCm.unregisterNetworkCallback(perUidCb));
}
- private VpnProfile setupLegacyLockdownVpn() {
+ private VpnProfile setupLockdownVpn(int profileType) {
final String profileName = "testVpnProfile";
final byte[] profileTag = profileName.getBytes(StandardCharsets.UTF_8);
doReturn(profileTag).when(mVpnProfileStore).get(Credentials.LOCKDOWN_VPN);
@@ -10201,7 +10192,9 @@
profile.name = "My VPN";
profile.server = "192.0.2.1";
profile.dnsServers = "8.8.8.8";
- profile.type = VpnProfile.TYPE_IPSEC_XAUTH_PSK;
+ profile.ipsecIdentifier = "My ipsecIdentifier";
+ profile.ipsecSecret = "My PSK";
+ profile.type = profileType;
final byte[] encodedProfile = profile.encode();
doReturn(encodedProfile).when(mVpnProfileStore).get(Credentials.VPN + profileName);
@@ -10219,8 +10212,8 @@
mMockVpn.connect(true);
}
- @Test
- public void testLegacyLockdownVpn() throws Exception {
+ private void doTestLockdownVpn(VpnProfile profile, boolean expectSetVpnDefaultForUids)
+ throws Exception {
mServiceContext.setPermission(
Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
@@ -10235,107 +10228,63 @@
mCm.registerSystemDefaultNetworkCallback(systemDefaultCallback,
new Handler(ConnectivityThread.getInstanceLooper()));
- // Pretend lockdown VPN was configured.
- final VpnProfile profile = 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());
-
- // VMSHandlerThread was used inside VpnManagerService and taken into LockDownVpnTracker.
- // VpnManagerService was decoupled from this test but this handlerThread is still required
- // in LockDownVpnTracker. Keep it until LockDownVpnTracker related verification is moved to
- // its own test.
- final HandlerThread VMSHandlerThread = new HandlerThread("TestVpnManagerService");
- VMSHandlerThread.start();
-
- // LockdownVpnTracker is created from VpnManagerService but VpnManagerService is decoupled
- // from ConnectivityServiceTest. Create it directly to simulate LockdownVpnTracker is
- // created.
- // TODO: move LockdownVpnTracker related tests to its own test.
- // Lockdown VPN disables teardown and enables lockdown.
- final LockdownVpnTracker lockdownVpnTracker = new LockdownVpnTracker(mServiceContext,
- VMSHandlerThread.getThreadHandler(), mMockVpn, profile);
- lockdownVpnTracker.init();
- assertFalse(mMockVpn.getEnableTeardown());
- assertTrue(mMockVpn.getLockdown());
+ // Init lockdown state to simulate LockdownVpnTracker behavior.
+ mCm.setLegacyLockdownVpnEnabled(true);
+ final List<Range<Integer>> ranges =
+ intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
+ mCm.setRequireVpnForUids(true /* requireVpn */, ranges);
// 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"));
- mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
- mCellAgent.connect(false /* validated */);
- callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- 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"));
- mCellAgent.sendLinkProperties(cellLp);
- callback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- defaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- systemDefaultCallback.expect(LINK_PROPERTIES_CHANGED, mCellAgent);
- 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);
- mCellAgent.disconnect();
- callback.expect(LOST, mCellAgent);
- defaultCallback.expect(LOST, mCellAgent);
- systemDefaultCallback.expect(LOST, mCellAgent);
- 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);
+ final ExpectedBroadcast b = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTING);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
mCellAgent.connect(false /* validated */);
callback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
systemDefaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mCellAgent);
- b1.expectBroadcast();
- assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+ b.expectBroadcast();
+ // Simulate LockdownVpnTracker attempting to start the VPN since it received the
+ // systemDefault callback.
+ mMockVpn.startLegacyVpnPrivileged(profile);
+ if (expectSetVpnDefaultForUids) {
+ // setVpnDefaultForUids() releases the original network request and creates a VPN
+ // request so LOST callback is received.
+ defaultCallback.expect(LOST, mCellAgent);
+ // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+ // as the network satisfier.
+ assertNull(mCm.getActiveNetworkInfo());
+ } else {
+ assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
assertExtraInfoFromCmBlocked(mCellAgent);
- // 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);
+ final ExpectedBroadcast b2 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+ final ExpectedBroadcast b3 = expectConnectivityAction(TYPE_MOBILE, DetailedState.CONNECTED);
establishLegacyLockdownVpn(mCellAgent.getNetwork());
callback.expectAvailableThenValidatedCallbacks(mMockVpn);
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
systemDefaultCallback.assertNoCallback();
- NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
- b1.expectBroadcast();
+ final NetworkCapabilities vpnNc = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
b2.expectBroadcast();
- assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+ b3.expectBroadcast();
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
@@ -10356,53 +10305,78 @@
wifiNc.addCapability(NET_CAPABILITY_NOT_METERED);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp, wifiNc);
- b1 = expectConnectivityAction(TYPE_MOBILE, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b4 =
+ 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);
+ final ExpectedBroadcast b5 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTING);
mWiFiAgent.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?
+ // Wifi is not blocked since VPN network is still connected.
callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ defaultCallback.assertNoCallback();
+ systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ b4.expectBroadcast();
+ b5.expectBroadcast();
+
+ // Simulate LockdownVpnTracker restarting the VPN since it received the systemDefault
+ // callback with different network.
+ final ExpectedBroadcast b6 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ mMockVpn.stopVpnRunnerPrivileged();
+ mMockVpn.startLegacyVpnPrivileged(profile);
+ // VPN network is disconnected (to restart)
callback.expect(LOST, mMockVpn);
defaultCallback.expect(LOST, mMockVpn);
+ // The network preference is cleared when VPN is disconnected so it receives callbacks for
+ // the system-wide default.
defaultCallback.expectAvailableCallbacksUnvalidatedAndBlocked(mWiFiAgent);
- systemDefaultCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
+ if (expectSetVpnDefaultForUids) {
+ // setVpnDefaultForUids() releases the original network request and creates a VPN
+ // request so LOST callback is received.
+ defaultCallback.expect(LOST, mWiFiAgent);
+ }
+ systemDefaultCallback.assertNoCallback();
+ b6.expectBroadcast();
// While the VPN is reconnecting on the new network, everything is blocked.
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the mNoServiceNetwork
+ // as the network satisfier.
+ assertNull(mCm.getActiveNetworkInfo());
+ } else {
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
assertNetworkInfo(TYPE_VPN, DetailedState.BLOCKED);
assertExtraInfoFromCmBlocked(mWiFiAgent);
// The VPN comes up again on wifi.
- b1 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
- b2 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
+ final ExpectedBroadcast b7 = expectConnectivityAction(TYPE_VPN, DetailedState.CONNECTED);
+ final ExpectedBroadcast b8 = expectConnectivityAction(TYPE_WIFI, DetailedState.CONNECTED);
establishLegacyLockdownVpn(mWiFiAgent.getNetwork());
callback.expectAvailableThenValidatedCallbacks(mMockVpn);
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
systemDefaultCallback.assertNoCallback();
- b1.expectBroadcast();
- b2.expectBroadcast();
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ b7.expectBroadcast();
+ b8.expectBroadcast();
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mWiFiAgent);
- 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));
+ final NetworkCapabilities vpnNc2 = mCm.getNetworkCapabilities(mMockVpn.getNetwork());
+ assertTrue(vpnNc2.hasTransport(TRANSPORT_VPN));
+ assertTrue(vpnNc2.hasTransport(TRANSPORT_WIFI));
+ assertFalse(vpnNc2.hasTransport(TRANSPORT_CELLULAR));
+ assertTrue(vpnNc2.hasCapability(NET_CAPABILITY_NOT_METERED));
// Disconnect cell. Nothing much happens since it's not the default network.
mCellAgent.disconnect();
@@ -10410,30 +10384,61 @@
defaultCallback.assertNoCallback();
systemDefaultCallback.assertNoCallback();
- assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ if (expectSetVpnDefaultForUids) {
+ // Due to the VPN default request, getActiveNetworkInfo() gets the VPN network as the
+ // network satisfier which has TYPE_VPN.
+ assertActiveNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
+ } else {
+ // LegacyVpnRunner does not call setVpnDefaultsForUids(), which means
+ // getActiveNetworkInfo() can only return the info for the system-wide default instead.
+ // This should be fixed, but LegacyVpnRunner will be removed soon anyway.
+ assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
+ }
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);
assertExtraInfoFromCmPresent(mWiFiAgent);
- b1 = expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
- b2 = expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b9 =
+ expectConnectivityAction(TYPE_WIFI, DetailedState.DISCONNECTED);
+ final ExpectedBroadcast b10 =
+ expectConnectivityAction(TYPE_VPN, DetailedState.DISCONNECTED);
mWiFiAgent.disconnect();
callback.expect(LOST, mWiFiAgent);
- systemDefaultCallback.expect(LOST, mWiFiAgent);
- b1.expectBroadcast();
callback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
- mMockVpn.expectStopVpnRunnerPrivileged();
+ defaultCallback.expectCaps(mMockVpn, c -> !c.hasTransport(TRANSPORT_WIFI));
+ systemDefaultCallback.expect(LOST, mWiFiAgent);
+ // TODO: There should only be one LOST callback. Since the WIFI network is underlying a VPN
+ // network, ConnectivityService#propagateUnderlyingNetworkCapabilities() causes a rematch to
+ // occur. Notably, this happens before setting the satisfiers of its network requests to
+ // null. Since the satisfiers are set to null in the rematch, an extra LOST callback is
+ // called.
+ systemDefaultCallback.expect(LOST, mWiFiAgent);
+ b9.expectBroadcast();
+ mMockVpn.stopVpnRunnerPrivileged();
callback.expect(LOST, mMockVpn);
- b2.expectBroadcast();
+ defaultCallback.expect(LOST, mMockVpn);
+ b10.expectBroadcast();
- VMSHandlerThread.quitSafely();
- VMSHandlerThread.join();
+ assertNoCallbacks(callback, defaultCallback, systemDefaultCallback);
+ }
+
+ @Test
+ public void testLockdownVpn_LegacyVpnRunner() throws Exception {
+ final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
+ doTestLockdownVpn(profile, false /* expectSetVpnDefaultForUids */);
+ }
+
+ @Test
+ public void testLockdownVpn_Ikev2VpnRunner() throws Exception {
+ final VpnProfile profile = setupLockdownVpn(VpnProfile.TYPE_IKEV2_IPSEC_PSK);
+ doTestLockdownVpn(profile, true /* expectSetVpnDefaultForUids */);
}
@Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
public void testLockdownSetFirewallUidRule() throws Exception {
- final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
+ final List<Range<Integer>> lockdownRange =
+ intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
// Enable Lockdown
mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
waitForIdle();
@@ -10483,6 +10488,7 @@
doTestSetUidFirewallRule(FIREWALL_CHAIN_POWERSAVE, FIREWALL_RULE_DENY);
doTestSetUidFirewallRule(FIREWALL_CHAIN_RESTRICTED, FIREWALL_RULE_DENY);
doTestSetUidFirewallRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, FIREWALL_RULE_DENY);
+ doTestSetUidFirewallRule(FIREWALL_CHAIN_BACKGROUND, FIREWALL_RULE_DENY);
doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_1, FIREWALL_RULE_ALLOW);
doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_2, FIREWALL_RULE_ALLOW);
doTestSetUidFirewallRule(FIREWALL_CHAIN_OEM_DENY_3, FIREWALL_RULE_ALLOW);
@@ -10496,6 +10502,7 @@
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_BACKGROUND,
FIREWALL_CHAIN_OEM_DENY_1,
FIREWALL_CHAIN_OEM_DENY_2,
FIREWALL_CHAIN_OEM_DENY_3);
@@ -10541,13 +10548,12 @@
final boolean allowlist = true;
final boolean denylist = false;
- doReturn(true).when(mBpfNetMaps).isFirewallAllowList(anyInt());
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_DOZABLE, allowlist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_POWERSAVE, allowlist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_RESTRICTED, allowlist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_LOW_POWER_STANDBY, allowlist);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_BACKGROUND, allowlist);
- doReturn(false).when(mBpfNetMaps).isFirewallAllowList(anyInt());
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_STANDBY, denylist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_1, denylist);
doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_2, denylist);
@@ -10568,6 +10574,7 @@
doTestReplaceFirewallChain(FIREWALL_CHAIN_POWERSAVE);
doTestReplaceFirewallChain(FIREWALL_CHAIN_RESTRICTED);
doTestReplaceFirewallChain(FIREWALL_CHAIN_LOW_POWER_STANDBY);
+ doTestReplaceFirewallChain(FIREWALL_CHAIN_BACKGROUND);
doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_1);
doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_2);
doTestReplaceFirewallChain(FIREWALL_CHAIN_OEM_DENY_3);
@@ -10800,6 +10807,11 @@
expectNativeNetworkCreated(netId, permission, iface, null /* inOrder */);
}
+ private int getIdleTimerLabel(int netId, int transportType) {
+ return ConnectivityService.LegacyNetworkActivityTracker.getIdleTimerLabel(
+ mDeps.isAtLeastV(), netId, transportType);
+ }
+
@Test
public void testStackedLinkProperties() throws Exception {
final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -11041,7 +11053,7 @@
networkCallback.expect(LOST, mCellAgent);
networkCallback.assertNoCallback();
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
verify(mMockNetd).networkDestroy(cellNetId);
if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
@@ -11100,7 +11112,7 @@
}
verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(Integer.toString(getIdleTimerLabel(cellNetId, TRANSPORT_CELLULAR))));
verify(mMockNetd).networkDestroy(cellNetId);
if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
@@ -11355,8 +11367,21 @@
final ConditionVariable onNetworkActiveCv = new ConditionVariable();
final ConnectivityManager.OnNetworkActiveListener listener = onNetworkActiveCv::open;
+ TestNetworkCallback defaultCallback = new TestNetworkCallback();
+
testAndCleanup(() -> {
+ mCm.registerDefaultNetworkCallback(defaultCallback);
agent.connect(true);
+ defaultCallback.expectAvailableThenValidatedCallbacks(agent);
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ }
+ clearInvocations(mIBatteryStats);
+ final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
// Network is considered active when the network becomes the default network.
assertTrue(mCm.isDefaultNetworkActive());
@@ -11365,19 +11390,57 @@
// Interface goes to inactive state
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
mServiceContext.expectDataActivityBroadcast(legacyType, false /* isActive */,
TIMESTAMP);
assertFalse(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
assertFalse(mCm.isDefaultNetworkActive());
+ if (mDeps.isAtLeastV()) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID));
+ }
+ } else {
+ // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+ // BatteryStats API by the netd activity change callback since BatteryStatsService
+ // listen to netd callback via NetworkManagementService and update battery stats by
+ // itself.
+ verify(mIBatteryStats, never())
+ .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+ verify(mIBatteryStats, never())
+ .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+ }
// Interface goes to active state
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
- transportType, TIMESTAMP, TEST_PACKAGE_UID);
+ idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
mServiceContext.expectDataActivityBroadcast(legacyType, true /* isActive */, TIMESTAMP);
assertTrue(onNetworkActiveCv.block(TEST_CALLBACK_TIMEOUT_MS));
assertTrue(mCm.isDefaultNetworkActive());
+ if (mDeps.isAtLeastV()) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ verify(mIBatteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+ } else if (transportType == TRANSPORT_WIFI) {
+ verify(mIBatteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(TEST_PACKAGE_UID));
+ }
+ } else {
+ // If TrackMultiNetworks is disabled, LegacyNetworkActivityTracker does not call
+ // BatteryStats API by the netd activity change callback since BatteryStatsService
+ // listen to netd callback via NetworkManagementService and update battery stats by
+ // itself.
+ verify(mIBatteryStats, never())
+ .noteMobileRadioPowerState(anyInt(), anyLong(), anyInt());
+ verify(mIBatteryStats, never())
+ .noteWifiRadioPowerState(anyInt(), anyLong(), anyInt());
+ }
}, () -> { // Cleanup
+ mCm.unregisterNetworkCallback(defaultCallback);
+ }, () -> { // Cleanup
mCm.removeDefaultNetworkActiveListener(listener);
}, () -> { // Cleanup
agent.disconnect();
@@ -11425,12 +11488,13 @@
}
@Test
- public void testOnNetworkActive_NewEthernetConnects_CallbackNotCalled() throws Exception {
- // LegacyNetworkActivityTracker calls onNetworkActive callback only for networks that
- // tracker adds the idle timer to. And the tracker does not set the idle timer for the
- // ethernet network.
+ public void testOnNetworkActive_NewEthernetConnects_Callback() throws Exception {
+ // On pre-V devices, LegacyNetworkActivityTracker calls onNetworkActive callback only for
+ // networks that tracker adds the idle timer to. And the tracker does not set the idle timer
+ // for the ethernet network.
// So onNetworkActive is not called when the ethernet becomes the default network
- doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, false /* expectCallback */);
+ final boolean expectCallback = mDeps.isAtLeastV();
+ doTestOnNetworkActive_NewNetworkConnects(TRANSPORT_ETHERNET, expectCallback);
}
@Test
@@ -11460,15 +11524,19 @@
mCm.registerNetworkCallback(networkRequest, networkCallback);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ final String cellIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR));
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(true);
networkCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_CELLULAR)));
+ eq(cellIdleTimerLabel));
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ String wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
final LinkProperties wifiLp = new LinkProperties();
wifiLp.setInterfaceName(WIFI_IFNAME);
mWiFiAgent.sendLinkProperties(wifiLp);
@@ -11479,9 +11547,18 @@
networkCallback.expectLosing(mCellAgent);
networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
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)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ // V+ devices add idleTimer when the network is first connected and remove when the
+ // network is disconnected.
+ verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ // pre V devices add idleTimer when the network becomes the default network and remove
+ // when the network becomes no longer the default network.
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// Disconnect wifi and switch back to cell
reset(mMockNetd);
@@ -11489,13 +11566,20 @@
networkCallback.expect(LOST, mWiFiAgent);
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)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, never()).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ verify(mMockNetd, times(1)).idletimerAddInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// reconnect wifi
reset(mMockNetd);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ wifiIdleTimerLabel = Integer.toString(getIdleTimerLabel(
+ mWiFiAgent.getNetwork().netId, TRANSPORT_WIFI));
wifiLp.setInterfaceName(WIFI_IFNAME);
mWiFiAgent.sendLinkProperties(wifiLp);
mWiFiAgent.connect(true);
@@ -11503,20 +11587,30 @@
networkCallback.expectLosing(mCellAgent);
networkCallback.expectCaps(mWiFiAgent, c -> c.hasCapability(NET_CAPABILITY_VALIDATED));
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)));
+ eq(wifiIdleTimerLabel));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, never()).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
// Disconnect cell
reset(mMockNetd);
mCellAgent.disconnect();
networkCallback.expect(LOST, mCellAgent);
- // 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)));
+ if (mDeps.isAtLeastV()) {
+ verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(mCellAgent.getNetwork().netId)));
+ } else {
+ // 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.
+ verify(mMockNetd, times(0)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
+ eq(Integer.toString(TRANSPORT_CELLULAR)));
+ }
verify(mMockNetd, times(1)).networkDestroy(eq(mCellAgent.getNetwork().netId));
verify(mMockDnsResolver, times(1)).destroyNetworkCache(eq(mCellAgent.getNetwork().netId));
@@ -11525,12 +11619,27 @@
mWiFiAgent.disconnect();
b.expectBroadcast();
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(WIFI_IFNAME), anyInt(),
- eq(Integer.toString(TRANSPORT_WIFI)));
+ eq(wifiIdleTimerLabel));
// Clean up
mCm.unregisterNetworkCallback(networkCallback);
}
+ @Test
+ public void testDataActivityTracking_VpnNetwork() throws Exception {
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ mWiFiAgent.connect(true /* validated */);
+ mMockVpn.setUnderlyingNetworks(new Network[] { mWiFiAgent.getNetwork() });
+
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(VPN_IFNAME);
+ mMockVpn.establishForMyUid(lp);
+
+ // NetworkActivityTracker should not track the VPN network since VPN can change the
+ // underlying network without disconnect.
+ verify(mMockNetd, never()).idletimerAddInterface(eq(VPN_IFNAME), anyInt(), any());
+ }
+
private void verifyTcpBufferSizeChange(String tcpBufferSizes) throws Exception {
String[] values = tcpBufferSizes.split(",");
String rmemValues = String.join(" ", values[0], values[1], values[2]);
@@ -12552,9 +12661,6 @@
mMockVpn.establish(new LinkProperties(), vpnOwnerUid, vpnRange);
assertVpnUidRangesUpdated(true, vpnRange, vpnOwnerUid);
- final UnderlyingNetworkInfo underlyingNetworkInfo =
- new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList<>());
- mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo);
mDeps.setConnectionOwnerUid(42);
}
@@ -12798,7 +12904,8 @@
private NetworkAgentInfo fakeNai(NetworkCapabilities nc, NetworkInfo networkInfo) {
return new NetworkAgentInfo(null, new Network(NET_ID), networkInfo, new LinkProperties(),
- nc, new NetworkScore.Builder().setLegacyInt(0).build(),
+ nc, null /* localNetworkConfig */,
+ new NetworkScore.Builder().setLegacyInt(0).build(),
mServiceContext, null, new NetworkAgentConfig(), mService, null, null, 0,
INVALID_UID, TEST_LINGER_DELAY_MS, mQosCallbackTracker,
new ConnectivityService.Dependencies());
@@ -12817,6 +12924,18 @@
}
@Test
+ public void testCheckConnectivityDiagnosticsPermissionsSysUi() throws Exception {
+ final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+
+ mServiceContext.setPermission(STATUS_BAR_SERVICE, PERMISSION_GRANTED);
+ assertTrue(
+ "SysUi permission (STATUS_BAR_SERVICE) not applied",
+ mService.checkConnectivityDiagnosticsPermissions(
+ Process.myPid(), Process.myUid(), naiWithoutUid,
+ mContext.getOpPackageName()));
+ }
+
+ @Test
public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception {
final int wrongUid = Process.myUid() + 1;
@@ -13190,7 +13309,7 @@
}
@Test
- public void testDumpDoesNotCrash() {
+ public void testDumpDoesNotCrash() throws Exception {
mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
// Filing a couple requests prior to testing the dump.
final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
@@ -13202,6 +13321,44 @@
mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
+ // NetworkProvider
+ final NetworkProvider wifiProvider = new NetworkProvider(mServiceContext,
+ mCsHandlerThread.getLooper(), "Wifi provider");
+ mCm.registerNetworkProvider(wifiProvider);
+
+ // NetworkAgent
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiAgent.connect(true);
+
+ // NetworkOffer
+ final NetworkScore wifiScore = new NetworkScore.Builder().build();
+ final NetworkCapabilities wifiCaps = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ final TestableNetworkOfferCallback wifiCallback = new TestableNetworkOfferCallback(
+ TIMEOUT_MS /* timeout */, TEST_CALLBACK_TIMEOUT_MS /* noCallbackTimeout */);
+ wifiProvider.registerNetworkOffer(wifiScore, wifiCaps, r -> r.run(), wifiCallback);
+
+ // Profile preferences
+ final UserHandle testHandle = setupEnterpriseNetwork();
+ final TestNetworkAgentWrapper workAgent = makeEnterpriseNetworkAgent();
+ workAgent.connect(true);
+ mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
+ null /* executor */, null /* listener */);
+
+ // OEM preferences
+ @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+ OEM_NETWORK_PREFERENCE_OEM_PAID;
+ setOemNetworkPreferenceAgentConnected(TRANSPORT_CELLULAR, true);
+ setOemNetworkPreference(networkPref, TEST_PACKAGE_NAME);
+
+ // Mobile data preferred UIDs
+ setAndUpdateMobileDataPreferredUids(Set.of(TEST_PACKAGE_UID));
+
verifyDump(new String[0]);
// Verify dump with arguments.
@@ -15295,6 +15452,8 @@
expectNoRequestChanged(oemPaidFactory);
internetFactory.expectRequestAdd();
mCm.unregisterNetworkCallback(wifiCallback);
+ handlerThread.quitSafely();
+ handlerThread.join();
}
/**
@@ -15659,6 +15818,8 @@
assertTrue(testFactory.getMyStartRequested());
} finally {
testFactory.terminate();
+ handlerThread.quitSafely();
+ handlerThread.join();
}
}
@@ -17223,6 +17384,7 @@
mCm.requestNetwork(new NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
+ .addTransportType(TRANSPORT_CELLULAR)
.build(),
cb);
@@ -17368,7 +17530,7 @@
// In this test TEST_PACKAGE_UID will be the UID of the carrier service UID.
doReturn(true).when(mCarrierPrivilegeAuthenticator)
- .hasCarrierPrivilegeForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
+ .isCarrierServiceUidForNetworkCapabilities(eq(TEST_PACKAGE_UID), any());
// Simulate a restricted telephony network. The telephony factory is entitled to set
// the access UID to the service package on any of its restricted networks.
@@ -17433,17 +17595,18 @@
// TODO : fix the builder
ncb.setNetworkSpecifier(null);
ncb.removeTransportType(TRANSPORT_CELLULAR);
- ncb.addTransportType(TRANSPORT_WIFI);
+ ncb.addTransportType(TRANSPORT_BLUETOOTH);
// Wifi does not get to set access UID, even to the correct UID
mCm.requestNetwork(new NetworkRequest.Builder()
- .addTransportType(TRANSPORT_WIFI)
+ .addTransportType(TRANSPORT_BLUETOOTH)
.removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
.build(), cb);
- mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(), ncb.build());
- mWiFiAgent.connect(true);
- cb.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+ final TestNetworkAgentWrapper bluetoothAgent = new TestNetworkAgentWrapper(
+ TRANSPORT_BLUETOOTH, new LinkProperties(), ncb.build());
+ bluetoothAgent.connect(true);
+ cb.expectAvailableThenValidatedCallbacks(bluetoothAgent);
ncb.setAllowedUids(serviceUidSet);
- mWiFiAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
+ bluetoothAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
cb.assertNoCallback(TEST_CALLBACK_TIMEOUT_MS);
mCm.unregisterNetworkCallback(cb);
}
@@ -18669,6 +18832,7 @@
final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(transportToTestIfaceName(transportType));
final TestNetworkAgentWrapper agent = new TestNetworkAgentWrapper(transportType, lp);
+ final int idleTimerLabel = getIdleTimerLabel(agent.getNetwork().netId, transportType);
testAndCleanup(() -> {
final UidFrozenStateChangedCallback uidFrozenStateChangedCallback =
getUidFrozenStateChangedCallback().get();
@@ -18681,7 +18845,7 @@
if (freezeWithNetworkInactive) {
// Make network inactive
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- transportType, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
}
// Freeze TEST_FROZEN_UID and TEST_UNFROZEN_UID
@@ -18705,7 +18869,7 @@
// Make network active
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
- transportType, TIMESTAMP, TEST_PACKAGE_UID);
+ idleTimerLabel, TIMESTAMP, TEST_PACKAGE_UID);
waitForIdle();
if (expectDelay) {
@@ -18724,8 +18888,8 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_ActiveCellular() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
- false /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, false /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
@Test
@@ -18733,22 +18897,22 @@
public void testDelayFrozenUidSocketDestroy_InactiveCellular() throws Exception {
// When the default network is cellular and cellular network is inactive, closing socket
// is delayed.
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR,
- true /* freezeWithNetworkInactive */, true /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_CELLULAR, true /* freezeWithNetworkInactive */,
+ true /* expectDelay */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_ActiveWifi() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
- false /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, false /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
@Test
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
public void testDelayFrozenUidSocketDestroy_InactiveWifi() throws Exception {
- doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI,
- true /* freezeWithNetworkInactive */, false /* expectDelay */);
+ doTestDelayFrozenUidSocketDestroy(TRANSPORT_WIFI, true /* freezeWithNetworkInactive */,
+ false /* expectDelay */);
}
/**
@@ -18769,6 +18933,8 @@
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+ final int idleTimerLabel =
+ getIdleTimerLabel(mCellAgent.getNetwork().netId, TRANSPORT_CELLULAR);
final TestNetworkCallback defaultCallback = new TestNetworkCallback();
mCm.registerDefaultNetworkCallback(defaultCallback);
@@ -18778,7 +18944,7 @@
// Make cell network inactive
netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
- TRANSPORT_CELLULAR, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
+ idleTimerLabel, TIMESTAMP, NETWORK_ACTIVITY_NO_UID);
// Freeze TEST_FROZEN_UID
final int[] uids = {TEST_FROZEN_UID};
diff --git a/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/tests/unit/java/com/android/server/HandlerUtilsTest.kt
new file mode 100644
index 0000000..62bb651
--- /dev/null
+++ b/tests/unit/java/com/android/server/HandlerUtilsTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.os.HandlerThread
+import com.android.server.connectivity.HandlerUtils
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val THREAD_BLOCK_TIMEOUT_MS = 1000L
+const val TEST_REPEAT_COUNT = 100
+@RunWith(DevSdkIgnoreRunner::class)
+class HandlerUtilsTest {
+ val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
+ it.start()
+ }
+ val handler = handlerThread.threadHandler
+
+ @Test
+ fun testRunWithScissors() {
+ // Repeat the test a fair amount of times to ensure that it does not pass by chance.
+ repeat(TEST_REPEAT_COUNT) {
+ var result = false
+ HandlerUtils.runWithScissors(handler, {
+ assertEquals(Thread.currentThread(), handlerThread)
+ result = true
+ }, THREAD_BLOCK_TIMEOUT_MS)
+ // Assert that the result is modified on the handler thread, but can also be seen from
+ // the current thread. The assertion should pass if the runWithScissors provides
+ // the guarantee where the assignment happens-before the assertion.
+ assertTrue(result)
+ }
+ }
+
+ @After
+ fun tearDown() {
+ handlerThread.quitSafely()
+ handlerThread.join()
+ }
+}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 71bd330..ffc8aa1 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -35,14 +35,17 @@
import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
+
import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
import static com.android.server.NsdService.DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF;
import static com.android.server.NsdService.MdnsListener;
import static com.android.server.NsdService.NO_TRANSACTION;
import static com.android.server.NsdService.parseTypeAndSubtype;
import static com.android.testutils.ContextUtils.mockService;
+
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -142,6 +145,7 @@
// TODOs:
// - test client can send requests and receive replies
// - test NSD_ON ENABLE/DISABLED listening
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
@@ -220,7 +224,7 @@
anyInt(), anyString(), anyString(), anyString(), anyInt());
doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
doReturn(mDiscoveryManager).when(mDeps)
- .makeMdnsDiscoveryManager(any(), any(), any());
+ .makeMdnsDiscoveryManager(any(), any(), any(), any());
doReturn(mMulticastLock).when(mWifiManager).createMulticastLock(any());
doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any(), any());
doReturn(DEFAULT_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF).when(mDeps).getDeviceConfigInt(
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 986c389..10a0982 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -72,11 +72,14 @@
import android.os.SystemClock;
import android.telephony.SubscriptionManager;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.ArraySet;
import android.util.Log;
+import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive;
import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import com.android.testutils.DevSdkIgnoreRule;
@@ -94,13 +97,16 @@
import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
+import java.io.StringWriter;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
@@ -230,6 +236,9 @@
private static final byte[] TEST_RESPONSE_BYTES =
HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
+ private static final Set<Range<Integer>> TEST_UID_RANGES =
+ new ArraySet<>(Arrays.asList(new Range<>(10000, 99999)));
+
private static class TestKeepaliveInfo {
private static List<Socket> sOpenSockets = new ArrayList<>();
@@ -407,28 +416,28 @@
public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
setupResponseWithSocketExisting();
assertThrows(IllegalStateException.class,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES));
}
@Test
public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
setupResponseWithSocketExisting();
assertTrue(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
}
@Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID, TEST_UID_RANGES)));
}
@Test
public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
setupResponseWithoutSocketExisting();
assertFalse(visibleOnHandlerThread(mTestHandler,
- () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID, TEST_UID_RANGES)));
}
private void triggerEventKeepalive(int slot, int reason) {
@@ -472,14 +481,16 @@
setupResponseWithoutSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
+ autoKi, TEST_NETID, TEST_UID_RANGES));
}
private void doResumeKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
setupResponseWithSocketExisting();
visibleOnHandlerThread(
mTestHandler,
- () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(autoKi, TEST_NETID));
+ () -> mAOOKeepaliveTracker.handleMonitorAutomaticKeepalive(
+ autoKi, TEST_NETID, TEST_UID_RANGES));
}
private void doStopKeepalive(AutomaticOnOffKeepalive autoKi) throws Exception {
@@ -974,4 +985,19 @@
// The keepalive should be removed in AutomaticOnOffKeepaliveTracker.
assertNull(getAutoKiForBinder(testInfo.binder));
}
+
+ @Test
+ public void testDumpDoesNotCrash() throws Exception {
+ final TestKeepaliveInfo testInfo1 = doStartNattKeepalive();
+ final TestKeepaliveInfo testInfo2 = doStartNattKeepalive();
+ checkAndProcessKeepaliveStart(TEST_SLOT, testInfo1.kpd);
+ checkAndProcessKeepaliveStart(TEST_SLOT + 1, testInfo2.kpd);
+ final AutomaticOnOffKeepalive autoKi1 = getAutoKiForBinder(testInfo1.binder);
+ doPauseKeepalive(autoKi1);
+
+ final StringWriter stringWriter = new StringWriter();
+ final IndentingPrintWriter pw = new IndentingPrintWriter(stringWriter, " ");
+ visibleOnHandlerThread(mTestHandler, () -> mAOOKeepaliveTracker.dump(pw));
+ assertFalse(stringWriter.toString().isEmpty());
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
index 8113626..f07593e 100644
--- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java
@@ -45,6 +45,7 @@
import android.net.NetworkCapabilities;
import android.net.TelephonyNetworkSpecifier;
import android.os.Build;
+import android.os.HandlerThread;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
@@ -52,10 +53,11 @@
import com.android.networkstack.apishim.TelephonyManagerShimImpl;
import com.android.networkstack.apishim.common.TelephonyManagerShim.CarrierPrivilegesListenerShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-import com.android.server.ConnectivityService;
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator.Dependencies;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -86,10 +88,11 @@
private final int mCarrierConfigPkgUid = 12345;
private final String mTestPkg = "com.android.server.connectivity.test";
private final BroadcastReceiver mMultiSimBroadcastReceiver;
+ @NonNull private final HandlerThread mHandlerThread;
public class TestCarrierPrivilegeAuthenticator extends CarrierPrivilegeAuthenticator {
TestCarrierPrivilegeAuthenticator(@NonNull final Context c,
- @NonNull final ConnectivityService.Dependencies deps,
+ @NonNull final Dependencies deps,
@NonNull final TelephonyManager t) {
super(c, deps, t, mTelephonyManagerShim);
}
@@ -100,6 +103,11 @@
}
}
+ @After
+ public void tearDown() {
+ mHandlerThread.quit();
+ }
+
/** Parameters to test both using callbacks or the old broadcast */
@Parameterized.Parameters
public static Collection<Boolean> shouldUseCallbacks() {
@@ -111,9 +119,11 @@
mTelephonyManager = mock(TelephonyManager.class);
mTelephonyManagerShim = mock(TelephonyManagerShimImpl.class);
mPackageManager = mock(PackageManager.class);
- final ConnectivityService.Dependencies deps = mock(ConnectivityService.Dependencies.class);
+ mHandlerThread = new HandlerThread(CarrierPrivilegeAuthenticatorTest.class.getSimpleName());
+ final Dependencies deps = mock(Dependencies.class);
doReturn(useCallbacks).when(deps).isFeatureEnabled(any() /* context */,
eq(CARRIER_SERVICE_CHANGED_USE_CALLBACK));
+ doReturn(mHandlerThread).when(deps).makeHandlerThread();
doReturn(SUBSCRIPTION_COUNT).when(mTelephonyManager).getActiveModemCount();
doReturn(mTestPkg).when(mTelephonyManagerShim)
.getCarrierServicePackageNameForLogicalSlot(anyInt());
@@ -164,9 +174,9 @@
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
- assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
- assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid + 1, ncBuilder.build()));
}
@@ -203,9 +213,9 @@
.addTransportType(TRANSPORT_CELLULAR)
.setNetworkSpecifier(specifier)
.build();
- assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, nc));
- assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid + 1, nc));
}
@@ -225,9 +235,9 @@
listener.onCarrierPrivilegesChanged(Collections.emptyList(), new int[] {});
listener.onCarrierServiceChanged(null, applicationInfo.uid);
- assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, nc));
- assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid + 1, nc));
}
@@ -238,11 +248,11 @@
final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder();
ncBuilder.addTransportType(TRANSPORT_CELLULAR);
- assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
- assertTrue(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertTrue(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
// The builder for NetworkCapabilities doesn't allow removing the transport as long as a
@@ -251,7 +261,7 @@
ncBuilder.removeTransportType(TRANSPORT_CELLULAR);
ncBuilder.addTransportType(TRANSPORT_WIFI);
ncBuilder.setNetworkSpecifier(new TelephonyNetworkSpecifier(0));
- assertFalse(mCarrierPrivilegeAuthenticator.hasCarrierPrivilegeForNetworkCapabilities(
+ assertFalse(mCarrierPrivilegeAuthenticator.isCarrierServiceUidForNetworkCapabilities(
mCarrierConfigPkgUid, ncBuilder.build()));
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 24aecdb..afb9abd 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -139,7 +139,9 @@
assertEquals(actual.tlsConnectTimeoutMs, expected.tlsConnectTimeoutMs);
assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions);
assertContainsExactly(actual.transportTypes, expected.transportTypes);
- assertFieldCountEquals(16, ResolverParamsParcel.class);
+ assertEquals(actual.meteredNetwork, expected.meteredNetwork);
+ assertEquals(actual.dohParams, expected.dohParams);
+ assertFieldCountEquals(18, ResolverParamsParcel.class);
}
@Before
@@ -169,10 +171,12 @@
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);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
- mDnsManager.updateTransportsForNetwork(TEST_NETID_ALTERNATE, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID_ALTERNATE, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID_ALTERNATE, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -205,7 +209,7 @@
InetAddress.parseNumericAddress("6.6.6.6"),
InetAddress.parseNumericAddress("2001:db8:66:66::1")
}));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
fixedLp = new LinkProperties(lp);
@@ -242,7 +246,9 @@
// be tracked.
LinkProperties lp = new LinkProperties();
lp.addDnsServer(InetAddress.getByName("3.3.3.3"));
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -256,7 +262,7 @@
// Validation event has untracked netId
mDnsManager.updatePrivateDns(new Network(TEST_NETID),
mDnsManager.getPrivateDnsConfig());
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -307,7 +313,7 @@
ConnectivitySettingsManager.setPrivateDnsMode(mCtx, PRIVATE_DNS_MODE_OFF);
mDnsManager.updatePrivateDns(new Network(TEST_NETID),
mDnsManager.getPrivateDnsConfig());
- mDnsManager.updateTransportsForNetwork(TEST_NETID, TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
mDnsManager.updatePrivateDnsValidation(
@@ -352,7 +358,9 @@
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);
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setTransportTypes(TEST_TRANSPORT_TYPES);
+ mDnsManager.updateCapabilitiesForNetwork(TEST_NETID, nc);
mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
mDnsManager.flushVmDnsCache();
@@ -373,6 +381,8 @@
expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"};
expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
expectedParams.resolverOptions = null;
+ expectedParams.meteredNetwork = true;
+ expectedParams.dohParams = null;
assertResolverParamsEquals(actualParams, expectedParams);
}
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 90a0edd..1b964e2 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -37,6 +37,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.BroadcastReceiver;
@@ -1293,5 +1294,18 @@
expectRegisteredDurations,
expectActiveDurations,
new KeepaliveCarrierStats[0]);
+
+ assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
+
+ // Write time after 26 hours.
+ final int writeTime2 = 26 * 60 * 60 * 1000;
+ setElapsedRealtime(writeTime2);
+
+ visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
+ verify(mDependencies, times(2)).writeStats(dailyKeepaliveInfoReportedCaptor.capture());
+ final DailykeepaliveInfoReported dailyKeepaliveInfoReported2 =
+ dailyKeepaliveInfoReportedCaptor.getValue();
+
+ assertFalse(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported2));
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index e6c0c83..07883ff 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -372,9 +372,10 @@
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,
+ new LinkProperties(), caps, null /* localNetworkConfiguration */,
+ 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());
if (setEverValidated) {
// As tests in this class deal with testing lingering, most tests are interested
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
new file mode 100644
index 0000000..12758c6
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity
+
+import android.net.INetd
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.mock
+import kotlin.test.assertFailsWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class RoutingCoordinatorServiceTest {
+ val mNetd = mock(INetd::class.java)
+ val mService = RoutingCoordinatorService(mNetd)
+
+ @Test
+ fun testInterfaceForward() {
+ val inOrder = inOrder(mNetd)
+
+ mService.addInterfaceForward("from1", "to1")
+ inOrder.verify(mNetd).ipfwdEnableForwarding(any())
+ inOrder.verify(mNetd).tetherAddForward("from1", "to1")
+ inOrder.verify(mNetd).ipfwdAddInterfaceForward("from1", "to1")
+
+ mService.addInterfaceForward("from2", "to1")
+ inOrder.verify(mNetd).tetherAddForward("from2", "to1")
+ inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
+
+ assertFailsWith<IllegalStateException> {
+ // Can't add the same pair again
+ mService.addInterfaceForward("from2", "to1")
+ }
+
+ mService.removeInterfaceForward("from1", "to1")
+ inOrder.verify(mNetd).ipfwdRemoveInterfaceForward("from1", "to1")
+ inOrder.verify(mNetd).tetherRemoveForward("from1", "to1")
+
+ mService.removeInterfaceForward("from2", "to1")
+ inOrder.verify(mNetd).ipfwdRemoveInterfaceForward("from2", "to1")
+ inOrder.verify(mNetd).tetherRemoveForward("from2", "to1")
+
+ inOrder.verify(mNetd).ipfwdDisableForwarding(any())
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 56346ad..ea2228e 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -56,11 +56,9 @@
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV4_UDP;
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_ESP;
import static com.android.server.connectivity.Vpn.PREFERRED_IKE_PROTOCOL_IPV6_UDP;
-import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
import static com.android.testutils.MiscAsserts.assertThrows;
-import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -76,7 +74,9 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.longThat;
import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
@@ -149,7 +149,6 @@
import android.net.wifi.WifiInfo;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
-import android.os.ConditionVariable;
import android.os.INetworkManagementService;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -191,6 +190,7 @@
import org.mockito.AdditionalAnswers;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -207,6 +207,7 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -317,6 +318,8 @@
@Mock DeviceIdleInternal mDeviceIdleInternal;
private final VpnProfile mVpnProfile;
+ @Captor private ArgumentCaptor<Collection<Range<Integer>>> mUidRangesCaptor;
+
private IpSecManager mIpSecManager;
private TestDeps mTestDeps;
@@ -579,6 +582,18 @@
}
@Test
+ public void testAlwaysOnWithoutLockdown() throws Exception {
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[1], false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
+
+ assertTrue(vpn.setAlwaysOnPackage(
+ null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager, never()).setRequireVpnForUids(anyBoolean(), any());
+ }
+
+ @Test
public void testLockdownChangingPackage() throws Exception {
final Vpn vpn = createVpn(PRIMARY_USER.id);
final Range<Integer> user = PRIMARY_USER_RANGE;
@@ -724,6 +739,37 @@
}
@Test
+ public void testLockdownSystemUser() throws Exception {
+ final Vpn vpn = createVpn(SYSTEM_USER_ID);
+
+ // Uid 0 is always excluded and PKG_UIDS[1] is the uid of the VPN.
+ final List<Integer> excludedUids = new ArrayList<>(List.of(0, PKG_UIDS[1]));
+ final List<Range<Integer>> ranges = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
+
+ // Set always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[1], true /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager).setRequireVpnForUids(true, ranges);
+
+ // Disable always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(
+ null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager).setRequireVpnForUids(false, ranges);
+
+ // Set always-on with lockdown and allow the app PKGS[2].
+ excludedUids.add(PKG_UIDS[2]);
+ final List<Range<Integer>> ranges2 = makeVpnUidRange(SYSTEM_USER_ID, excludedUids);
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[1], true /* lockdown */, Collections.singletonList(PKGS[2])));
+ verify(mConnectivityManager).setRequireVpnForUids(true, ranges2);
+
+ // Disable always-on with lockdown.
+ assertTrue(vpn.setAlwaysOnPackage(
+ null /* packageName */, false /* lockdown */, null /* lockdownAllowlist */));
+ verify(mConnectivityManager).setRequireVpnForUids(false, ranges2);
+ }
+
+ @Test
public void testLockdownRuleRepeatability() throws Exception {
final Vpn vpn = createVpn(PRIMARY_USER.id);
final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
@@ -788,6 +834,101 @@
}
@Test
+ public void testOnUserAddedAndRemoved_restrictedUser() throws Exception {
+ final InOrder order = inOrder(mMockNetworkAgent);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Set<Range<Integer>> initialRange = rangeSet(PRIMARY_USER_RANGE);
+ // Note since mVpnProfile is a Ikev2VpnProfile, this starts an IkeV2VpnRunner.
+ startLegacyVpn(vpn, mVpnProfile);
+ // Set an initial Uid range and mock the network agent
+ vpn.mNetworkCapabilities.setUids(initialRange);
+ vpn.mNetworkAgent = mMockNetworkAgent;
+
+ // Add the restricted user
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+ vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+ // Expect restricted user range to be added to the NetworkCapabilities.
+ final Set<Range<Integer>> expectRestrictedRange =
+ rangeSet(PRIMARY_USER_RANGE, uidRangeForUser(RESTRICTED_PROFILE_A.id));
+ assertEquals(expectRestrictedRange, vpn.mNetworkCapabilities.getUids());
+ order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
+ argThat(nc -> expectRestrictedRange.equals(nc.getUids())));
+
+ // Remove the restricted user
+ vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+ // Expect restricted user range to be removed from the NetworkCapabilities.
+ assertEquals(initialRange, vpn.mNetworkCapabilities.getUids());
+ order.verify(mMockNetworkAgent).doSendNetworkCapabilities(
+ argThat(nc -> initialRange.equals(nc.getUids())));
+ }
+
+ @Test
+ public void testOnUserAddedAndRemoved_restrictedUserLockdown() throws Exception {
+ final UidRangeParcel[] primaryUserRangeParcel = new UidRangeParcel[] {
+ new UidRangeParcel(PRIMARY_USER_RANGE.getLower(), PRIMARY_USER_RANGE.getUpper())};
+ final Range<Integer> restrictedUserRange = uidRangeForUser(RESTRICTED_PROFILE_A.id);
+ final UidRangeParcel[] restrictedUserRangeParcel = new UidRangeParcel[] {
+ new UidRangeParcel(restrictedUserRange.getLower(), restrictedUserRange.getUpper())};
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+
+ // Set lockdown calls setRequireVpnForUids
+ vpn.setLockdown(true);
+ verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(primaryUserRangeParcel));
+
+ // Add the restricted user
+ doReturn(true).when(mUserManager).canHaveRestrictedProfile();
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+ vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+
+ // Expect restricted user range to be added.
+ verify(mConnectivityManager).setRequireVpnForUids(true,
+ toRanges(restrictedUserRangeParcel));
+
+ // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
+ // return the restricted user but it is still returned in mUserManager.getUserInfo().
+ RESTRICTED_PROFILE_A.partial = true;
+ // Remove the restricted user
+ vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+ verify(mConnectivityManager).setRequireVpnForUids(false,
+ toRanges(restrictedUserRangeParcel));
+ // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
+ RESTRICTED_PROFILE_A.partial = false;
+ }
+
+ @Test
+ public void testOnUserAddedAndRemoved_restrictedUserAlwaysOn() throws Exception {
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+
+ // setAlwaysOnPackage() calls setRequireVpnForUids()
+ assertTrue(vpn.setAlwaysOnPackage(
+ PKGS[0], true /* lockdown */, null /* lockdownAllowlist */));
+ final List<Integer> excludedUids = List.of(PKG_UIDS[0]);
+ final List<Range<Integer>> primaryRanges =
+ makeVpnUidRange(PRIMARY_USER.id, excludedUids);
+ verify(mConnectivityManager).setRequireVpnForUids(true, primaryRanges);
+
+ // Add the restricted user
+ doReturn(true).when(mUserManager).canHaveRestrictedProfile();
+ setMockedUsers(PRIMARY_USER, RESTRICTED_PROFILE_A);
+ vpn.onUserAdded(RESTRICTED_PROFILE_A.id);
+
+ final List<Range<Integer>> restrictedRanges =
+ makeVpnUidRange(RESTRICTED_PROFILE_A.id, excludedUids);
+ // Expect restricted user range to be added.
+ verify(mConnectivityManager).setRequireVpnForUids(true, restrictedRanges);
+
+ // Mark as partial indicates that the user is removed, mUserManager.getAliveUsers() does not
+ // return the restricted user but it is still returned in mUserManager.getUserInfo().
+ RESTRICTED_PROFILE_A.partial = true;
+ // Remove the restricted user
+ vpn.onUserRemoved(RESTRICTED_PROFILE_A.id);
+ verify(mConnectivityManager).setRequireVpnForUids(false, restrictedRanges);
+
+ // reset to avoid affecting other tests since RESTRICTED_PROFILE_A is static.
+ RESTRICTED_PROFILE_A.partial = false;
+ }
+
+ @Test
public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
throws Exception {
mTestDeps.mIgnoreCallingUidChecks = false;
@@ -958,37 +1099,53 @@
}
}
- private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
- final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ private String startVpnForVerifyAppExclusionList(Vpn vpn) throws Exception {
when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
.thenReturn(mVpnProfile.encode());
when(mVpnProfileStore.get(PRIMARY_USER_APP_EXCLUDE_KEY))
.thenReturn(HexDump.hexStringToByteArray(PKGS_BYTES));
-
- vpn.startVpnProfile(TEST_VPN_PKG);
+ final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
+ final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
+ clearInvocations(mConnectivityManager);
verify(mVpnProfileStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
vpn.mNetworkAgent = mMockNetworkAgent;
+
+ return sessionKey;
+ }
+
+ private Vpn prepareVpnForVerifyAppExclusionList() throws Exception {
+ final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ startVpnForVerifyAppExclusionList(vpn);
+
return vpn;
}
@Test
public void testSetAndGetAppExclusionList() throws Exception {
- final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
verify(mVpnProfileStore, never()).put(eq(PRIMARY_USER_APP_EXCLUDE_KEY), any());
vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
verify(mVpnProfileStore)
.put(eq(PRIMARY_USER_APP_EXCLUDE_KEY),
eq(HexDump.hexStringToByteArray(PKGS_BYTES)));
- assertEquals(vpn.createUserAndRestrictedProfilesRanges(
- PRIMARY_USER.id, null, Arrays.asList(PKGS)),
- vpn.mNetworkCapabilities.getUids());
+ final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
+ assertEquals(uidRanges, vpn.mNetworkCapabilities.getUids());
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
}
@Test
public void testRefreshPlatformVpnAppExclusionList_updatesExcludedUids() throws Exception {
- final Vpn vpn = prepareVpnForVerifyAppExclusionList();
+ final Vpn vpn = createVpn(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+ final String sessionKey = startVpnForVerifyAppExclusionList(vpn);
vpn.setAppExclusionList(TEST_VPN_PKG, Arrays.asList(PKGS));
+ final Set<Range<Integer>> uidRanges = vpn.createUserAndRestrictedProfilesRanges(
+ PRIMARY_USER.id, null /* allowedApplications */, Arrays.asList(PKGS));
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
verify(mMockNetworkAgent).doSendNetworkCapabilities(any());
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
@@ -997,48 +1154,53 @@
// Remove one of the package
List<Integer> newExcludedUids = toList(PKG_UIDS);
newExcludedUids.remove((Integer) PKG_UIDS[0]);
+ Set<Range<Integer>> newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
sPackages.remove(PKGS[0]);
vpn.refreshPlatformVpnAppExclusionList();
// List in keystore is not changed, but UID for the removed packages is no longer exempted.
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
- vpn.mNetworkCapabilities.getUids());
+ assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
ArgumentCaptor<NetworkCapabilities> ncCaptor =
ArgumentCaptor.forClass(NetworkCapabilities.class);
verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
- ncCaptor.getValue().getUids());
+ assertEquals(newUidRanges, ncCaptor.getValue().getUids());
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
reset(mMockNetworkAgent);
// Add the package back
newExcludedUids.add(PKG_UIDS[0]);
+ newUidRanges = makeVpnUidRangeSet(PRIMARY_USER.id, newExcludedUids);
sPackages.put(PKGS[0], PKG_UIDS[0]);
vpn.refreshPlatformVpnAppExclusionList();
// List in keystore is not changed and the uid list should be updated in the net cap.
assertEquals(Arrays.asList(PKGS), vpn.getAppExclusionList(TEST_VPN_PKG));
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
- vpn.mNetworkCapabilities.getUids());
+ assertEquals(newUidRanges, vpn.mNetworkCapabilities.getUids());
verify(mMockNetworkAgent).doSendNetworkCapabilities(ncCaptor.capture());
- assertEquals(makeVpnUidRange(PRIMARY_USER.id, newExcludedUids),
- ncCaptor.getValue().getUids());
+ assertEquals(newUidRanges, ncCaptor.getValue().getUids());
+
+ // The uidRange is the same as the original setAppExclusionList so this is the second call
+ verify(mConnectivityManager, times(2))
+ .setVpnDefaultForUids(eq(sessionKey), eq(newUidRanges));
}
- private Set<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedList) {
+ private List<Range<Integer>> makeVpnUidRange(int userId, List<Integer> excludedAppIdList) {
final SortedSet<Integer> list = new TreeSet<>();
final int userBase = userId * UserHandle.PER_USER_RANGE;
- for (int uid : excludedList) {
- final int applicationUid = UserHandle.getUid(userId, uid);
- list.add(applicationUid);
- list.add(Process.toSdkSandboxUid(applicationUid)); // Add Sdk Sandbox UID
+ for (int appId : excludedAppIdList) {
+ final int uid = UserHandle.getUid(userId, appId);
+ list.add(uid);
+ if (Process.isApplicationUid(uid)) {
+ list.add(Process.toSdkSandboxUid(uid)); // Add Sdk Sandbox UID
+ }
}
final int minUid = userBase;
final int maxUid = userBase + UserHandle.PER_USER_RANGE - 1;
- final Set<Range<Integer>> ranges = new ArraySet<>();
+ final List<Range<Integer>> ranges = new ArrayList<>();
// Iterate the list to create the ranges between each uid.
int start = minUid;
@@ -1059,6 +1221,10 @@
return ranges;
}
+ private Set<Range<Integer>> makeVpnUidRangeSet(int userId, List<Integer> excludedAppIdList) {
+ return new ArraySet<>(makeVpnUidRange(userId, excludedAppIdList));
+ }
+
@Test
public void testSetAndGetAppExclusionListRestrictedUser() throws Exception {
final Vpn vpn = prepareVpnForVerifyAppExclusionList();
@@ -1643,6 +1809,9 @@
.getRedactedLinkPropertiesForPackage(any(), anyInt(), anyString());
final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
+ final Set<Range<Integer>> uidRanges = rangeSet(PRIMARY_USER_RANGE);
+ // This is triggered by Ikev2VpnRunner constructor.
+ verify(mConnectivityManager, times(1)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
final NetworkCallback cb = triggerOnAvailableAndGetCallback();
verifyInterfaceSetCfgWithFlags(IF_STATE_UP);
@@ -1651,6 +1820,8 @@
// state
verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
.createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+ // This is triggered by Vpn#startOrMigrateIkeSession().
+ verify(mConnectivityManager, times(2)).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
reset(mIkev2SessionCreator);
// For network lost case, the process should be triggered by calling onLost(), which is the
// same process with the real case.
@@ -1670,16 +1841,43 @@
new String[] {TEST_VPN_PKG}, new VpnProfileState(VpnProfileState.STATE_CONNECTING,
sessionKey, false /* alwaysOn */, false /* lockdown */));
if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
+ eq(Collections.EMPTY_LIST));
verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
.unregisterNetworkCallback(eq(cb));
} else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE
// Vpn won't retry when there is no usable underlying network.
&& errorCode != VpnManager.ERROR_CODE_NETWORK_LOST) {
int retryIndex = 0;
- final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+ // First failure occurred above.
+ final IkeSessionCallback retryCb = verifyRetryAndGetNewIkeCb(retryIndex++);
+ // Trigger 2 more failures to let the retry delay increase to 5s.
+ mExecutor.execute(() -> retryCb.onClosedWithException(exception));
+ final IkeSessionCallback retryCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
+ mExecutor.execute(() -> retryCb2.onClosedWithException(exception));
+ final IkeSessionCallback retryCb3 = verifyRetryAndGetNewIkeCb(retryIndex++);
- mExecutor.execute(() -> ikeCb2.onClosedWithException(exception));
+ // setVpnDefaultForUids may be called again but the uidRanges should not change.
+ verify(mConnectivityManager, atLeast(2)).setVpnDefaultForUids(eq(sessionKey),
+ mUidRangesCaptor.capture());
+ final List<Collection<Range<Integer>>> capturedUidRanges =
+ mUidRangesCaptor.getAllValues();
+ for (int i = 2; i < capturedUidRanges.size(); i++) {
+ // Assert equals no order.
+ assertTrue(
+ "uid ranges should not be modified. Expected: " + uidRanges
+ + ", actual: " + capturedUidRanges.get(i),
+ capturedUidRanges.get(i).containsAll(uidRanges)
+ && capturedUidRanges.get(i).size() == uidRanges.size());
+ }
+
+ // A fourth failure will cause the retry delay to be greater than 5s.
+ mExecutor.execute(() -> retryCb3.onClosedWithException(exception));
verifyRetryAndGetNewIkeCb(retryIndex++);
+
+ // The VPN network preference will be cleared when the retry delay is greater than 5s.
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey),
+ eq(Collections.EMPTY_LIST));
}
}
@@ -1841,16 +2039,7 @@
private Vpn startLegacyVpn(final Vpn vpn, final VpnProfile vpnProfile) throws Exception {
setMockedUsers(PRIMARY_USER);
-
- // 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);
+ vpn.startLegacyVpn(vpnProfile);
return vpn;
}
@@ -1962,7 +2151,9 @@
when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
.thenReturn(vpnProfile.encode());
- vpn.startVpnProfile(TEST_VPN_PKG);
+ final String sessionKey = vpn.startVpnProfile(TEST_VPN_PKG);
+ final Set<Range<Integer>> uidRanges = Collections.singleton(PRIMARY_USER_RANGE);
+ verify(mConnectivityManager).setVpnDefaultForUids(eq(sessionKey), eq(uidRanges));
final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
// There are 4 interactions with the executor.
// - Network available
@@ -2055,6 +2246,7 @@
final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+ verify(mConnectivityManager).setVpnDefaultForUids(anyString(), eq(Collections.EMPTY_LIST));
}
@Test
@@ -2952,23 +3144,29 @@
}
@Test
- public void testStartRacoonNumericAddress() throws Exception {
- startRacoon("1.2.3.4", "1.2.3.4");
+ public void testStartLegacyVpnType() throws Exception {
+ setMockedUsers(PRIMARY_USER);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final VpnProfile profile = new VpnProfile("testProfile" /* key */);
+
+ profile.type = VpnProfile.TYPE_PPTP;
+ assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
+ profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
+ assertThrows(UnsupportedOperationException.class, () -> startLegacyVpn(vpn, profile));
}
@Test
- public void testStartRacoonHostname() throws Exception {
- startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
- }
+ public void testStartLegacyVpnModifyProfile_TypePSK() throws Exception {
+ setMockedUsers(PRIMARY_USER);
+ final Vpn vpn = createVpn(PRIMARY_USER.id);
+ final Ikev2VpnProfile ikev2VpnProfile =
+ new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
+ .setAuthPsk(TEST_VPN_PSK)
+ .build();
+ final VpnProfile profile = ikev2VpnProfile.toVpnProfile();
- @Test
- public void testStartPptp() throws Exception {
- startPptp(true /* useMppe */);
- }
-
- @Test
- public void testStartPptp_NoMppe() throws Exception {
- startPptp(false /* useMppe */);
+ startLegacyVpn(vpn, profile);
+ assertEquals(profile, ikev2VpnProfile.toVpnProfile());
}
private void assertTransportInfoMatches(NetworkCapabilities nc, int type) {
@@ -2978,111 +3176,6 @@
assertEquals(type, ti.getType());
}
- private void startPptp(boolean useMppe) throws Exception {
- final VpnProfile profile = new VpnProfile("testProfile" /* key */);
- profile.type = VpnProfile.TYPE_PPTP;
- profile.name = "testProfileName";
- profile.username = "userName";
- profile.password = "thePassword";
- profile.server = "192.0.2.123";
- profile.mppe = useMppe;
-
- doReturn(new Network[] { new Network(101) }).when(mConnectivityManager).getAllNetworks();
- doReturn(new Network(102)).when(mConnectivityManager).registerNetworkAgent(any(), any(),
- any(), any(), any(), any(), anyInt());
-
- final Vpn vpn = startLegacyVpn(createVpn(PRIMARY_USER.id), profile);
- final TestDeps deps = (TestDeps) vpn.mDeps;
-
- testAndCleanup(() -> {
- final String[] mtpdArgs = deps.mtpdArgs.get(10, TimeUnit.SECONDS);
- final String[] argsPrefix = new String[]{
- EGRESS_IFACE, "pptp", profile.server, "1723", "name", profile.username,
- "password", profile.password, "linkname", "vpn", "refuse-eap", "nodefaultroute",
- "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270"
- };
- assertArrayEquals(argsPrefix, Arrays.copyOf(mtpdArgs, argsPrefix.length));
- if (useMppe) {
- assertEquals(argsPrefix.length + 2, mtpdArgs.length);
- assertEquals("+mppe", mtpdArgs[argsPrefix.length]);
- assertEquals("-pap", mtpdArgs[argsPrefix.length + 1]);
- } else {
- assertEquals(argsPrefix.length + 1, mtpdArgs.length);
- assertEquals("nomppe", mtpdArgs[argsPrefix.length]);
- }
-
- verify(mConnectivityManager, timeout(10_000)).registerNetworkAgent(any(), any(),
- any(), any(), any(), any(), anyInt());
- }, () -> { // Cleanup
- vpn.mVpnRunner.exitVpnRunner();
- deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
- vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
- });
- }
-
- public void startRacoon(final String serverAddr, final String expectedAddr)
- throws Exception {
- final ConditionVariable legacyRunnerReady = new ConditionVariable();
- 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(PRIMARY_USER.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
- }
- }
-
// Make it public and un-final so as to spy it
public class TestDeps extends Vpn.Dependencies {
public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
@@ -3220,12 +3313,6 @@
}
@Override
- public long getNextRetryDelayMs(int retryCount) {
- // Simply return retryCount as the delay seconds for retrying.
- return retryCount * 1000;
- }
-
- @Override
public long getValidationFailRecoveryMs(int retryCount) {
// Simply return retryCount as the delay seconds for retrying.
return retryCount * 100L;
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index 8eace1c..a86f923 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -153,10 +153,10 @@
thread.start()
doReturn(TEST_HOSTNAME).`when`(mockDeps).generateHostname()
doReturn(mockInterfaceAdvertiser1).`when`(mockDeps).makeAdvertiser(eq(mockSocket1),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
+ any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
)
doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
- any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
+ any(), any(), any(), any(), eq(TEST_HOSTNAME), any(), any()
)
doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
@@ -202,6 +202,7 @@
any(),
intAdvCbCaptor.capture(),
eq(TEST_HOSTNAME),
+ any(),
any()
)
@@ -259,10 +260,10 @@
val intAdvCbCaptor1 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
val intAdvCbCaptor2 = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any()
+ eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any()
+ eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(
anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
@@ -367,7 +368,7 @@
val intAdvCbCaptor = ArgumentCaptor.forClass(MdnsInterfaceAdvertiser.Callback::class.java)
verify(mockDeps).makeAdvertiser(eq(mockSocket1), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any()
+ eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
argThat { it.matches(SERVICE_1) }, eq(null))
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
index c39ee1e..2797462 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAnnouncerTest.kt
@@ -82,7 +82,8 @@
@Test
fun testAnnounce() {
- val replySender = MdnsReplySender( thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
@Suppress("UNCHECKED_CAST")
val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<BaseAnnouncementInfo>
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
index e869b91..5251e2a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -56,6 +56,7 @@
import java.util.concurrent.ScheduledExecutorService;
/** Tests for {@link MdnsDiscoveryManager}. */
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsDiscoveryManagerTests {
@@ -106,7 +107,7 @@
doReturn(thread.getLooper()).when(socketClient).getLooper();
doReturn(true).when(socketClient).supportsRequestingSpecificNetworks();
discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
- sharedLog) {
+ sharedLog, MdnsFeatureFlags.newBuilder().build()) {
@Override
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@NonNull SocketKey socketKey) {
@@ -134,9 +135,10 @@
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
if (thread != null) {
thread.quitSafely();
+ thread.join();
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index c19747e..db41a6a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -77,6 +77,7 @@
private val announcer = mock(MdnsAnnouncer::class.java)
private val prober = mock(MdnsProber::class.java)
private val sharedlog = SharedLog("MdnsInterfaceAdvertiserTest")
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Suppress("UNCHECKED_CAST")
private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -99,15 +100,14 @@
cb,
deps,
TEST_HOSTNAME,
- sharedlog
+ sharedlog,
+ flags
)
}
@Before
fun setUp() {
- doReturn(repository).`when`(deps).makeRecordRepository(any(),
- eq(TEST_HOSTNAME)
- )
+ doReturn(repository).`when`(deps).makeRecordRepository(any(), eq(TEST_HOSTNAME), any())
doReturn(replySender).`when`(deps).makeReplySender(anyString(), any(), any(), any(), any())
doReturn(announcer).`when`(deps).makeMdnsAnnouncer(anyString(), any(), any(), any(), any())
doReturn(prober).`when`(deps).makeMdnsProber(anyString(), any(), any(), any(), any())
@@ -190,8 +190,8 @@
fun testReplyToQuery() {
addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val mockReply = mock(MdnsRecordRepository.ReplyInfo::class.java)
- doReturn(mockReply).`when`(repository).getReply(any(), any())
+ val testReply = MdnsReplyInfo(emptyList(), emptyList(), 0, InetSocketAddress(0))
+ doReturn(testReply).`when`(repository).getReply(any(), any())
// Query obtained with:
// scapy.raw(scapy.DNS(
@@ -216,7 +216,7 @@
assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.questions[0].name)
}
- verify(replySender).queueReply(mockReply)
+ verify(replySender).queueReply(testReply)
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 3701b0c..ad30ce0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -42,6 +42,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -69,6 +70,7 @@
@Mock private SocketCreationCallback mSocketCreationCallback;
@Mock private SharedLog mSharedLog;
private MdnsMultinetworkSocketClient mSocketClient;
+ private HandlerThread mHandlerThread;
private Handler mHandler;
private SocketKey mSocketKey;
@@ -76,14 +78,23 @@
public void setUp() throws SocketException {
MockitoAnnotations.initMocks(this);
- final HandlerThread thread = new HandlerThread("MdnsMultinetworkSocketClientTest");
- thread.start();
- mHandler = new Handler(thread.getLooper());
+ mHandlerThread = new HandlerThread("MdnsMultinetworkSocketClientTest");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
mSocketKey = new SocketKey(1000 /* interfaceIndex */);
- mSocketClient = new MdnsMultinetworkSocketClient(thread.getLooper(), mProvider, mSharedLog);
+ mSocketClient = new MdnsMultinetworkSocketClient(mHandlerThread.getLooper(), mProvider,
+ mSharedLog, MdnsFeatureFlags.newBuilder().build());
mHandler.post(() -> mSocketClient.setCallback(mCallback));
}
+ @After
+ public void tearDown() throws Exception {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ }
+
private SocketCallback expectSocketCallback() {
return expectSocketCallback(mListener, mNetwork);
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
index 19d8a00..37588b5 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketReaderTests.java
@@ -75,7 +75,7 @@
+ "the packet length");
} catch (IOException e) {
// Expected
- } catch (Exception e) {
+ } catch (RuntimeException e) {
fail(String.format(
Locale.ROOT,
"Should not have thrown any other exception except " + "for IOException: %s",
@@ -83,4 +83,4 @@
}
assertEquals(data.length, packetReader.getRemaining());
}
-}
\ No newline at end of file
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
index b667e5f..0877b68 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsPacketTest.kt
@@ -27,20 +27,24 @@
@RunWith(DevSdkIgnoreRunner::class)
class MdnsPacketTest {
+ private fun makeFlags(isLabelCountLimitEnabled: Boolean = false): MdnsFeatureFlags =
+ MdnsFeatureFlags.newBuilder()
+ .setIsLabelCountLimitEnabled(isLabelCountLimitEnabled).build()
@Test
fun testParseQuery() {
// Probe packet with 1 question for Android.local, and 4 additionalRecords with 4 addresses
// for Android.local (similar to legacy mdnsresponder probes, although it used to put 4
// identical questions(!!) for Android.local when there were 4 addresses).
- val packetHex = "00000000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
+ val packetHex = "007b0000000100000004000007416e64726f6964056c6f63616c0000ff0001c00c000100" +
"01000000780004c000027bc00c001c000100000078001020010db8000000000000000000000123c0" +
"0c001c000100000078001020010db8000000000000000000000456c00c001c000100000078001020" +
"010db8000000000000000000000789"
val bytes = HexDump.hexStringToByteArray(packetHex)
- val reader = MdnsPacketReader(bytes, bytes.size)
+ val reader = MdnsPacketReader(bytes, bytes.size, makeFlags())
val packet = MdnsPacket.parse(reader)
+ assertEquals(123, packet.transactionId)
assertEquals(1, packet.questions.size)
assertEquals(0, packet.answers.size)
assertEquals(4, packet.authorityRecords.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
index f284819..5b7c0ba 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsProberTest.kt
@@ -119,7 +119,8 @@
@Test
fun testProbe() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)))
@@ -143,7 +144,8 @@
@Test
fun testProbeMultipleRecords() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(listOf(
makeServiceRecord(TEST_SERVICE_NAME_1, 37890),
@@ -181,7 +183,8 @@
@Test
fun testStopProbing() {
- val replySender = MdnsReplySender(thread.looper, socket, buffer, sharedLog)
+ val replySender = MdnsReplySender(
+ thread.looper, socket, buffer, sharedLog, true /* enableDebugLog */)
val prober = TestProber(thread.looper, replySender, cb, sharedLog)
val probeInfo = TestProbeInfo(
listOf(makeServiceRecord(TEST_SERVICE_NAME_1, 37890)),
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 88fb66a..f26f7e1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -78,6 +78,7 @@
override fun getInterfaceInetAddresses(iface: NetworkInterface) =
Collections.enumeration(TEST_ADDRESSES.map { it.address })
}
+ private val flags = MdnsFeatureFlags.newBuilder().build()
@Before
fun setUp() {
@@ -92,7 +93,7 @@
@Test
fun testAddServiceAndProbe() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertEquals(0, repository.servicesCount)
assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
null /* subtype */))
@@ -105,6 +106,7 @@
assertEquals(TEST_SERVICE_ID_1, probingInfo.serviceId)
val packet = probingInfo.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
assertEquals(0, packet.answers.size)
assertEquals(0, packet.additionalRecords.size)
@@ -126,7 +128,7 @@
@Test
fun testAddAndConflicts() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(NameConflictException::class) {
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
@@ -138,7 +140,7 @@
@Test
fun testInvalidReuseOfServiceId() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(IllegalArgumentException::class) {
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
@@ -147,7 +149,7 @@
@Test
fun testHasActiveService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
@@ -164,7 +166,7 @@
@Test
fun testExitAnnouncements() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -173,6 +175,7 @@
assertEquals(1, repository.servicesCount)
val packet = exitAnnouncement.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(0x8400 /* response, authoritative */, packet.flags)
assertEquals(0, packet.questions.size)
assertEquals(0, packet.authorityRecords.size)
@@ -193,7 +196,7 @@
@Test
fun testExitAnnouncements_WithSubtype() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
@@ -202,6 +205,7 @@
assertEquals(1, repository.servicesCount)
val packet = exitAnnouncement.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(0x8400 /* response, authoritative */, packet.flags)
assertEquals(0, packet.questions.size)
assertEquals(0, packet.authorityRecords.size)
@@ -228,7 +232,7 @@
@Test
fun testExitingServiceReAdded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
repository.exitService(TEST_SERVICE_ID_1)
@@ -243,12 +247,13 @@
@Test
fun testOnProbingSucceeded() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
TEST_SUBTYPE)
repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
val packet = announcementInfo.getPacket(0)
+ assertEquals(0, packet.transactionId)
assertEquals(0x8400 /* response, authoritative */, packet.flags)
assertEquals(0, packet.questions.size)
assertEquals(0, packet.authorityRecords.size)
@@ -367,11 +372,12 @@
@Test
fun testGetOffloadPacket() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val serviceType = arrayOf("_testservice", "_tcp", "local")
val offloadPacket = repository.getOffloadPacket(TEST_SERVICE_ID_1)
+ assertEquals(0, offloadPacket.transactionId)
assertEquals(0x8400, offloadPacket.flags)
assertEquals(0, offloadPacket.questions.size)
assertEquals(0, offloadPacket.additionalRecords.size)
@@ -428,7 +434,7 @@
@Test
fun testGetReplyCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
val questionsCaseInSensitive =
listOf(MdnsPointerRecord(arrayOf("_TESTSERVICE", "_TCP", "local"),
@@ -458,7 +464,7 @@
}
private fun doGetReplyTest(subtype: String?) {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
@@ -546,7 +552,7 @@
@Test
fun testGetConflictingServices() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -574,7 +580,7 @@
@Test
fun testGetConflictingServicesCaseInsensitive() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -602,7 +608,7 @@
@Test
fun testGetConflictingServices_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -631,7 +637,7 @@
@Test
fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
@@ -660,7 +666,7 @@
@Test
fun testGetServiceRepliedRequestsCount() {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
// Verify that there is no packet replied.
assertEquals(MdnsConstants.NO_PACKET,
@@ -685,6 +691,68 @@
assertEquals(MdnsConstants.NO_PACKET,
repository.getServiceRepliedRequestsCount(TEST_SERVICE_ID_2))
}
+
+ @Test
+ fun testIncludeInetAddressRecordsInProbing() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME,
+ MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
+ repository.updateAddresses(TEST_ADDRESSES)
+ assertEquals(0, repository.servicesCount)
+ assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ null /* subtype */))
+ assertEquals(1, repository.servicesCount)
+
+ val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
+ assertNotNull(probingInfo)
+ assertTrue(repository.isProbing(TEST_SERVICE_ID_1))
+
+ assertEquals(TEST_SERVICE_ID_1, probingInfo.serviceId)
+ val packet = probingInfo.getPacket(0)
+
+ assertEquals(MdnsConstants.FLAGS_QUERY, packet.flags)
+ assertEquals(0, packet.answers.size)
+ assertEquals(0, packet.additionalRecords.size)
+
+ assertEquals(2, packet.questions.size)
+ val expectedName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+ assertContentEquals(listOf(
+ MdnsAnyRecord(expectedName, false /* unicast */),
+ MdnsAnyRecord(TEST_HOSTNAME, false /* unicast */),
+ ), packet.questions)
+
+ assertEquals(4, packet.authorityRecords.size)
+ assertContentEquals(listOf(
+ MdnsServiceRecord(
+ expectedName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ TEST_PORT,
+ TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ 120_000L /* ttlMillis */,
+ TEST_ADDRESSES[2].address)
+ ), packet.authorityRecords)
+
+ assertContentEquals(intArrayOf(TEST_SERVICE_ID_1), repository.clearServices())
+ }
}
private fun MdnsRecordRepository.initWithService(
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
index 3fc656a..a22e8c6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -17,8 +17,10 @@
package com.android.server.connectivity.mdns;
import static android.net.InetAddresses.parseNumericAddress;
+
import static com.android.server.connectivity.mdns.util.MdnsUtils.Clock;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -337,7 +339,8 @@
packet.setSocketAddress(
new InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT));
- final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data6, data6.length);
+ final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(
+ data6, data6.length, MdnsFeatureFlags.newBuilder().build());
assertNotNull(parsedPacket);
final Network network = mock(Network.class);
@@ -636,7 +639,8 @@
private ArraySet<MdnsResponse> decode(MdnsResponseDecoder decoder, byte[] data,
Collection<MdnsResponse> existingResponses) throws MdnsPacket.ParseException {
- final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(data, data.length);
+ final MdnsPacket parsedPacket = MdnsResponseDecoder.parseResponse(
+ data, data.length, MdnsFeatureFlags.newBuilder().build());
assertNotNull(parsedPacket);
return new ArraySet<>(decoder.augmentResponses(parsedPacket,
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
index b43bcf7..b040ab6 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceCacheTest.kt
@@ -19,6 +19,10 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.net.module.util.ArrayTrackRecord
+import com.android.server.connectivity.mdns.MdnsServiceCache.CacheKey
+import com.android.server.connectivity.mdns.MdnsServiceCacheTest.ExpiredRecord.ExpiredEvent.ServiceRecordExpired
+import com.android.server.connectivity.mdns.util.MdnsUtils
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.util.concurrent.CompletableFuture
@@ -31,24 +35,66 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
private const val SERVICE_NAME_1 = "service-instance-1"
private const val SERVICE_NAME_2 = "service-instance-2"
+private const val SERVICE_NAME_3 = "service-instance-3"
private const val SERVICE_TYPE_1 = "_test1._tcp.local"
private const val SERVICE_TYPE_2 = "_test2._tcp.local"
private const val INTERFACE_INDEX = 999
private const val DEFAULT_TIMEOUT_MS = 2000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+private const val TEST_ELAPSED_REALTIME_MS = 123L
+private const val DEFAULT_TTL_TIME_MS = 120000L
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsServiceCacheTest {
private val socketKey = SocketKey(null /* network */, INTERFACE_INDEX)
+ private val cacheKey1 = CacheKey(SERVICE_TYPE_1, socketKey)
+ private val cacheKey2 = CacheKey(SERVICE_TYPE_2, socketKey)
private val thread = HandlerThread(MdnsServiceCacheTest::class.simpleName)
+ private val clock = mock(MdnsUtils.Clock::class.java)
private val handler by lazy {
Handler(thread.looper)
}
- private val serviceCache by lazy {
- MdnsServiceCache(thread.looper)
+
+ private class ExpiredRecord : MdnsServiceCache.ServiceExpiredCallback {
+ val history = ArrayTrackRecord<ExpiredEvent>().newReadHead()
+
+ sealed class ExpiredEvent {
+ abstract val previousResponse: MdnsResponse
+ abstract val newResponse: MdnsResponse?
+ data class ServiceRecordExpired(
+ override val previousResponse: MdnsResponse,
+ override val newResponse: MdnsResponse?
+ ) : ExpiredEvent()
+ }
+
+ override fun onServiceRecordExpired(
+ previousResponse: MdnsResponse,
+ newResponse: MdnsResponse?
+ ) {
+ history.add(ServiceRecordExpired(previousResponse, newResponse))
+ }
+
+ fun expectedServiceRecordExpired(
+ serviceName: String,
+ timeoutMs: Long = DEFAULT_TIMEOUT_MS
+ ) {
+ val event = history.poll(timeoutMs)
+ assertNotNull(event)
+ assertTrue(event is ServiceRecordExpired)
+ assertEquals(serviceName, event.previousResponse.serviceInstanceName)
+ }
+
+ fun assertNoCallback() {
+ val cb = history.poll(NO_CALLBACK_TIMEOUT_MS)
+ assertNull("Expected no callback but got $cb", cb)
+ }
}
@Before
@@ -59,8 +105,14 @@
@After
fun tearDown() {
thread.quitSafely()
+ thread.join()
}
+ private fun makeFlags(isExpiredServicesRemovalEnabled: Boolean = false) =
+ MdnsFeatureFlags.Builder()
+ .setIsExpiredServicesRemovalEnabled(isExpiredServicesRemovalEnabled)
+ .build()
+
private fun <T> runningOnHandlerAndReturn(functor: (() -> T)): T {
val future = CompletableFuture<T>()
handler.post {
@@ -70,46 +122,58 @@
}
private fun addOrUpdateService(
- serviceType: String,
- socketKey: SocketKey,
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey,
service: MdnsResponse
- ): Unit = runningOnHandlerAndReturn {
- serviceCache.addOrUpdateService(serviceType, socketKey, service)
+ ): Unit = runningOnHandlerAndReturn { serviceCache.addOrUpdateService(cacheKey, service) }
+
+ private fun removeService(
+ serviceCache: MdnsServiceCache,
+ serviceName: String,
+ cacheKey: CacheKey
+ ): Unit = runningOnHandlerAndReturn { serviceCache.removeService(serviceName, cacheKey) }
+
+ private fun getService(
+ serviceCache: MdnsServiceCache,
+ serviceName: String,
+ cacheKey: CacheKey
+ ): MdnsResponse? = runningOnHandlerAndReturn {
+ serviceCache.getCachedService(serviceName, cacheKey)
}
- private fun removeService(serviceName: String, serviceType: String, socketKey: SocketKey):
- Unit = runningOnHandlerAndReturn {
- serviceCache.removeService(serviceName, serviceType, socketKey) }
+ private fun getServices(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey
+ ): List<MdnsResponse> = runningOnHandlerAndReturn { serviceCache.getCachedServices(cacheKey) }
- private fun getService(serviceName: String, serviceType: String, socketKey: SocketKey):
- MdnsResponse? = runningOnHandlerAndReturn {
- serviceCache.getCachedService(serviceName, serviceType, socketKey) }
-
- private fun getServices(serviceType: String, socketKey: SocketKey): List<MdnsResponse> =
- runningOnHandlerAndReturn { serviceCache.getCachedServices(serviceType, socketKey) }
+ private fun registerServiceExpiredCallback(
+ serviceCache: MdnsServiceCache,
+ cacheKey: CacheKey,
+ callback: MdnsServiceCache.ServiceExpiredCallback
+ ) = runningOnHandlerAndReturn {
+ serviceCache.registerServiceExpiredCallback(cacheKey, callback)
+ }
@Test
fun testAddAndRemoveService() {
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
- var response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ var response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
assertNotNull(response)
assertEquals(SERVICE_NAME_1, response.serviceInstanceName)
- removeService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
- response = getService(SERVICE_NAME_1, SERVICE_TYPE_1, socketKey)
+ removeService(serviceCache, SERVICE_NAME_1, cacheKey1)
+ response = getService(serviceCache, SERVICE_NAME_1, cacheKey1)
assertNull(response)
}
@Test
fun testGetCachedServices_multipleServiceTypes() {
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
- addOrUpdateService(
- SERVICE_TYPE_1, socketKey, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
- addOrUpdateService(
- SERVICE_TYPE_2, socketKey, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
+ val serviceCache = MdnsServiceCache(thread.looper, makeFlags(), clock)
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_2, SERVICE_TYPE_2))
- val responses1 = getServices(SERVICE_TYPE_1, socketKey)
+ val responses1 = getServices(serviceCache, cacheKey1)
assertEquals(2, responses1.size)
assertTrue(responses1.stream().anyMatch { response ->
response.serviceInstanceName == SERVICE_NAME_1
@@ -117,26 +181,146 @@
assertTrue(responses1.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
- val responses2 = getServices(SERVICE_TYPE_2, socketKey)
+ val responses2 = getServices(serviceCache, cacheKey2)
assertEquals(1, responses2.size)
assertTrue(responses2.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
- removeService(SERVICE_NAME_2, SERVICE_TYPE_1, socketKey)
- val responses3 = getServices(SERVICE_TYPE_1, socketKey)
+ removeService(serviceCache, SERVICE_NAME_2, cacheKey1)
+ val responses3 = getServices(serviceCache, cacheKey1)
assertEquals(1, responses3.size)
assertTrue(responses3.any { response ->
response.serviceInstanceName == SERVICE_NAME_1
})
- val responses4 = getServices(SERVICE_TYPE_2, socketKey)
+ val responses4 = getServices(serviceCache, cacheKey2)
assertEquals(1, responses4.size)
assertTrue(responses4.any { response ->
response.serviceInstanceName == SERVICE_NAME_2
})
}
- private fun createResponse(serviceInstanceName: String, serviceType: String) = MdnsResponse(
- 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
- socketKey.interfaceIndex, socketKey.network)
+ @Test
+ fun testServiceExpiredAndSendCallbacks() {
+ val serviceCache = MdnsServiceCache(
+ thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+ // Register service expired callbacks
+ val callback1 = ExpiredRecord()
+ val callback2 = ExpiredRecord()
+ registerServiceExpiredCallback(serviceCache, cacheKey1, callback1)
+ registerServiceExpiredCallback(serviceCache, cacheKey2, callback2)
+
+ doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+
+ // Add multiple services with different ttl time.
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_1, SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS))
+ addOrUpdateService(serviceCache, cacheKey1, createResponse(SERVICE_NAME_2, SERVICE_TYPE_1,
+ DEFAULT_TTL_TIME_MS + 20L))
+ addOrUpdateService(serviceCache, cacheKey2, createResponse(SERVICE_NAME_3, SERVICE_TYPE_2,
+ DEFAULT_TTL_TIME_MS + 10L))
+
+ // Check the service expiration immediately. Should be no callback.
+ assertEquals(2, getServices(serviceCache, cacheKey1).size)
+ assertEquals(1, getServices(serviceCache, cacheKey2).size)
+ callback1.assertNoCallback()
+ callback2.assertNoCallback()
+
+ // Simulate the case where the response is after TTL then check expired services.
+ // Expect SERVICE_NAME_1 expired.
+ doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS).`when`(clock).elapsedRealtime()
+ assertEquals(1, getServices(serviceCache, cacheKey1).size)
+ assertEquals(1, getServices(serviceCache, cacheKey2).size)
+ callback1.expectedServiceRecordExpired(SERVICE_NAME_1)
+ callback2.assertNoCallback()
+
+ // Simulate the case where the response is after TTL then check expired services.
+ // Expect SERVICE_NAME_3 expired.
+ doReturn(TEST_ELAPSED_REALTIME_MS + DEFAULT_TTL_TIME_MS + 11L)
+ .`when`(clock).elapsedRealtime()
+ assertEquals(1, getServices(serviceCache, cacheKey1).size)
+ assertEquals(0, getServices(serviceCache, cacheKey2).size)
+ callback1.assertNoCallback()
+ callback2.expectedServiceRecordExpired(SERVICE_NAME_3)
+ }
+
+ @Test
+ fun testRemoveExpiredServiceWhenGetting() {
+ val serviceCache = MdnsServiceCache(
+ thread.looper, makeFlags(isExpiredServicesRemovalEnabled = true), clock)
+
+ doReturn(TEST_ELAPSED_REALTIME_MS).`when`(clock).elapsedRealtime()
+ addOrUpdateService(serviceCache, cacheKey1,
+ createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 1L /* ttlTime */))
+ doReturn(TEST_ELAPSED_REALTIME_MS + 2L).`when`(clock).elapsedRealtime()
+ assertNull(getService(serviceCache, SERVICE_NAME_1, cacheKey1))
+
+ addOrUpdateService(serviceCache, cacheKey2,
+ createResponse(SERVICE_NAME_2, SERVICE_TYPE_2, 3L /* ttlTime */))
+ doReturn(TEST_ELAPSED_REALTIME_MS + 4L).`when`(clock).elapsedRealtime()
+ assertEquals(0, getServices(serviceCache, cacheKey2).size)
+ }
+
+ @Test
+ fun testInsertResponseAndSortList() {
+ val responses = ArrayList<MdnsResponse>()
+ val response1 = createResponse(SERVICE_NAME_1, SERVICE_TYPE_1, 100L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response1, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(1, responses.size)
+ assertEquals(response1, responses[0])
+
+ val response2 = createResponse(SERVICE_NAME_2, SERVICE_TYPE_1, 50L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response2, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(2, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response1, responses[1])
+
+ val response3 = createResponse(SERVICE_NAME_3, SERVICE_TYPE_1, 75L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response3, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(3, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response3, responses[1])
+ assertEquals(response1, responses[2])
+
+ val response4 = createResponse("service-instance-4", SERVICE_TYPE_1, 125L /* ttlTime */)
+ MdnsServiceCache.insertResponseAndSortList(responses, response4, TEST_ELAPSED_REALTIME_MS)
+ assertEquals(4, responses.size)
+ assertEquals(response2, responses[0])
+ assertEquals(response3, responses[1])
+ assertEquals(response1, responses[2])
+ assertEquals(response4, responses[3])
+ }
+
+ private fun createResponse(
+ serviceInstanceName: String,
+ serviceType: String,
+ ttlTime: Long = 120000L
+ ): MdnsResponse {
+ val serviceName = "$serviceInstanceName.$serviceType".split(".").toTypedArray()
+ val response = MdnsResponse(
+ 0 /* now */, "$serviceInstanceName.$serviceType".split(".").toTypedArray(),
+ socketKey.interfaceIndex, socketKey.network)
+
+ // Set PTR record
+ val pointerRecord = MdnsPointerRecord(
+ serviceType.split(".").toTypedArray(),
+ TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ ttlTime /* ttlMillis */,
+ serviceName)
+ response.addPointerRecord(pointerRecord)
+
+ // Set SRV record.
+ val serviceRecord = MdnsServiceRecord(
+ serviceName,
+ TEST_ELAPSED_REALTIME_MS /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ ttlTime /* ttlMillis */,
+ 0 /* servicePriority */,
+ 0 /* serviceWeight */,
+ 12345 /* port */,
+ arrayOf("hostname"))
+ response.serviceRecord = serviceRecord
+ return response
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index fde5abd..7a2e4bf 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -87,6 +87,7 @@
import java.util.stream.Stream;
/** Tests for {@link MdnsServiceTypeClient}. */
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsServiceTypeClientTests {
@@ -193,7 +194,10 @@
thread = new HandlerThread("MdnsServiceTypeClientTests");
thread.start();
handler = new Handler(thread.getLooper());
- serviceCache = new MdnsServiceCache(thread.getLooper());
+ serviceCache = new MdnsServiceCache(
+ thread.getLooper(),
+ MdnsFeatureFlags.newBuilder().setIsExpiredServicesRemovalEnabled(false).build(),
+ mockDecoderClock);
doAnswer(inv -> {
latestDelayMs = 0;
@@ -227,9 +231,10 @@
}
@After
- public void tearDown() {
+ public void tearDown() throws Exception {
if (thread != null) {
thread.quitSafely();
+ thread.join();
}
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
index 74f1c37..8b7ab71 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketClientTests.java
@@ -78,6 +78,7 @@
@Mock private SharedLog sharedLog;
private MdnsSocketClient mdnsClient;
+ private MdnsFeatureFlags flags = MdnsFeatureFlags.newBuilder().build();
@Before
public void setup() throws RuntimeException, IOException {
@@ -86,7 +87,7 @@
when(mockWifiManager.createMulticastLock(ArgumentMatchers.anyString()))
.thenReturn(mockMulticastLock);
- mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog) {
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
@Override
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) throws IOException {
if (port == MdnsConstants.MDNS_PORT) {
@@ -515,7 +516,7 @@
//MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(true);
when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21);
- mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog) {
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
@Override
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) {
if (port == MdnsConstants.MDNS_PORT) {
@@ -538,7 +539,7 @@
//MdnsConfigsFlagsImpl.allowNetworkInterfaceIndexPropagation.override(false);
when(mockMulticastSocket.getInterfaceIndex()).thenReturn(21);
- mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog) {
+ mdnsClient = new MdnsSocketClient(mContext, mockMulticastLock, sharedLog, flags) {
@Override
MdnsSocket createMdnsSocket(int port, SharedLog sharedLog) {
if (port == MdnsConstants.MDNS_PORT) {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
index c62a081..3e1dab8 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/internal/SocketNetlinkMonitorTest.kt
@@ -27,6 +27,7 @@
private val LINKADDRV4 = LinkAddress("192.0.2.0/24")
private val IFACE_IDX = 32
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
internal class SocketNetlinkMonitorTest {
@@ -43,6 +44,7 @@
@After
fun tearDown() {
thread.quitSafely()
+ thread.join()
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
index 6f8ba6c..58f20a9 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
@file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it
package com.android.server
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
new file mode 100644
index 0000000..572c7bb
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val LONG_TIMEOUT_MS = 5_000
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+class CSDestroyedNetworkTests : CSTest() {
+ @Test
+ fun testDestroyNetworkNotKeptWhenUnvalidated() {
+ val nc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build()
+
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_WIFI)
+ .build()
+ val cbRequest = TestableNetworkCallback()
+ val cbCallback = TestableNetworkCallback()
+ cm.requestNetwork(nr, cbRequest)
+ cm.registerNetworkCallback(nr, cbCallback)
+
+ val firstAgent = Agent(nc = nc)
+ firstAgent.connect()
+ cbCallback.expectAvailableCallbacks(firstAgent.network, validated = false)
+
+ firstAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+ val secondAgent = Agent(nc = nc)
+ secondAgent.connect()
+ cbCallback.expectAvailableCallbacks(secondAgent.network, validated = false)
+
+ cbCallback.expect<Lost>(timeoutMs = 500) { it.network == firstAgent.network }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
new file mode 100644
index 0000000..a753922
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSKeepConnectedTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSKeepConnectedTest : CSTest() {
+ @Test
+ fun testKeepConnectedLocalAgent() {
+ deps.setBuildSdk(VERSION_V)
+ val nc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build()
+ val keepConnectedAgent = Agent(nc = nc, score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+ .build()),
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
+ val dontKeepConnectedAgent = Agent(nc = nc,
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
+ doTestKeepConnected(keepConnectedAgent, dontKeepConnectedAgent)
+ }
+
+ @Test
+ fun testKeepConnectedForTest() {
+ val keepAgent = Agent(score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST)
+ .build()))
+ val dontKeepAgent = Agent()
+ doTestKeepConnected(keepAgent, dontKeepAgent)
+ }
+
+ fun doTestKeepConnected(keepAgent: CSAgentWrapper, dontKeepAgent: CSAgentWrapper) {
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ keepAgent.connect()
+ dontKeepAgent.connect()
+
+ cb.expectAvailableCallbacks(keepAgent.network, validated = false)
+ cb.expectAvailableCallbacks(dontKeepAgent.network, validated = false)
+
+ // After the nascent timer, the agent without keep connected gets lost.
+ cb.expect<Lost>(dontKeepAgent.network)
+ cb.assertNoCallback()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
new file mode 100644
index 0000000..6add6b9
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentCreationTests.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import android.net.INetd
+import android.net.LocalNetworkConfig
+import android.net.NativeNetworkConfig
+import android.net.NativeNetworkType
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.VpnManager
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import kotlin.test.assertFailsWith
+
+private const val TIMEOUT_MS = 2_000L
+private const val NO_CALLBACK_TIMEOUT_MS = 200L
+
+private fun keepConnectedScore() =
+ FromS(NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build())
+
+private fun defaultLnc() = FromS(LocalNetworkConfig.Builder().build())
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSLocalAgentCreationTests(
+ private val sdkLevel: Int,
+ private val isTv: Boolean,
+ private val addLocalNetCapToRequest: Boolean
+) : CSTest() {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun arguments() = listOf(
+ arrayOf(VERSION_V, false /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_V, false /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_V, true /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_V, true /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, false /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, false /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, true /* isTv */, true /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_U, true /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_T, false /* isTv */, false /* addLocalNetCapToRequest */),
+ arrayOf(VERSION_T, true /* isTv */, false /* addLocalNetCapToRequest */),
+ )
+ }
+
+ private fun makeNativeNetworkConfigLocal(netId: Int, permission: Int) =
+ NativeNetworkConfig(netId, NativeNetworkType.PHYSICAL_LOCAL, permission,
+ false /* secure */, VpnManager.TYPE_VPN_NONE, false /* excludeLocalRoutes */)
+
+ @Test
+ fun testLocalAgents() {
+ val netdInOrder = inOrder(netd)
+ deps.setBuildSdk(sdkLevel)
+ doReturn(isTv).`when`(packageManager).hasSystemFeature(FEATURE_LEANBACK)
+ val allNetworksCb = TestableNetworkCallback()
+ val request = NetworkRequest.Builder()
+ if (addLocalNetCapToRequest) {
+ request.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ }
+ cm.registerNetworkCallback(request.build(), allNetworksCb)
+ val ncTemplate = NetworkCapabilities.Builder().run {
+ addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ }.build()
+ val localAgent = if (sdkLevel >= VERSION_V || sdkLevel == VERSION_U && isTv) {
+ Agent(nc = ncTemplate, score = keepConnectedScore(), lnc = defaultLnc())
+ } else {
+ assertFailsWith<IllegalArgumentException> { Agent(nc = ncTemplate, lnc = defaultLnc()) }
+ netdInOrder.verify(netd, never()).networkCreate(any())
+ return
+ }
+ localAgent.connect()
+ netdInOrder.verify(netd).networkCreate(
+ makeNativeNetworkConfigLocal(localAgent.network.netId, INetd.PERMISSION_NONE))
+ if (addLocalNetCapToRequest) {
+ assertEquals(localAgent.network, allNetworksCb.expect<Available>().network)
+ } else {
+ allNetworksCb.assertNoCallback(NO_CALLBACK_TIMEOUT_MS)
+ }
+ cm.unregisterNetworkCallback(allNetworksCb)
+ localAgent.disconnect()
+ netdInOrder.verify(netd, timeout(TIMEOUT_MS)).networkDestroy(localAgent.network.netId)
+ }
+
+ @Test
+ fun testBadAgents() {
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ lnc = null)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder().build(), lnc = defaultLnc())
+ }
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
new file mode 100644
index 0000000..ad21bf5
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.LocalNetworkConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_DUN
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_THREAD
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkScore.KEEP_CONNECTED_FOR_TEST
+import android.net.NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK
+import android.net.RouteInfo
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import kotlin.test.assertFailsWith
+
+private const val TIMEOUT_MS = 200L
+private const val MEDIUM_TIMEOUT_MS = 1_000L
+private const val LONG_TIMEOUT_MS = 5_000
+
+private fun nc(transport: Int, vararg caps: Int) = NetworkCapabilities.Builder().apply {
+ addTransportType(transport)
+ caps.forEach {
+ addCapability(it)
+ }
+ // Useful capabilities for everybody
+ addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ addCapability(NET_CAPABILITY_NOT_ROAMING)
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+}.build()
+
+private fun lp(iface: String) = LinkProperties().apply {
+ interfaceName = iface
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
+// This allows keeping all the networks connected without having to file individual requests
+// for them.
+private fun keepScore() = FromS(
+ NetworkScore.Builder().setKeepConnectedReason(KEEP_CONNECTED_FOR_TEST).build()
+)
+
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class CSLocalAgentTests : CSTest() {
+ @Test
+ fun testBadAgents() {
+ deps.setBuildSdk(VERSION_V)
+
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ lnc = null)
+ }
+ assertFailsWith<IllegalArgumentException> {
+ Agent(nc = NetworkCapabilities.Builder().build(),
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
+ }
+ }
+
+ @Test
+ fun testStructuralConstraintViolation() {
+ deps.setBuildSdk(VERSION_V)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(NetworkRequest.Builder()
+ .clearCapabilities()
+ .build(),
+ cb)
+ val agent = Agent(nc = NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ lnc = FromS(LocalNetworkConfig.Builder().build()))
+ agent.connect()
+ cb.expectAvailableCallbacks(agent.network, validated = false)
+ agent.sendNetworkCapabilities(NetworkCapabilities.Builder().build())
+ cb.expect<Lost>(agent.network)
+
+ val agent2 = Agent(nc = NetworkCapabilities.Builder()
+ .build(),
+ lnc = null)
+ agent2.connect()
+ cb.expectAvailableCallbacks(agent2.network, validated = false)
+ agent2.sendNetworkCapabilities(NetworkCapabilities.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build())
+ cb.expect<Lost>(agent2.network)
+ }
+
+ @Test
+ fun testUpdateLocalAgentConfig() {
+ deps.setBuildSdk(VERSION_V)
+
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ cb)
+
+ // Set up a local agent that should forward its traffic to the best DUN upstream.
+ val localAgent = Agent(
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = FromS(LocalNetworkConfig.Builder().build()),
+ )
+ localAgent.connect()
+
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ wifiAgent.connect()
+
+ val newLnc = LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ .build()
+ localAgent.sendLocalNetworkConfig(newLnc)
+
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ localAgent.sendLocalNetworkConfig(LocalNetworkConfig.Builder().build())
+ cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+ localAgent.sendLocalNetworkConfig(newLnc)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ wifiAgent.disconnect()
+ cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+ localAgent.disconnect()
+ }
+
+ @Test
+ fun testUnregisterUpstreamAfterReplacement_SameIfaceName() {
+ doTestUnregisterUpstreamAfterReplacement(true)
+ }
+
+ @Test
+ fun testUnregisterUpstreamAfterReplacement_DifferentIfaceName() {
+ doTestUnregisterUpstreamAfterReplacement(false)
+ }
+
+ fun doTestUnregisterUpstreamAfterReplacement(sameIfaceName: Boolean) {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ // Set up a local agent that should forward its traffic to the best wifi upstream.
+ val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ .build()),
+ score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+ .build())
+ )
+ localAgent.connect()
+
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val wifiAgent = Agent(lp = lp("wifi0"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ wifiAgent.connect()
+
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ clearInvocations(netd)
+ val inOrder = inOrder(netd)
+ wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ waitForIdle()
+ inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+ inOrder.verify(netd).networkDestroy(wifiAgent.network.netId)
+
+ val wifiIface2 = if (sameIfaceName) "wifi0" else "wifi1"
+ val wifiAgent2 = Agent(lp = lp(wifiIface2),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ wifiAgent2.connect()
+
+ cb.expectAvailableCallbacks(wifiAgent2.network, validated = false)
+ cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == wifiAgent2.network }
+ cb.expect<Lost> { it.network == wifiAgent.network }
+
+ inOrder.verify(netd).ipfwdAddInterfaceForward("local0", wifiIface2)
+ if (sameIfaceName) {
+ inOrder.verify(netd, never()).ipfwdRemoveInterfaceForward(any(), any())
+ }
+ }
+
+ @Test
+ fun testUnregisterUpstreamAfterReplacement_neverReplaced() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ // Set up a local agent that should forward its traffic to the best wifi upstream.
+ val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ .build()),
+ score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+ .build())
+ )
+ localAgent.connect()
+
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val wifiAgent = Agent(lp = lp("wifi0"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ wifiAgent.connect()
+
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+
+ clearInvocations(netd)
+ wifiAgent.unregisterAfterReplacement(TIMEOUT_MS.toInt())
+ waitForIdle()
+ verify(netd).networkDestroy(wifiAgent.network.netId)
+ verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi0")
+
+ cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+ cb.expect<Lost> { it.network == wifiAgent.network }
+ }
+
+ @Test
+ fun testUnregisterLocalAgentAfterReplacement() {
+ deps.setBuildSdk(VERSION_V)
+
+ val localCb = TestableNetworkCallback()
+ cm.requestNetwork(NetworkRequest.Builder().clearCapabilities()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ localCb)
+
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ val localNc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK)
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ .build())
+ val localScore = FromS(NetworkScore.Builder().build())
+
+ // Set up a local agent that should forward its traffic to the best wifi upstream.
+ val localAgent = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
+ localAgent.connect()
+
+ localCb.expectAvailableCallbacks(localAgent.network, validated = false)
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ wifiAgent.connect()
+
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ listOf(cb, localCb).forEach {
+ it.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgent.network
+ }
+ }
+
+ verify(netd).ipfwdAddInterfaceForward("local0", "wifi0")
+
+ localAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+ val localAgent2 = Agent(nc = localNc, lp = lp("local0"), lnc = lnc, score = localScore)
+ localAgent2.connect()
+
+ localCb.expectAvailableCallbacks(localAgent2.network,
+ validated = false, upstream = wifiAgent.network)
+ cb.expectAvailableCallbacks(localAgent2.network,
+ validated = false, upstream = wifiAgent.network)
+ cb.expect<Lost> { it.network == localAgent.network }
+ }
+
+ @Test
+ fun testDestroyedNetworkAsSelectedUpstream() {
+ deps.setBuildSdk(VERSION_V)
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder().clearCapabilities().build(), cb)
+
+ val wifiAgent = Agent(lp = lp("wifi0"), nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ wifiAgent.connect()
+ cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+
+ // Unregister wifi pending replacement, then set up a local agent that would have
+ // this network as its upstream.
+ wifiAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+ val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ .build()),
+ score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+ .build())
+ )
+
+ // Connect the local agent. The zombie wifi is its upstream, but the stack doesn't
+ // tell netd to add the forward since the wifi0 interface has gone.
+ localAgent.connect()
+ cb.expectAvailableCallbacks(localAgent.network,
+ validated = false, upstream = wifiAgent.network)
+
+ verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+
+ // Disconnect wifi without a replacement. Expect an update with upstream null.
+ wifiAgent.disconnect()
+ verify(netd, never()).ipfwdAddInterfaceForward("local0", "wifi0")
+ cb.expect<LocalInfoChanged> { it.info.upstreamNetwork == null }
+ }
+
+ @Test
+ fun testForwardingRules() {
+ deps.setBuildSdk(VERSION_V)
+ // Set up a local agent that should forward its traffic to the best DUN upstream.
+ val lnc = FromS(LocalNetworkConfig.Builder()
+ .setUpstreamSelector(NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_DUN)
+ .build())
+ .build())
+ val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = lnc,
+ score = FromS(NetworkScore.Builder()
+ .setKeepConnectedReason(KEEP_CONNECTED_LOCAL_NETWORK)
+ .build())
+ )
+ localAgent.connect()
+
+ val wifiAgent = Agent(score = keepScore(), lp = lp("wifi0"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET))
+ val cellAgentDun = Agent(score = keepScore(), lp = lp("cell0"),
+ nc = nc(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+ val wifiAgentDun = Agent(score = keepScore(), lp = lp("wifi1"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+
+ val cb = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ cb)
+ cb.expectAvailableCallbacks(localAgent.network, validated = false)
+
+ val inOrder = inOrder(netd)
+ inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+ cb.assertNoCallback()
+
+ wifiAgent.connect()
+ inOrder.verify(netd, never()).ipfwdAddInterfaceForward(any(), any())
+ cb.assertNoCallback()
+
+ cellAgentDun.connect()
+ inOrder.verify(netd).ipfwdEnableForwarding(any())
+ inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "cell0")
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == cellAgentDun.network
+ }
+
+ wifiAgentDun.connect()
+ inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
+ inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi1")
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgentDun.network
+ }
+
+ // Make sure sending the same config again doesn't do anything
+ repeat(5) {
+ localAgent.sendLocalNetworkConfig(lnc.value)
+ }
+ inOrder.verifyNoMoreInteractions()
+
+ wifiAgentDun.disconnect()
+ cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == cellAgentDun.network
+ }
+ inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi1")
+ // This can take a little bit of time because it needs to wait for the rematch
+ inOrder.verify(netd, timeout(MEDIUM_TIMEOUT_MS)).ipfwdAddInterfaceForward("local0", "cell0")
+
+ cellAgentDun.disconnect()
+ inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "cell0")
+ inOrder.verify(netd).ipfwdDisableForwarding(any())
+ cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+ val wifiAgentDun2 = Agent(score = keepScore(), lp = lp("wifi2"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+ wifiAgentDun2.connect()
+ inOrder.verify(netd).ipfwdEnableForwarding(any())
+ inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi2")
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgentDun2.network
+ }
+
+ wifiAgentDun2.disconnect()
+ inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi2")
+ inOrder.verify(netd).ipfwdDisableForwarding(any())
+ cb.expect<LocalInfoChanged>(localAgent.network) { it.info.upstreamNetwork == null }
+
+ val wifiAgentDun3 = Agent(score = keepScore(), lp = lp("wifi3"),
+ nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN))
+ wifiAgentDun3.connect()
+ inOrder.verify(netd).ipfwdEnableForwarding(any())
+ inOrder.verify(netd).ipfwdAddInterfaceForward("local0", "wifi3")
+ cb.expect<LocalInfoChanged>(localAgent.network) {
+ it.info.upstreamNetwork == wifiAgentDun3.network
+ }
+
+ localAgent.disconnect()
+ inOrder.verify(netd).ipfwdRemoveInterfaceForward("local0", "wifi3")
+ inOrder.verify(netd).ipfwdDisableForwarding(any())
+ cb.expect<Lost>(localAgent.network)
+ cb.assertNoCallback()
+ }
+
+ @Test
+ fun testLocalNetworkUnwanted_withUpstream() {
+ doTestLocalNetworkUnwanted(true)
+ }
+
+ @Test
+ fun testLocalNetworkUnwanted_withoutUpstream() {
+ doTestLocalNetworkUnwanted(false)
+ }
+
+ fun doTestLocalNetworkUnwanted(haveUpstream: Boolean) {
+ deps.setBuildSdk(VERSION_V)
+
+ val nr = NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build()
+ val requestCb = TestableNetworkCallback()
+ cm.requestNetwork(nr, requestCb)
+ val listenCb = TestableNetworkCallback()
+ cm.registerNetworkCallback(nr, listenCb)
+
+ val upstream = if (haveUpstream) {
+ Agent(score = keepScore(), lp = lp("wifi0"),
+ nc = nc(TRANSPORT_WIFI)).also { it.connect() }
+ } else {
+ null
+ }
+
+ // Set up a local agent.
+ val lnc = FromS(LocalNetworkConfig.Builder().apply {
+ if (haveUpstream) {
+ setUpstreamSelector(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build())
+ }
+ }.build())
+ val localAgent = Agent(nc = nc(TRANSPORT_THREAD, NET_CAPABILITY_LOCAL_NETWORK),
+ lp = lp("local0"),
+ lnc = lnc,
+ score = FromS(NetworkScore.Builder().build())
+ )
+ localAgent.connect()
+
+ requestCb.expectAvailableCallbacks(localAgent.network,
+ validated = false, upstream = upstream?.network)
+ listenCb.expectAvailableCallbacks(localAgent.network,
+ validated = false, upstream = upstream?.network)
+
+ cm.unregisterNetworkCallback(requestCb)
+
+ listenCb.expect<Lost>()
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
new file mode 100644
index 0000000..526ec9d
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkActivityTest.kt
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE
+import android.net.ConnectivityManager.EXTRA_DEVICE_TYPE
+import android.net.ConnectivityManager.EXTRA_IS_ACTIVE
+import android.net.ConnectivityManager.EXTRA_REALTIME_NS
+import android.net.LinkProperties
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.os.Build
+import android.os.ConditionVariable
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH
+import android.telephony.DataConnectionRealTimeInfo.DC_POWER_STATE_LOW
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener
+import com.android.server.CSTest.CSContext
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertNotNull
+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.eq
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+private const val DATA_CELL_IFNAME = "rmnet_data"
+private const val IMS_CELL_IFNAME = "rmnet_ims"
+private const val WIFI_IFNAME = "wlan0"
+private const val TIMESTAMP = 1234L
+private const val NETWORK_ACTIVITY_NO_UID = -1
+private const val PACKAGE_UID = 123
+private const val TIMEOUT_MS = 250L
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class CSNetworkActivityTest : CSTest() {
+
+ private fun getRegisteredNetdUnsolicitedEventListener(): BaseNetdUnsolicitedEventListener {
+ val captor = ArgumentCaptor.forClass(BaseNetdUnsolicitedEventListener::class.java)
+ verify(netd).registerUnsolicitedEventListener(captor.capture())
+ return captor.value
+ }
+
+ @Test
+ fun testInterfaceClassActivityChanged_NonDefaultNetwork() {
+ val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+ val batteryStatsInorder = inOrder(batteryStats)
+
+ val cellNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val cellCb = TestableNetworkCallback()
+ // Request cell network to keep cell network up
+ cm.requestNetwork(cellNr, cellCb)
+
+ val defaultCb = TestableNetworkCallback()
+ cm.registerDefaultNetworkCallback(defaultCb)
+
+ val cellNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val cellLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ // Connect Cellular network
+ val cellAgent = Agent(nc = cellNc, lp = cellLp)
+ cellAgent.connect()
+ defaultCb.expectAvailableCallbacks(cellAgent.network, validated = false)
+
+ val wifiNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+ val wifiLp = LinkProperties().apply {
+ interfaceName = WIFI_IFNAME
+ }
+ // Connect Wi-Fi network, Wi-Fi network should be the default network.
+ val wifiAgent = Agent(nc = wifiNc, lp = wifiLp)
+ wifiAgent.connect()
+ defaultCb.expectAvailableCallbacks(wifiAgent.network, validated = false)
+ batteryStatsInorder.verify(batteryStats).noteWifiRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ val onNetworkActiveCv = ConditionVariable()
+ val listener = ConnectivityManager.OnNetworkActiveListener { onNetworkActiveCv::open }
+ cm.addDefaultNetworkActiveListener(listener)
+
+ // Cellular network (non default network) goes to inactive state.
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ cellAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ // Non-default network activity change does not change default network activity
+ // But cellular radio power state is updated
+ assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+ context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+ assertTrue(cm.isDefaultNetworkActive)
+ batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_LOW),
+ anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ // Cellular network (non default network) goes to active state.
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ cellAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ // Non-default network activity change does not change default network activity
+ // But cellular radio power state is updated
+ assertFalse(onNetworkActiveCv.block(TIMEOUT_MS))
+ context.expectNoDataActivityBroadcast(0 /* timeoutMs */)
+ assertTrue(cm.isDefaultNetworkActive)
+ batteryStatsInorder.verify(batteryStats).noteMobileRadioPowerState(eq(DC_POWER_STATE_HIGH),
+ anyLong() /* timestampNs */, eq(PACKAGE_UID))
+
+ cm.unregisterNetworkCallback(cellCb)
+ cm.unregisterNetworkCallback(defaultCb)
+ cm.removeDefaultNetworkActiveListener(listener)
+ }
+
+ @Test
+ fun testDataActivityTracking_MultiCellNetwork() {
+ val netdUnsolicitedEventListener = getRegisteredNetdUnsolicitedEventListener()
+ val batteryStatsInorder = inOrder(batteryStats)
+
+ val dataNetworkNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .build()
+ val dataNetworkNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val dataNetworkLp = LinkProperties().apply {
+ interfaceName = DATA_CELL_IFNAME
+ }
+ val dataNetworkCb = TestableNetworkCallback()
+ cm.requestNetwork(dataNetworkNr, dataNetworkCb)
+ val dataNetworkAgent = Agent(nc = dataNetworkNc, lp = dataNetworkLp)
+ val dataNetworkNetId = dataNetworkAgent.network.netId.toString()
+
+ val imsNetworkNc = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .build()
+ val imsNetworkNr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .build()
+ val imsNetworkLp = LinkProperties().apply {
+ interfaceName = IMS_CELL_IFNAME
+ }
+ val imsNetworkCb = TestableNetworkCallback()
+ cm.requestNetwork(imsNetworkNr, imsNetworkCb)
+ val imsNetworkAgent = Agent(nc = imsNetworkNc, lp = imsNetworkLp)
+ val imsNetworkNetId = imsNetworkAgent.network.netId.toString()
+
+ dataNetworkAgent.connect()
+ dataNetworkCb.expectAvailableCallbacks(dataNetworkAgent.network, validated = false)
+
+ imsNetworkAgent.connect()
+ imsNetworkCb.expectAvailableCallbacks(imsNetworkAgent.network, validated = false)
+
+ // Both cell networks have idleTimers
+ verify(netd).idletimerAddInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+ verify(netd).idletimerAddInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+ verify(netd, never()).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(),
+ eq(dataNetworkNetId))
+ verify(netd, never()).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(),
+ eq(imsNetworkNetId))
+
+ // Both cell networks go to inactive state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+
+ // Data cell network goes to active state. This should update the cellular radio power state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+ eq(DC_POWER_STATE_HIGH), anyLong() /* timestampNs */, eq(PACKAGE_UID))
+ // Ims cell network goes to active state. But this should not update the cellular radio
+ // power state since cellular radio power state is already high
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(true /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, PACKAGE_UID)
+ waitForIdle()
+ batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+ anyLong() /* timestampNs */, anyInt())
+
+ // Data cell network goes to inactive state. But this should not update the cellular radio
+ // power state ims cell network is still active state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ dataNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ waitForIdle()
+ batteryStatsInorder.verify(batteryStats, never()).noteMobileRadioPowerState(anyInt(),
+ anyLong() /* timestampNs */, anyInt())
+
+ // Ims cell network goes to inactive state.
+ // This should update the cellular radio power state
+ netdUnsolicitedEventListener.onInterfaceClassActivityChanged(false /* isActive */,
+ imsNetworkAgent.network.netId, TIMESTAMP, NETWORK_ACTIVITY_NO_UID)
+ batteryStatsInorder.verify(batteryStats, timeout(TIMEOUT_MS)).noteMobileRadioPowerState(
+ eq(DC_POWER_STATE_LOW), anyLong() /* timestampNs */, eq(NETWORK_ACTIVITY_NO_UID))
+
+ dataNetworkAgent.disconnect()
+ dataNetworkCb.expect<Lost>(dataNetworkAgent.network)
+ verify(netd).idletimerRemoveInterface(eq(DATA_CELL_IFNAME), anyInt(), eq(dataNetworkNetId))
+
+ imsNetworkAgent.disconnect()
+ imsNetworkCb.expect<Lost>(imsNetworkAgent.network)
+ verify(netd).idletimerRemoveInterface(eq(IMS_CELL_IFNAME), anyInt(), eq(imsNetworkNetId))
+
+ cm.unregisterNetworkCallback(dataNetworkCb)
+ cm.unregisterNetworkCallback(imsNetworkCb)
+ }
+}
+
+internal fun CSContext.expectDataActivityBroadcast(
+ deviceType: Int,
+ isActive: Boolean,
+ tsNanos: Long
+) {
+ assertNotNull(orderedBroadcastAsUserHistory.poll(BROADCAST_TIMEOUT_MS) {
+ intent -> intent.action.equals(ACTION_DATA_ACTIVITY_CHANGE) &&
+ intent.getIntExtra(EXTRA_DEVICE_TYPE, -1) == deviceType &&
+ intent.getBooleanExtra(EXTRA_IS_ACTIVE, !isActive) == isActive &&
+ intent.getLongExtra(EXTRA_REALTIME_NS, -1) == tsNanos
+ })
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index a5a1aeb..d41c742 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.server
import android.content.Context
@@ -5,6 +21,7 @@
import android.net.INetworkMonitor
import android.net.INetworkMonitorCallbacks
import android.net.LinkProperties
+import android.net.LocalNetworkConfig
import android.net.Network
import android.net.NetworkAgent
import android.net.NetworkAgentConfig
@@ -17,8 +34,8 @@
import android.net.NetworkTestResultParcelable
import android.net.networkstack.NetworkStackClientBase
import android.os.HandlerThread
-import com.android.modules.utils.build.SdkLevel
import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
@@ -31,6 +48,8 @@
import kotlin.test.assertEquals
import kotlin.test.fail
+const val SHORT_TIMEOUT_MS = 200L
+
private inline fun <reified T> ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java)
private val agentCounter = AtomicInteger(1)
@@ -44,11 +63,13 @@
*/
class CSAgentWrapper(
val context: Context,
+ val deps: ConnectivityService.Dependencies,
csHandlerThread: HandlerThread,
networkStack: NetworkStackClientBase,
nac: NetworkAgentConfig,
val nc: NetworkCapabilities,
val lp: LinkProperties,
+ val lnc: FromS<LocalNetworkConfig>?,
val score: FromS<NetworkScore>,
val provider: NetworkProvider?
) : TestableNetworkCallback.HasNetwork {
@@ -78,9 +99,9 @@
nmCbCaptor.capture())
// Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
- if (SdkLevel.isAtLeastS()) {
+ if (deps.isAtLeastS()) {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
- nc, lp, score.value, nac, provider) {}
+ nc, lp, lnc?.value, score.value, nac, provider) {}
} else {
agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
nc, lp, 50 /* score */, nac, provider) {}
@@ -92,7 +113,7 @@
}
private fun onValidationRequested() {
- if (SdkLevel.isAtLeastT()) {
+ if (deps.isAtLeastT()) {
verify(networkMonitor).notifyNetworkConnectedParcel(any())
} else {
verify(networkMonitor).notifyNetworkConnected(any(), any())
@@ -109,9 +130,10 @@
fun connect() {
val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
- val request = NetworkRequest.Builder().clearCapabilities()
- .addTransportType(nc.transportTypes[0])
- .build()
+ val request = NetworkRequest.Builder().apply {
+ clearCapabilities()
+ if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ }.build()
val cb = TestableNetworkCallback()
mgr.registerNetworkCallback(request, cb)
agent.markConnected()
@@ -133,6 +155,20 @@
}
fun disconnect() {
+ val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val request = NetworkRequest.Builder().apply {
+ clearCapabilities()
+ if (nc.transportTypes.isNotEmpty()) addTransportType(nc.transportTypes[0])
+ }.build()
+ val cb = TestableNetworkCallback(timeoutMs = SHORT_TIMEOUT_MS)
+ mgr.registerNetworkCallback(request, cb)
+ cb.eventuallyExpect<Available> { it.network == agent.network }
agent.unregister()
+ cb.eventuallyExpect<Lost> { it.network == agent.network }
}
+
+ fun unregisterAfterReplacement(timeoutMs: Int) = agent.unregisterAfterReplacement(timeoutMs)
+
+ fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
+ fun sendNetworkCapabilities(nc: NetworkCapabilities) = agent.sendNetworkCapabilities(nc)
}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index b11878d..f4c62c5 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.server
import android.content.BroadcastReceiver
@@ -10,21 +26,24 @@
import android.net.ConnectivityManager
import android.net.INetd
import android.net.InetAddresses
-import android.net.IpPrefix
-import android.net.LinkAddress
import android.net.LinkProperties
+import android.net.LocalNetworkConfig
import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
-import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkCapabilities.TRANSPORT_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
import android.net.NetworkPolicyManager
import android.net.NetworkProvider
import android.net.NetworkScore
import android.net.PacProxyManager
-import android.net.RouteInfo
import android.net.networkstack.NetworkStackClientBase
import android.os.BatteryStatsManager
+import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.os.UserHandle
@@ -36,6 +55,7 @@
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
import com.android.modules.utils.build.SdkLevel
+import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
import com.android.server.connectivity.CarrierPrivilegeAuthenticator
@@ -46,20 +66,41 @@
import com.android.server.connectivity.ProxyTracker
import com.android.testutils.visibleOnHandlerThread
import com.android.testutils.waitForIdle
+import java.util.concurrent.Executors
+import kotlin.test.assertNull
+import kotlin.test.fail
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-import java.util.concurrent.Executors
-import kotlin.test.fail
internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val BROADCAST_TIMEOUT_MS = 3_000L
internal const val TEST_PACKAGE_NAME = "com.android.test.package"
internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
open class FromS<Type>(val value: Type)
+internal const val VERSION_UNMOCKED = -1
+internal const val VERSION_R = 1
+internal const val VERSION_S = 2
+internal const val VERSION_T = 3
+internal const val VERSION_U = 4
+internal const val VERSION_V = 5
+internal const val VERSION_MAX = VERSION_V
+
+private fun NetworkCapabilities.getLegacyType() =
+ when (transportTypes.getOrElse(0) { TRANSPORT_WIFI }) {
+ TRANSPORT_BLUETOOTH -> ConnectivityManager.TYPE_BLUETOOTH
+ TRANSPORT_CELLULAR -> ConnectivityManager.TYPE_MOBILE
+ TRANSPORT_ETHERNET -> ConnectivityManager.TYPE_ETHERNET
+ TRANSPORT_TEST -> ConnectivityManager.TYPE_TEST
+ TRANSPORT_VPN -> ConnectivityManager.TYPE_VPN
+ TRANSPORT_WIFI -> ConnectivityManager.TYPE_WIFI
+ else -> ConnectivityManager.TYPE_NONE
+ }
+
/**
* Base class for tests testing ConnectivityService and its satellites.
*
@@ -90,6 +131,7 @@
it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
+ it[ConnectivityService.ALLOW_SYSUI_CONNECTIVITY_REPORTS] = true
}
fun enableFeature(f: String) = enabledFeatures.set(f, true)
fun disableFeature(f: String) = enabledFeatures.set(f, false)
@@ -108,21 +150,23 @@
val networkStack = mock<NetworkStackClientBase>()
val csHandlerThread = HandlerThread("CSTestHandler")
val sysResources = mock<Resources>().also { initMockedResources(it) }
- val packageManager = makeMockPackageManager()
+ val packageManager = makeMockPackageManager(instrumentationContext)
val connResources = makeMockConnResources(sysResources, packageManager)
+ val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
- val batteryManager = BatteryStatsManager(mock<IBatteryStats>())
+ val batteryStats = mock<IBatteryStats>()
+ val batteryManager = BatteryStatsManager(batteryStats)
val telephonyManager = mock<TelephonyManager>().also {
doReturn(true).`when`(it).isDataCapable()
}
val deps = CSDeps()
- val service = makeConnectivityService(context, deps).also { it.systemReadyInternal() }
+ val service = makeConnectivityService(context, netd, deps).also { it.systemReadyInternal() }
val cm = ConnectivityManager(context, service)
val csHandler = Handler(csHandlerThread.looper)
@@ -132,7 +176,7 @@
override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
override fun getNetworkStack() = this@CSTest.networkStack
- override fun makeHandlerThread() = csHandlerThread
+ override fun makeHandlerThread(tag: String) = csHandlerThread
override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker
override fun makeCarrierPrivilegeAuthenticator(
@@ -157,6 +201,8 @@
// checking permissions.
override fun isFeatureEnabled(context: Context?, name: String?) =
enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+ override fun isFeatureNotChickenedOut(context: Context?, name: String?) =
+ enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
// Mocked change IDs
private val enabledChangeIds = ArraySet<Long>()
@@ -176,6 +222,24 @@
changeId in enabledChangeIds
override fun isChangeEnabled(changeId: Long, uid: Int) =
changeId in enabledChangeIds
+
+ // In AOSP, build version codes can't always distinguish between some versions (e.g. at the
+ // time of this writing U == V). Define custom ones.
+ private var sdkLevel = VERSION_UNMOCKED
+ private val isSdkUnmocked get() = sdkLevel == VERSION_UNMOCKED
+
+ fun setBuildSdk(sdkLevel: Int) {
+ require(sdkLevel <= VERSION_MAX) {
+ "setBuildSdk must not be called with Build.VERSION constants but " +
+ "CsTest.VERSION_* constants"
+ }
+ visibleOnHandlerThread(csHandler) { this.sdkLevel = sdkLevel }
+ }
+
+ override fun isAtLeastS() = if (isSdkUnmocked) super.isAtLeastS() else sdkLevel >= VERSION_S
+ override fun isAtLeastT() = if (isSdkUnmocked) super.isAtLeastT() else sdkLevel >= VERSION_T
+ override fun isAtLeastU() = if (isSdkUnmocked) super.isAtLeastU() else sdkLevel >= VERSION_U
+ override fun isAtLeastV() = if (isSdkUnmocked) super.isAtLeastV() else sdkLevel >= VERSION_V
}
inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
@@ -226,34 +290,42 @@
Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
else -> super.getSystemService(serviceName)
}
+
+ internal val orderedBroadcastAsUserHistory = ArrayTrackRecord<Intent>().newReadHead()
+
+ fun expectNoDataActivityBroadcast(timeoutMs: Int) {
+ assertNull(orderedBroadcastAsUserHistory.poll(
+ timeoutMs.toLong()) { intent -> true })
+ }
+
+ override fun sendOrderedBroadcastAsUser(
+ intent: Intent,
+ user: UserHandle,
+ receiverPermission: String?,
+ resultReceiver: BroadcastReceiver?,
+ scheduler: Handler?,
+ initialCode: Int,
+ initialData: String?,
+ initialExtras: Bundle?
+ ) {
+ orderedBroadcastAsUserHistory.add(intent)
+ }
}
// Utility methods for subclasses to use
fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
- private fun emptyAgentConfig() = NetworkAgentConfig.Builder().build()
- private fun defaultNc() = NetworkCapabilities.Builder()
- // Add sensible defaults for agents that don't want to care
- .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
- .addCapability(NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
- .build()
- private fun defaultScore() = FromS<NetworkScore>(NetworkScore.Builder().build())
- private fun defaultLp() = LinkProperties().apply {
- addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
- addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
- }
-
// Network agents. See CSAgentWrapper. This class contains utility methods to simplify
// creation.
fun Agent(
- nac: NetworkAgentConfig = emptyAgentConfig(),
nc: NetworkCapabilities = defaultNc(),
+ nac: NetworkAgentConfig = emptyAgentConfig(nc.getLegacyType()),
lp: LinkProperties = defaultLp(),
+ lnc: FromS<LocalNetworkConfig>? = null,
score: FromS<NetworkScore> = defaultScore(),
provider: NetworkProvider? = null
- ) = CSAgentWrapper(context, csHandlerThread, networkStack, nac, nc, lp, score, provider)
-
+ ) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
+ nac, nc, lp, lnc, score, provider)
fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
val nc = NetworkCapabilities.Builder().apply {
transports.forEach {
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
index b8f2151..c1828b2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
@file:JvmName("CsTestHelpers")
package com.android.server
@@ -14,7 +30,15 @@
import android.content.res.Resources
import android.net.IDnsResolver
import android.net.INetd
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkScore
+import android.net.RouteInfo
import android.net.metrics.IpConnectivityLog
+import android.os.Binder
import android.os.Handler
import android.os.HandlerThread
import android.os.SystemClock
@@ -31,19 +55,38 @@
import com.android.server.connectivity.ConnectivityResources
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.argThat
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito
import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
import kotlin.test.fail
internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
internal inline fun <reified T> any() = any(T::class.java)
+internal fun emptyAgentConfig(legacyType: Int) = NetworkAgentConfig.Builder()
+ .setLegacyType(legacyType)
+ .build()
+
+internal fun defaultNc() = NetworkCapabilities.Builder()
+ // Add sensible defaults for agents that don't want to care
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build()
+
+internal fun defaultScore() = FromS(NetworkScore.Builder().build())
+
+internal fun defaultLp() = LinkProperties().apply {
+ addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+ addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+}
+
internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply {
addProvider(Settings.AUTHORITY, FakeSettingsProvider())
}
@@ -59,9 +102,22 @@
}
}
-internal fun makeMockPackageManager() = mock<PackageManager>().also { pm ->
+internal fun makeMockPackageManager(realContext: Context) = mock<PackageManager>().also { pm ->
val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
+ val myPackageName = realContext.packageName
+ val myPackageInfo = realContext.packageManager.getPackageInfo(myPackageName,
+ PackageManager.GET_PERMISSIONS)
+ // Very high version code so that the checks for the module version will always
+ // say that it is recent enough. This is the most sensible default, but if some
+ // test needs to test with different version codes they can re-mock this with a
+ // different value.
+ myPackageInfo.longVersionCode = 9999999L
+ doReturn(arrayOf(myPackageName)).`when`(pm).getPackagesForUid(Binder.getCallingUid())
+ doReturn(myPackageInfo).`when`(pm).getPackageInfoAsUser(
+ eq(myPackageName), anyInt(), eq(UserHandle.getCallingUserId()))
+ doReturn(listOf(myPackageInfo)).`when`(pm)
+ .getInstalledPackagesAsUser(eq(PackageManager.GET_PERMISSIONS), anyInt())
}
internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock<Context>().let {
@@ -129,12 +185,13 @@
private val TEST_LINGER_DELAY_MS = 400
private val TEST_NASCENT_DELAY_MS = 300
-internal fun makeConnectivityService(context: Context, deps: Dependencies) = ConnectivityService(
- context,
- mock<IDnsResolver>(),
- mock<IpConnectivityLog>(),
- mock<INetd>(),
- deps).also {
- it.mLingerDelayMs = TEST_LINGER_DELAY_MS
- it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
-}
+internal fun makeConnectivityService(context: Context, netd: INetd, deps: Dependencies) =
+ ConnectivityService(
+ context,
+ mock<IDnsResolver>(),
+ mock<IpConnectivityLog>(),
+ netd,
+ deps).also {
+ it.mLingerDelayMs = TEST_LINGER_DELAY_MS
+ it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
+ }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
new file mode 100644
index 0000000..9f2d4d3
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/NetworkStatsEventLoggerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.util.IndentingPrintWriter
+import com.android.server.net.NetworkStatsEventLogger.MAX_POLL_REASON
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_DUMPSYS
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_FORCE_UPDATE
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_GLOBAL_ALERT
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_NETWORK_STATUS_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_OPEN_SESSION
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_PERIODIC
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED
+import com.android.server.net.NetworkStatsEventLogger.POLL_REASON_REG_CALLBACK
+import com.android.server.net.NetworkStatsEventLogger.PollEvent
+import com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.StringWriter
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+const val TEST_PERSIST_FLAG = 0x101
+
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkStatsEventLoggerTest {
+ val logger = NetworkStatsEventLogger()
+ val stringWriter = TestStringWriter()
+ val pw = IndentingPrintWriter(stringWriter)
+
+ @Test
+ fun testDump_invalid() {
+ // Verify it won't crash.
+ logger.dump(pw)
+ // Clear output buffer.
+ stringWriter.getOutputAndClear()
+
+ // Verify log invalid event throws. And nothing output in the dump.
+ val invalidReasons = listOf(-1, MAX_POLL_REASON + 1)
+ invalidReasons.forEach {
+ assertFailsWith<IllegalArgumentException> {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+ }
+ logger.dumpRecentPollEvents(pw)
+ val output = stringWriter.getOutputAndClear()
+ assertStringNotContains(output, pollReasonNameOf(it))
+ }
+ }
+
+ @Test
+ fun testDump_valid() {
+ // Choose arbitrary set of reasons for testing.
+ val loggedReasons = listOf(
+ POLL_REASON_GLOBAL_ALERT,
+ POLL_REASON_FORCE_UPDATE,
+ POLL_REASON_DUMPSYS,
+ POLL_REASON_PERIODIC,
+ POLL_REASON_RAT_CHANGED
+ )
+ val nonLoggedReasons = listOf(
+ POLL_REASON_NETWORK_STATUS_CHANGED,
+ POLL_REASON_OPEN_SESSION,
+ POLL_REASON_REG_CALLBACK)
+
+ // Add some valid records.
+ loggedReasons.forEach {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(it))
+ }
+
+ // Collect dumps.
+ logger.dumpRecentPollEvents(pw)
+ val outputRecentEvents = stringWriter.getOutputAndClear()
+ logger.dumpPollCountsPerReason(pw)
+ val outputCountsPerReason = stringWriter.getOutputAndClear()
+
+ // Verify the output contains at least necessary information.
+ loggedReasons.forEach {
+ // Verify all events are shown in the recent event dump.
+ val eventString = PollEvent(it).toString()
+ assertStringContains(outputRecentEvents, TEST_PERSIST_FLAG.toString())
+ assertStringContains(eventString, pollReasonNameOf(it))
+ assertStringContains(outputRecentEvents, eventString)
+ // Verify counts are 1 for each reason.
+ assertCountForReason(outputCountsPerReason, it, 1)
+ }
+
+ // Verify the output remains untouched for other reasons.
+ nonLoggedReasons.forEach {
+ assertStringNotContains(outputRecentEvents, PollEvent(it).toString())
+ assertCountForReason(outputCountsPerReason, it, 0)
+ }
+ }
+
+ @Test
+ fun testDump_maxEventLogs() {
+ // Choose arbitrary reason.
+ val reasonToBeTested = POLL_REASON_PERIODIC
+ val repeatCount = NetworkStatsEventLogger.MAX_EVENTS_LOGS * 2
+
+ // Collect baseline.
+ logger.dumpRecentPollEvents(pw)
+ val lineCountBaseLine = getLineCount(stringWriter.getOutputAndClear())
+
+ repeat(repeatCount) {
+ logger.logPollEvent(TEST_PERSIST_FLAG, PollEvent(reasonToBeTested))
+ }
+
+ // Collect dump.
+ logger.dumpRecentPollEvents(pw)
+ val lineCountAfterTest = getLineCount(stringWriter.getOutputAndClear())
+
+ // Verify line count increment is limited.
+ assertEquals(
+ NetworkStatsEventLogger.MAX_EVENTS_LOGS,
+ lineCountAfterTest - lineCountBaseLine
+ )
+
+ // Verify count per reason increased for the testing reason.
+ logger.dumpPollCountsPerReason(pw)
+ val outputCountsPerReason = stringWriter.getOutputAndClear()
+ for (reason in 0..MAX_POLL_REASON) {
+ assertCountForReason(
+ outputCountsPerReason,
+ reason,
+ if (reason == reasonToBeTested) repeatCount else 0
+ )
+ }
+ }
+
+ private fun getLineCount(multilineString: String) = multilineString.lines().size
+
+ private fun assertStringContains(got: String, want: String) {
+ assertTrue(got.contains(want), "Wanted: $want, but got: $got")
+ }
+
+ private fun assertStringNotContains(got: String, unwant: String) {
+ assertFalse(got.contains(unwant), "Unwanted: $unwant, but got: $got")
+ }
+
+ /**
+ * Assert the reason and the expected count are at the same line.
+ */
+ private fun assertCountForReason(dump: String, reason: Int, expectedCount: Int) {
+ // Matches strings like "GLOBAL_ALERT: 50" but not too strict since the format might change.
+ val regex = Regex(pollReasonNameOf(reason) + "[^0-9]+" + expectedCount)
+ assertEquals(
+ 1,
+ regex.findAll(dump).count(),
+ "Unexpected output: $dump " + " for reason: " + pollReasonNameOf(reason)
+ )
+ }
+
+ class TestStringWriter : StringWriter() {
+ fun getOutputAndClear() = toString().also { buffer.setLength(0) }
+ }
+}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index 292f77e..e62ac74 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -59,6 +59,7 @@
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -124,7 +125,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- mObserverHandlerThread = new HandlerThread("HandlerThread");
+ mObserverHandlerThread = new HandlerThread("NetworkStatsObserversTest");
mObserverHandlerThread.start();
final Looper observerLooper = mObserverHandlerThread.getLooper();
mStatsObservers = new NetworkStatsObservers() {
@@ -139,6 +140,14 @@
mUsageCallback = new TestableUsageCallback(mUsageCallbackBinder);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mObserverHandlerThread != null) {
+ mObserverHandlerThread.quitSafely();
+ mObserverHandlerThread.join();
+ }
+ }
+
@Test
public void testRegister_thresholdTooLow_setsDefaultThreshold() throws Exception {
final long thresholdTooLowBytes = 1L;
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 9453617..7a4dfed 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -64,6 +64,8 @@
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+import static com.android.server.net.NetworkStatsEventLogger.POLL_REASON_RAT_CHANGED;
+import static com.android.server.net.NetworkStatsEventLogger.PollEvent.pollReasonNameOf;
import static com.android.server.net.NetworkStatsService.ACTION_NETWORK_STATS_POLL;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME;
import static com.android.server.net.NetworkStatsService.NETSTATS_IMPORT_FALLBACKS_COUNTER_NAME;
@@ -118,6 +120,7 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.Looper;
import android.os.PowerManager;
import android.os.SimpleClock;
import android.provider.Settings;
@@ -190,6 +193,7 @@
* 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.
*/
+@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
// NetworkStatsService is not updatable before T, so tests do not need to be backwards compatible
@@ -282,6 +286,7 @@
private @Mock PersistentInt mImportLegacyFallbacksCounter;
private @Mock Resources mResources;
private Boolean mIsDebuggable;
+ private HandlerThread mObserverHandlerThread;
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -363,10 +368,23 @@
PowerManager.WakeLock wakeLock =
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
- mHandlerThread = new HandlerThread("HandlerThread");
+ mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
final NetworkStatsService.Dependencies deps = makeDependencies();
+ // Create a separate thread for observers to run on. This thread cannot be the same
+ // as the handler thread, because the observer callback is fired on this thread, and
+ // it should not be blocked by client code. Additionally, creating the observers
+ // object requires a looper, which can only be obtained after a thread has been started.
+ mObserverHandlerThread = new HandlerThread("NetworkStatsServiceTest-ObserversThread");
+ mObserverHandlerThread.start();
+ final Looper observerLooper = mObserverHandlerThread.getLooper();
+ final NetworkStatsObservers statsObservers = new NetworkStatsObservers() {
+ @Override
+ protected Looper getHandlerLooperLocked() {
+ return observerLooper;
+ }
+ };
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
+ mClock, mSettings, mStatsFactory, statsObservers, deps);
mElapsedRealtime = 0L;
@@ -525,6 +543,11 @@
IBpfMap<CookieTagMapKey, CookieTagMapValue> cookieTagMap, Handler handler) {
return mSkDestroyListener;
}
+
+ @Override
+ public boolean supportEventLogger(@NonNull Context cts) {
+ return true;
+ }
};
}
@@ -538,8 +561,14 @@
mSession.close();
mService = null;
- mHandlerThread.quitSafely();
- mHandlerThread.join();
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join();
+ }
+ if (mObserverHandlerThread != null) {
+ mObserverHandlerThread.quitSafely();
+ mObserverHandlerThread.join();
+ }
}
private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {
@@ -2674,4 +2703,14 @@
doReturn(null).when(mBpfInterfaceMapUpdater).getIfNameByIndex(10 /* index */);
doTestDumpIfaceStatsMap("unknown");
}
+
+ // Basic test to ensure event logger dump is called.
+ // Note that tests to ensure detailed correctness is done in the dedicated tests.
+ // See NetworkStatsEventLoggerTest.
+ @Test
+ public void testDumpEventLogger() {
+ setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+ final String dump = getDump();
+ assertDumpContains(dump, pollReasonNameOf(POLL_REASON_RAT_CHANGED));
+ }
}
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index 17a74f6..30aeca5 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -1,9 +1,12 @@
{
- // TODO (b/297729075): graduate this test to presubmit once it meets the SLO requirements.
- // See go/test-mapping-slo-guide
- "postsubmit": [
+ "presubmit": [
{
"name": "CtsThreadNetworkTestCases"
}
+ ],
+ "postsubmit": [
+ {
+ "name": "ThreadNetworkUnitTests"
+ }
]
}
diff --git a/thread/flags/thread_base.aconfig b/thread/flags/thread_base.aconfig
new file mode 100644
index 0000000..bf1f288
--- /dev/null
+++ b/thread/flags/thread_base.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.net.thread.flags"
+
+flag {
+ name: "thread_enabled"
+ namespace: "thread_network"
+ description: "Controls whether the Android Thread feature is enabled"
+ bug: "301473012"
+}
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
new file mode 100644
index 0000000..8bf12a4
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+parcelable ActiveOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/ActiveOperationalDataset.java b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
new file mode 100644
index 0000000..b74a15a
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ActiveOperationalDataset.java
@@ -0,0 +1,1096 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkState;
+import static com.android.net.module.util.HexDump.dumpHexString;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.SystemApi;
+import android.net.IpPrefix;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/**
+ * Data interface for managing a Thread Active Operational Dataset.
+ *
+ * <p>An example usage of creating an Active Operational Dataset with randomized parameters:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = controller.createRandomizedDataset("MyNet");
+ * }</pre>
+ *
+ * <p>or randomized Dataset with customized channel:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset =
+ * new ActiveOperationalDataset.Builder(controller.createRandomizedDataset("MyNet"))
+ * .setChannel(CHANNEL_PAGE_24_GHZ, 17)
+ * .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(Instant.now()))
+ * .build();
+ * }</pre>
+ *
+ * <p>If the Active Operational Dataset is already known as <a
+ * href="https://www.threadgroup.org">Thread TLVs</a>, you can simply use:
+ *
+ * <pre>{@code
+ * ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlvs);
+ * }</pre>
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class ActiveOperationalDataset implements Parcelable {
+ /** The maximum length of the Active Operational Dataset TLV array in bytes. */
+ public static final int LENGTH_MAX_DATASET_TLVS = 254;
+ /** The length of Extended PAN ID in bytes. */
+ public static final int LENGTH_EXTENDED_PAN_ID = 8;
+ /** The minimum length of Network Name as UTF-8 bytes. */
+ public static final int LENGTH_MIN_NETWORK_NAME_BYTES = 1;
+ /** The maximum length of Network Name as UTF-8 bytes. */
+ public static final int LENGTH_MAX_NETWORK_NAME_BYTES = 16;
+ /** The length of Network Key in bytes. */
+ public static final int LENGTH_NETWORK_KEY = 16;
+ /** The length of Mesh-Local Prefix in bits. */
+ public static final int LENGTH_MESH_LOCAL_PREFIX_BITS = 64;
+ /** The length of PSKc in bytes. */
+ public static final int LENGTH_PSKC = 16;
+ /** The 2.4 GHz channel page. */
+ public static final int CHANNEL_PAGE_24_GHZ = 0;
+ /** The minimum 2.4GHz channel. */
+ public static final int CHANNEL_MIN_24_GHZ = 11;
+ /** The maximum 2.4GHz channel. */
+ public static final int CHANNEL_MAX_24_GHZ = 26;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_CHANNEL = 0;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_PAN_ID = 1;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_EXTENDED_PAN_ID = 2;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_NETWORK_NAME = 3;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_PSKC = 4;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_NETWORK_KEY = 5;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_MESH_LOCAL_PREFIX = 7;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_SECURITY_POLICY = 12;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_ACTIVE_TIMESTAMP = 14;
+ /** @hide */
+ @VisibleForTesting public static final int TYPE_CHANNEL_MASK = 53;
+
+ /** @hide */
+ public static final byte MESH_LOCAL_PREFIX_FIRST_BYTE = (byte) 0xfd;
+
+ private static final int LENGTH_CHANNEL = 3;
+ private static final int LENGTH_PAN_ID = 2;
+
+ @NonNull
+ public static final Creator<ActiveOperationalDataset> CREATOR =
+ new Creator<>() {
+ @Override
+ public ActiveOperationalDataset createFromParcel(Parcel in) {
+ return ActiveOperationalDataset.fromThreadTlvs(in.createByteArray());
+ }
+
+ @Override
+ public ActiveOperationalDataset[] newArray(int size) {
+ return new ActiveOperationalDataset[size];
+ }
+ };
+
+ private final OperationalDatasetTimestamp mActiveTimestamp;
+ private final String mNetworkName;
+ private final byte[] mExtendedPanId;
+ private final int mPanId;
+ private final int mChannel;
+ private final int mChannelPage;
+ private final SparseArray<byte[]> mChannelMask;
+ private final byte[] mPskc;
+ private final byte[] mNetworkKey;
+ private final IpPrefix mMeshLocalPrefix;
+ private final SecurityPolicy mSecurityPolicy;
+ private final SparseArray<byte[]> mUnknownTlvs;
+
+ private ActiveOperationalDataset(Builder builder) {
+ this(
+ requireNonNull(builder.mActiveTimestamp),
+ requireNonNull(builder.mNetworkName),
+ requireNonNull(builder.mExtendedPanId),
+ requireNonNull(builder.mPanId),
+ requireNonNull(builder.mChannelPage),
+ requireNonNull(builder.mChannel),
+ requireNonNull(builder.mChannelMask),
+ requireNonNull(builder.mPskc),
+ requireNonNull(builder.mNetworkKey),
+ requireNonNull(builder.mMeshLocalPrefix),
+ requireNonNull(builder.mSecurityPolicy),
+ requireNonNull(builder.mUnknownTlvs));
+ }
+
+ private ActiveOperationalDataset(
+ OperationalDatasetTimestamp activeTimestamp,
+ String networkName,
+ byte[] extendedPanId,
+ int panId,
+ int channelPage,
+ int channel,
+ SparseArray<byte[]> channelMask,
+ byte[] pskc,
+ byte[] networkKey,
+ IpPrefix meshLocalPrefix,
+ SecurityPolicy securityPolicy,
+ SparseArray<byte[]> unknownTlvs) {
+ this.mActiveTimestamp = activeTimestamp;
+ this.mNetworkName = networkName;
+ this.mExtendedPanId = extendedPanId.clone();
+ this.mPanId = panId;
+ this.mChannel = channel;
+ this.mChannelPage = channelPage;
+ this.mChannelMask = deepCloneSparseArray(channelMask);
+ this.mPskc = pskc.clone();
+ this.mNetworkKey = networkKey.clone();
+ this.mMeshLocalPrefix = meshLocalPrefix;
+ this.mSecurityPolicy = securityPolicy;
+ this.mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object from a series of Thread TLVs.
+ *
+ * <p>{@code tlvs} can be obtained from the value of a Thread Active Operational Dataset TLV
+ * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+ *
+ * @param tlvs a series of Thread TLVs which contain the Active Operational Dataset
+ * @return the decoded Active Operational Dataset
+ * @throws IllegalArgumentException if {@code tlvs} is malformed or the length is larger than
+ * {@link LENGTH_MAX_DATASET_TLVS}
+ */
+ @NonNull
+ public static ActiveOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+ requireNonNull(tlvs, "tlvs cannot be null");
+ if (tlvs.length > LENGTH_MAX_DATASET_TLVS) {
+ throw new IllegalArgumentException(
+ String.format(
+ "tlvs length exceeds max length %d (actual is %d)",
+ LENGTH_MAX_DATASET_TLVS, tlvs.length));
+ }
+
+ Builder builder = new Builder();
+ int i = 0;
+ while (i < tlvs.length) {
+ int type = tlvs[i++] & 0xff;
+ if (i >= tlvs.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Found TLV type %d at end of operational dataset with length %d",
+ type, tlvs.length));
+ }
+
+ int length = tlvs[i++] & 0xff;
+ if (i + length > tlvs.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Found TLV type %d with length %d which exceeds the remaining data"
+ + " in the operational dataset with length %d",
+ type, length, tlvs.length));
+ }
+
+ initWithTlv(builder, type, Arrays.copyOfRange(tlvs, i, i + length));
+ i += length;
+ }
+ try {
+ return builder.build();
+ } catch (IllegalStateException e) {
+ throw new IllegalArgumentException(
+ "Failed to build the ActiveOperationalDataset object", e);
+ }
+ }
+
+ private static void initWithTlv(Builder builder, int type, byte[] value) {
+ // The max length of the dataset is 254 bytes, so the max length of a single TLV value is
+ // 252 (254 - 1 - 1)
+ if (value.length > LENGTH_MAX_DATASET_TLVS - 2) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Length of TLV %d exceeds %d (actualLength = %d)",
+ (type & 0xff), LENGTH_MAX_DATASET_TLVS - 2, value.length));
+ }
+
+ switch (type) {
+ case TYPE_CHANNEL:
+ checkArgument(
+ value.length == LENGTH_CHANNEL,
+ "Invalid channel (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_CHANNEL);
+ builder.setChannel((value[0] & 0xff), ((value[1] & 0xff) << 8) | (value[2] & 0xff));
+ break;
+ case TYPE_PAN_ID:
+ checkArgument(
+ value.length == LENGTH_PAN_ID,
+ "Invalid PAN ID (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_PAN_ID);
+ builder.setPanId(((value[0] & 0xff) << 8) | (value[1] & 0xff));
+ break;
+ case TYPE_EXTENDED_PAN_ID:
+ builder.setExtendedPanId(value);
+ break;
+ case TYPE_NETWORK_NAME:
+ builder.setNetworkName(new String(value, UTF_8));
+ break;
+ case TYPE_PSKC:
+ builder.setPskc(value);
+ break;
+ case TYPE_NETWORK_KEY:
+ builder.setNetworkKey(value);
+ break;
+ case TYPE_MESH_LOCAL_PREFIX:
+ builder.setMeshLocalPrefix(value);
+ break;
+ case TYPE_SECURITY_POLICY:
+ builder.setSecurityPolicy(SecurityPolicy.fromTlvValue(value));
+ break;
+ case TYPE_ACTIVE_TIMESTAMP:
+ builder.setActiveTimestamp(OperationalDatasetTimestamp.fromTlvValue(value));
+ break;
+ case TYPE_CHANNEL_MASK:
+ builder.setChannelMask(decodeChannelMask(value));
+ break;
+ default:
+ builder.addUnknownTlv(type & 0xff, value);
+ break;
+ }
+ }
+
+ private static SparseArray<byte[]> decodeChannelMask(byte[] tlvValue) {
+ SparseArray<byte[]> channelMask = new SparseArray<>();
+ int i = 0;
+ while (i < tlvValue.length) {
+ int channelPage = tlvValue[i++] & 0xff;
+ if (i >= tlvValue.length) {
+ throw new IllegalArgumentException(
+ "Invalid channel mask - channel mask length is missing");
+ }
+
+ int maskLength = tlvValue[i++] & 0xff;
+ if (i + maskLength > tlvValue.length) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid channel mask - channel mask is incomplete "
+ + "(offset = %d, length = %d, totalLength = %d)",
+ i, maskLength, tlvValue.length));
+ }
+
+ channelMask.put(channelPage, Arrays.copyOfRange(tlvValue, i, i + maskLength));
+ i += maskLength;
+ }
+ return channelMask;
+ }
+
+ private static void encodeChannelMask(
+ SparseArray<byte[]> channelMask, ByteArrayOutputStream outputStream) {
+ ByteArrayOutputStream entryStream = new ByteArrayOutputStream();
+
+ for (int i = 0; i < channelMask.size(); i++) {
+ int key = channelMask.keyAt(i);
+ byte[] value = channelMask.get(key);
+ entryStream.write(key);
+ entryStream.write(value.length);
+ entryStream.write(value, 0, value.length);
+ }
+
+ byte[] entries = entryStream.toByteArray();
+
+ outputStream.write(TYPE_CHANNEL_MASK);
+ outputStream.write(entries.length);
+ outputStream.write(entries, 0, entries.length);
+ }
+
+ private static boolean areByteSparseArraysEqual(
+ @NonNull SparseArray<byte[]> first, @NonNull SparseArray<byte[]> second) {
+ if (first == second) {
+ return true;
+ } else if (first == null || second == null) {
+ return false;
+ } else if (first.size() != second.size()) {
+ return false;
+ } else {
+ for (int i = 0; i < first.size(); i++) {
+ int firstKey = first.keyAt(i);
+ int secondKey = second.keyAt(i);
+ if (firstKey != secondKey) {
+ return false;
+ }
+
+ byte[] firstValue = first.valueAt(i);
+ byte[] secondValue = second.valueAt(i);
+ if (!Arrays.equals(firstValue, secondValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ /** An easy-to-use wrapper of {@link Arrays#deepHashCode}. */
+ private static int deepHashCode(Object... values) {
+ return Arrays.deepHashCode(values);
+ }
+
+ /**
+ * Converts this {@link ActiveOperationalDataset} object to a series of Thread TLVs.
+ *
+ * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition of the Thread TLV format.
+ *
+ * @return a series of Thread TLVs which contain this Active Operational Dataset
+ */
+ @NonNull
+ public byte[] toThreadTlvs() {
+ ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+ dataset.write(TYPE_ACTIVE_TIMESTAMP);
+ byte[] activeTimestampBytes = mActiveTimestamp.toTlvValue();
+ dataset.write(activeTimestampBytes.length);
+ dataset.write(activeTimestampBytes, 0, activeTimestampBytes.length);
+
+ dataset.write(TYPE_NETWORK_NAME);
+ byte[] networkNameBytes = mNetworkName.getBytes(UTF_8);
+ dataset.write(networkNameBytes.length);
+ dataset.write(networkNameBytes, 0, networkNameBytes.length);
+
+ dataset.write(TYPE_EXTENDED_PAN_ID);
+ dataset.write(mExtendedPanId.length);
+ dataset.write(mExtendedPanId, 0, mExtendedPanId.length);
+
+ dataset.write(TYPE_PAN_ID);
+ dataset.write(LENGTH_PAN_ID);
+ dataset.write(mPanId >> 8);
+ dataset.write(mPanId);
+
+ dataset.write(TYPE_CHANNEL);
+ dataset.write(LENGTH_CHANNEL);
+ dataset.write(mChannelPage);
+ dataset.write(mChannel >> 8);
+ dataset.write(mChannel);
+
+ encodeChannelMask(mChannelMask, dataset);
+
+ dataset.write(TYPE_PSKC);
+ dataset.write(mPskc.length);
+ dataset.write(mPskc, 0, mPskc.length);
+
+ dataset.write(TYPE_NETWORK_KEY);
+ dataset.write(mNetworkKey.length);
+ dataset.write(mNetworkKey, 0, mNetworkKey.length);
+
+ dataset.write(TYPE_MESH_LOCAL_PREFIX);
+ dataset.write(mMeshLocalPrefix.getPrefixLength() / 8);
+ dataset.write(mMeshLocalPrefix.getRawAddress(), 0, mMeshLocalPrefix.getPrefixLength() / 8);
+
+ dataset.write(TYPE_SECURITY_POLICY);
+ byte[] securityPolicyBytes = mSecurityPolicy.toTlvValue();
+ dataset.write(securityPolicyBytes.length);
+ dataset.write(securityPolicyBytes, 0, securityPolicyBytes.length);
+
+ for (int i = 0; i < mUnknownTlvs.size(); i++) {
+ byte[] value = mUnknownTlvs.valueAt(i);
+ dataset.write(mUnknownTlvs.keyAt(i));
+ dataset.write(value.length);
+ dataset.write(value, 0, value.length);
+ }
+
+ return dataset.toByteArray();
+ }
+
+ /** Returns the Active Timestamp. */
+ @NonNull
+ public OperationalDatasetTimestamp getActiveTimestamp() {
+ return mActiveTimestamp;
+ }
+
+ /** Returns the Network Name. */
+ @NonNull
+ @Size(min = LENGTH_MIN_NETWORK_NAME_BYTES, max = LENGTH_MAX_NETWORK_NAME_BYTES)
+ public String getNetworkName() {
+ return mNetworkName;
+ }
+
+ /** Returns the Extended PAN ID. */
+ @NonNull
+ @Size(LENGTH_EXTENDED_PAN_ID)
+ public byte[] getExtendedPanId() {
+ return mExtendedPanId.clone();
+ }
+
+ /** Returns the PAN ID. */
+ @IntRange(from = 0, to = 0xfffe)
+ public int getPanId() {
+ return mPanId;
+ }
+
+ /** Returns the Channel. */
+ @IntRange(from = 0, to = 65535)
+ public int getChannel() {
+ return mChannel;
+ }
+
+ /** Returns the Channel Page. */
+ @IntRange(from = 0, to = 255)
+ public int getChannelPage() {
+ return mChannelPage;
+ }
+
+ /**
+ * Returns the Channel masks. For the returned {@link SparseArray}, the key is the Channel Page
+ * and the value is the Channel Mask.
+ */
+ @NonNull
+ @Size(min = 1)
+ public SparseArray<byte[]> getChannelMask() {
+ return deepCloneSparseArray(mChannelMask);
+ }
+
+ private static SparseArray<byte[]> deepCloneSparseArray(SparseArray<byte[]> src) {
+ SparseArray<byte[]> dst = new SparseArray<>(src.size());
+ for (int i = 0; i < src.size(); i++) {
+ dst.put(src.keyAt(i), src.valueAt(i).clone());
+ }
+ return dst;
+ }
+
+ /** Returns the PSKc. */
+ @NonNull
+ @Size(LENGTH_PSKC)
+ public byte[] getPskc() {
+ return mPskc.clone();
+ }
+
+ /** Returns the Network Key. */
+ @NonNull
+ @Size(LENGTH_NETWORK_KEY)
+ public byte[] getNetworkKey() {
+ return mNetworkKey.clone();
+ }
+
+ /**
+ * Returns the Mesh-local Prefix. The length of the returned prefix is always {@link
+ * #LENGTH_MESH_LOCAL_PREFIX_BITS}.
+ */
+ @NonNull
+ public IpPrefix getMeshLocalPrefix() {
+ return mMeshLocalPrefix;
+ }
+
+ /** Returns the Security Policy. */
+ @NonNull
+ public SecurityPolicy getSecurityPolicy() {
+ return mSecurityPolicy;
+ }
+
+ /**
+ * Returns Thread TLVs which are not recognized by this device. The returned {@link SparseArray}
+ * associates TLV values to their keys.
+ *
+ * @hide
+ */
+ @NonNull
+ public SparseArray<byte[]> getUnknownTlvs() {
+ return deepCloneSparseArray(mUnknownTlvs);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(toThreadTlvs());
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ } else if (!(other instanceof ActiveOperationalDataset)) {
+ return false;
+ } else {
+ ActiveOperationalDataset otherDataset = (ActiveOperationalDataset) other;
+ return mActiveTimestamp.equals(otherDataset.mActiveTimestamp)
+ && mNetworkName.equals(otherDataset.mNetworkName)
+ && Arrays.equals(mExtendedPanId, otherDataset.mExtendedPanId)
+ && mPanId == otherDataset.mPanId
+ && mChannelPage == otherDataset.mChannelPage
+ && mChannel == otherDataset.mChannel
+ && areByteSparseArraysEqual(mChannelMask, otherDataset.mChannelMask)
+ && Arrays.equals(mPskc, otherDataset.mPskc)
+ && Arrays.equals(mNetworkKey, otherDataset.mNetworkKey)
+ && mMeshLocalPrefix.equals(otherDataset.mMeshLocalPrefix)
+ && mSecurityPolicy.equals(otherDataset.mSecurityPolicy)
+ && areByteSparseArraysEqual(mUnknownTlvs, otherDataset.mUnknownTlvs);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(
+ mActiveTimestamp,
+ mNetworkName,
+ mExtendedPanId,
+ mPanId,
+ mChannel,
+ mChannelPage,
+ mChannelMask,
+ mPskc,
+ mNetworkKey,
+ mMeshLocalPrefix,
+ mSecurityPolicy);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{networkName=")
+ .append(getNetworkName())
+ .append(", extendedPanId=")
+ .append(dumpHexString(getExtendedPanId()))
+ .append(", panId=")
+ .append(getPanId())
+ .append(", channel=")
+ .append(getChannel())
+ .append(", activeTimestamp=")
+ .append(getActiveTimestamp())
+ .append("}");
+ return sb.toString();
+ }
+
+ static String checkNetworkName(@NonNull String networkName) {
+ requireNonNull(networkName, "networkName cannot be null");
+
+ int nameLength = networkName.getBytes(UTF_8).length;
+ checkArgument(
+ nameLength >= LENGTH_MIN_NETWORK_NAME_BYTES
+ && nameLength <= LENGTH_MAX_NETWORK_NAME_BYTES,
+ "Invalid network name (length = %d, expectedLengthRange = [%d, %d])",
+ nameLength,
+ LENGTH_MIN_NETWORK_NAME_BYTES,
+ LENGTH_MAX_NETWORK_NAME_BYTES);
+ return networkName;
+ }
+
+ /** The builder for creating {@link ActiveOperationalDataset} objects. */
+ public static final class Builder {
+ private OperationalDatasetTimestamp mActiveTimestamp;
+ private String mNetworkName;
+ private byte[] mExtendedPanId;
+ private Integer mPanId;
+ private Integer mChannel;
+ private Integer mChannelPage;
+ private SparseArray<byte[]> mChannelMask;
+ private byte[] mPskc;
+ private byte[] mNetworkKey;
+ private IpPrefix mMeshLocalPrefix;
+ private SecurityPolicy mSecurityPolicy;
+ private SparseArray<byte[]> mUnknownTlvs;
+
+ /**
+ * Creates a {@link Builder} object with values from an {@link ActiveOperationalDataset}
+ * object.
+ */
+ public Builder(@NonNull ActiveOperationalDataset activeOpDataset) {
+ requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+
+ this.mActiveTimestamp = activeOpDataset.mActiveTimestamp;
+ this.mNetworkName = activeOpDataset.mNetworkName;
+ this.mExtendedPanId = activeOpDataset.mExtendedPanId.clone();
+ this.mPanId = activeOpDataset.mPanId;
+ this.mChannel = activeOpDataset.mChannel;
+ this.mChannelPage = activeOpDataset.mChannelPage;
+ this.mChannelMask = deepCloneSparseArray(activeOpDataset.mChannelMask);
+ this.mPskc = activeOpDataset.mPskc.clone();
+ this.mNetworkKey = activeOpDataset.mNetworkKey.clone();
+ this.mMeshLocalPrefix = activeOpDataset.mMeshLocalPrefix;
+ this.mSecurityPolicy = activeOpDataset.mSecurityPolicy;
+ this.mUnknownTlvs = deepCloneSparseArray(activeOpDataset.mUnknownTlvs);
+ }
+
+ /**
+ * Creates an empty {@link Builder} object.
+ *
+ * <p>An empty builder cannot build a new {@link ActiveOperationalDataset} object. The
+ * Active Operational Dataset parameters must be set with setters of this builder.
+ */
+ public Builder() {
+ mChannelMask = new SparseArray<>();
+ mUnknownTlvs = new SparseArray<>();
+ }
+
+ /**
+ * Sets the Active Timestamp.
+ *
+ * @param activeTimestamp Active Timestamp of the Operational Dataset
+ */
+ @NonNull
+ public Builder setActiveTimestamp(@NonNull OperationalDatasetTimestamp activeTimestamp) {
+ requireNonNull(activeTimestamp, "activeTimestamp cannot be null");
+ this.mActiveTimestamp = activeTimestamp;
+ return this;
+ }
+
+ /**
+ * Sets the Network Name.
+ *
+ * @param networkName the name of the Thread network
+ * @throws IllegalArgumentException if length of the UTF-8 representation of {@code
+ * networkName} isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
+ * #LENGTH_MAX_NETWORK_NAME_BYTES}]
+ */
+ @NonNull
+ public Builder setNetworkName(
+ @NonNull
+ @Size(
+ min = LENGTH_MIN_NETWORK_NAME_BYTES,
+ max = LENGTH_MAX_NETWORK_NAME_BYTES)
+ String networkName) {
+ this.mNetworkName = checkNetworkName(networkName);
+ return this;
+ }
+
+ /**
+ * Sets the Extended PAN ID.
+ *
+ * <p>Use with caution. A randomized Extended PAN ID should be used for real Thread
+ * networks. It's discouraged to call this method to override the default value created by
+ * {@link ThreadNetworkController#createRandomizedDataset} in production.
+ *
+ * @throws IllegalArgumentException if length of {@code extendedPanId} is not {@link
+ * #LENGTH_EXTENDED_PAN_ID}.
+ */
+ @NonNull
+ public Builder setExtendedPanId(
+ @NonNull @Size(LENGTH_EXTENDED_PAN_ID) byte[] extendedPanId) {
+ requireNonNull(extendedPanId, "extendedPanId cannot be null");
+ checkArgument(
+ extendedPanId.length == LENGTH_EXTENDED_PAN_ID,
+ "Invalid extended PAN ID (length = %d, expectedLength = %d)",
+ extendedPanId.length,
+ LENGTH_EXTENDED_PAN_ID);
+ this.mExtendedPanId = extendedPanId.clone();
+ return this;
+ }
+
+ /**
+ * Sets the PAN ID.
+ *
+ * @throws IllegalArgumentException if {@code panId} is not in range of 0x0-0xfffe
+ */
+ @NonNull
+ public Builder setPanId(@IntRange(from = 0, to = 0xfffe) int panId) {
+ checkArgument(
+ panId >= 0 && panId <= 0xfffe,
+ "PAN ID exceeds allowed range (panid = %d, allowedRange = [0x0, 0xffff])",
+ panId);
+ this.mPanId = panId;
+ return this;
+ }
+
+ /**
+ * Sets the Channel Page and Channel.
+ *
+ * <p>Channel Pages other than {@link #CHANNEL_PAGE_24_GHZ} are undefined and may lead to
+ * unexpected behavior if it's applied to Thread devices.
+ *
+ * @throws IllegalArgumentException if invalid channel is specified for the {@code
+ * channelPage}
+ */
+ @NonNull
+ public Builder setChannel(
+ @IntRange(from = 0, to = 255) int page,
+ @IntRange(from = 0, to = 65535) int channel) {
+ checkArgument(
+ page >= 0 && page <= 255,
+ "Invalid channel page (page = %d, allowedRange = [0, 255])",
+ page);
+ if (page == CHANNEL_PAGE_24_GHZ) {
+ checkArgument(
+ channel >= CHANNEL_MIN_24_GHZ && channel <= CHANNEL_MAX_24_GHZ,
+ "Invalid channel %d in page %d (allowedChannelRange = [%d, %d])",
+ channel,
+ page,
+ CHANNEL_MIN_24_GHZ,
+ CHANNEL_MAX_24_GHZ);
+ } else {
+ checkArgument(
+ channel >= 0 && channel <= 65535,
+ "Invalid channel %d in page %d "
+ + "(channel = %d, allowedChannelRange = [0, 65535])",
+ channel,
+ page,
+ channel);
+ }
+
+ this.mChannelPage = page;
+ this.mChannel = channel;
+ return this;
+ }
+
+ /**
+ * Sets the Channel Mask.
+ *
+ * @throws IllegalArgumentException if {@code channelMask} is empty
+ */
+ @NonNull
+ public Builder setChannelMask(@NonNull @Size(min = 1) SparseArray<byte[]> channelMask) {
+ requireNonNull(channelMask, "channelMask cannot be null");
+ checkArgument(channelMask.size() > 0, "channelMask is empty");
+ this.mChannelMask = deepCloneSparseArray(channelMask);
+ return this;
+ }
+
+ /**
+ * Sets the PSKc.
+ *
+ * <p>Use with caution. A randomly generated PSKc should be used for real Thread networks.
+ * It's discouraged to call this method to override the default value created by {@link
+ * ThreadNetworkController#createRandomizedDataset} in production.
+ *
+ * @param pskc the key stretched version of the Commissioning Credential for the network
+ * @throws IllegalArgumentException if length of {@code pskc} is not {@link #LENGTH_PSKC}
+ */
+ @NonNull
+ public Builder setPskc(@NonNull @Size(LENGTH_PSKC) byte[] pskc) {
+ requireNonNull(pskc, "pskc cannot be null");
+ checkArgument(
+ pskc.length == LENGTH_PSKC,
+ "Invalid PSKc length (length = %d, expectedLength = %d)",
+ pskc.length,
+ LENGTH_PSKC);
+ this.mPskc = pskc.clone();
+ return this;
+ }
+
+ /**
+ * Sets the Network Key.
+ *
+ * <p>Use with caution, randomly generated Network Key should be used for real Thread
+ * networks. It's discouraged to call this method to override the default value created by
+ * {@link ThreadNetworkController#createRandomizedDataset} in production.
+ *
+ * @param networkKey a 128-bit security key-derivation key for the Thread Network
+ * @throws IllegalArgumentException if length of {@code networkKey} is not {@link
+ * #LENGTH_NETWORK_KEY}
+ */
+ @NonNull
+ public Builder setNetworkKey(@NonNull @Size(LENGTH_NETWORK_KEY) byte[] networkKey) {
+ requireNonNull(networkKey, "networkKey cannot be null");
+ checkArgument(
+ networkKey.length == LENGTH_NETWORK_KEY,
+ "Invalid network key length (length = %d, expectedLength = %d)",
+ networkKey.length,
+ LENGTH_NETWORK_KEY);
+ this.mNetworkKey = networkKey.clone();
+ return this;
+ }
+
+ /**
+ * Sets the Mesh-Local Prefix.
+ *
+ * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
+ * @throws IllegalArgumentException if prefix length of {@code meshLocalPrefix} isn't {@link
+ * #LENGTH_MESH_LOCAL_PREFIX_BITS} or {@code meshLocalPrefix} doesn't start with {@code
+ * 0xfd}
+ */
+ @NonNull
+ public Builder setMeshLocalPrefix(@NonNull IpPrefix meshLocalPrefix) {
+ requireNonNull(meshLocalPrefix, "meshLocalPrefix cannot be null");
+ checkArgument(
+ meshLocalPrefix.getPrefixLength() == LENGTH_MESH_LOCAL_PREFIX_BITS,
+ "Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
+ meshLocalPrefix.getPrefixLength(),
+ LENGTH_MESH_LOCAL_PREFIX_BITS);
+ checkArgument(
+ meshLocalPrefix.getRawAddress()[0] == MESH_LOCAL_PREFIX_FIRST_BYTE,
+ "Mesh-local prefix must start with 0xfd: " + meshLocalPrefix);
+ this.mMeshLocalPrefix = meshLocalPrefix;
+ return this;
+ }
+
+ /**
+ * Sets the Mesh-Local Prefix.
+ *
+ * @param meshLocalPrefix the prefix used for realm-local traffic within the mesh
+ * @throws IllegalArgumentException if {@code meshLocalPrefix} doesn't start with {@code
+ * 0xfd} or has length other than {@code LENGTH_MESH_LOCAL_PREFIX_BITS / 8}
+ * @hide
+ */
+ @NonNull
+ public Builder setMeshLocalPrefix(byte[] meshLocalPrefix) {
+ final int prefixLength = meshLocalPrefix.length * 8;
+ checkArgument(
+ prefixLength == LENGTH_MESH_LOCAL_PREFIX_BITS,
+ "Invalid mesh-local prefix length (length = %d, expectedLength = %d)",
+ prefixLength,
+ LENGTH_MESH_LOCAL_PREFIX_BITS);
+ byte[] ip6RawAddress = new byte[16];
+ System.arraycopy(meshLocalPrefix, 0, ip6RawAddress, 0, meshLocalPrefix.length);
+ try {
+ return setMeshLocalPrefix(
+ new IpPrefix(Inet6Address.getByAddress(ip6RawAddress), prefixLength));
+ } catch (UnknownHostException e) {
+ // Can't happen because numeric address is provided
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Sets the Security Policy. */
+ @NonNull
+ public Builder setSecurityPolicy(@NonNull SecurityPolicy securityPolicy) {
+ requireNonNull(securityPolicy, "securityPolicy cannot be null");
+ this.mSecurityPolicy = securityPolicy;
+ return this;
+ }
+
+ /**
+ * Sets additional unknown TLVs.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setUnknownTlvs(@NonNull SparseArray<byte[]> unknownTlvs) {
+ requireNonNull(unknownTlvs, "unknownTlvs cannot be null");
+ mUnknownTlvs = deepCloneSparseArray(unknownTlvs);
+ return this;
+ }
+
+ /** Adds one more unknown TLV. @hide */
+ @VisibleForTesting
+ @NonNull
+ public Builder addUnknownTlv(int type, byte[] value) {
+ mUnknownTlvs.put(type, value);
+ return this;
+ }
+
+ /**
+ * Creates a new {@link ActiveOperationalDataset} object.
+ *
+ * @throws IllegalStateException if any of the fields isn't set or the total length exceeds
+ * {@link #LENGTH_MAX_DATASET_TLVS} bytes
+ */
+ @NonNull
+ public ActiveOperationalDataset build() {
+ checkState(mActiveTimestamp != null, "Active Timestamp is missing");
+ checkState(mNetworkName != null, "Network Name is missing");
+ checkState(mExtendedPanId != null, "Extended PAN ID is missing");
+ checkState(mPanId != null, "PAN ID is missing");
+ checkState(mChannel != null, "Channel is missing");
+ checkState(mChannelPage != null, "Channel Page is missing");
+ checkState(mChannelMask.size() != 0, "Channel Mask is missing");
+ checkState(mPskc != null, "PSKc is missing");
+ checkState(mNetworkKey != null, "Network Key is missing");
+ checkState(mMeshLocalPrefix != null, "Mesh Local Prefix is missing");
+ checkState(mSecurityPolicy != null, "Security Policy is missing");
+
+ int length = getTotalDatasetLength();
+ if (length > LENGTH_MAX_DATASET_TLVS) {
+ throw new IllegalStateException(
+ String.format(
+ "Total dataset length exceeds max length %d (actual is %d)",
+ LENGTH_MAX_DATASET_TLVS, length));
+ }
+
+ return new ActiveOperationalDataset(this);
+ }
+
+ private int getTotalDatasetLength() {
+ int length =
+ 2 * 9 // 9 fields with 1 byte of type and 1 byte of length
+ + OperationalDatasetTimestamp.LENGTH_TIMESTAMP
+ + mNetworkName.getBytes(UTF_8).length
+ + LENGTH_EXTENDED_PAN_ID
+ + LENGTH_PAN_ID
+ + LENGTH_CHANNEL
+ + LENGTH_PSKC
+ + LENGTH_NETWORK_KEY
+ + LENGTH_MESH_LOCAL_PREFIX_BITS / 8
+ + mSecurityPolicy.toTlvValue().length;
+
+ for (int i = 0; i < mChannelMask.size(); i++) {
+ length += 2 + mChannelMask.valueAt(i).length;
+ }
+
+ // For the type and length bytes of the Channel Mask TLV because the masks are encoded
+ // as TLVs in TLV.
+ length += 2;
+
+ for (int i = 0; i < mUnknownTlvs.size(); i++) {
+ length += 2 + mUnknownTlvs.valueAt(i).length;
+ }
+
+ return length;
+ }
+ }
+
+ /**
+ * The Security Policy of Thread Operational Dataset which provides an administrator with a way
+ * to enable or disable certain security related behaviors.
+ */
+ public static final class SecurityPolicy {
+ /** The default Rotation Time in hours. */
+ public static final int DEFAULT_ROTATION_TIME_HOURS = 672;
+ /** The minimum length of Security Policy flags in bytes. */
+ public static final int LENGTH_MIN_SECURITY_POLICY_FLAGS = 1;
+ /** The length of Rotation Time TLV value in bytes. */
+ private static final int LENGTH_SECURITY_POLICY_ROTATION_TIME = 2;
+
+ private final int mRotationTimeHours;
+ private final byte[] mFlags;
+
+ /**
+ * Creates a new {@link SecurityPolicy} object.
+ *
+ * @param rotationTimeHours the value for Thread key rotation in hours. Must be in range of
+ * 0x1-0xffff.
+ * @param flags security policy flags with length of either 1 byte for Thread 1.1 or 2 bytes
+ * for Thread 1.2 or higher.
+ * @throws IllegalArgumentException if {@code rotationTimeHours} is not in range of
+ * 0x1-0xffff or length of {@code flags} is smaller than {@link
+ * #LENGTH_MIN_SECURITY_POLICY_FLAGS}.
+ */
+ public SecurityPolicy(
+ @IntRange(from = 0x1, to = 0xffff) int rotationTimeHours,
+ @NonNull @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS) byte[] flags) {
+ requireNonNull(flags, "flags cannot be null");
+ checkArgument(
+ rotationTimeHours >= 1 && rotationTimeHours <= 0xffff,
+ "Rotation time exceeds allowed range (rotationTimeHours = %d, allowedRange ="
+ + " [0x1, 0xffff])",
+ rotationTimeHours);
+ checkArgument(
+ flags.length >= LENGTH_MIN_SECURITY_POLICY_FLAGS,
+ "Invalid security policy flags length (length = %d, minimumLength = %d)",
+ flags.length,
+ LENGTH_MIN_SECURITY_POLICY_FLAGS);
+ this.mRotationTimeHours = rotationTimeHours;
+ this.mFlags = flags.clone();
+ }
+
+ /**
+ * Creates a new {@link SecurityPolicy} object from the Security Policy TLV value.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public static SecurityPolicy fromTlvValue(byte[] encodedSecurityPolicy) {
+ checkArgument(
+ encodedSecurityPolicy.length
+ >= LENGTH_SECURITY_POLICY_ROTATION_TIME
+ + LENGTH_MIN_SECURITY_POLICY_FLAGS,
+ "Invalid Security Policy TLV length (length = %d, minimumLength = %d)",
+ encodedSecurityPolicy.length,
+ LENGTH_SECURITY_POLICY_ROTATION_TIME + LENGTH_MIN_SECURITY_POLICY_FLAGS);
+
+ return new SecurityPolicy(
+ ((encodedSecurityPolicy[0] & 0xff) << 8) | (encodedSecurityPolicy[1] & 0xff),
+ Arrays.copyOfRange(
+ encodedSecurityPolicy,
+ LENGTH_SECURITY_POLICY_ROTATION_TIME,
+ encodedSecurityPolicy.length));
+ }
+
+ /**
+ * Converts this {@link SecurityPolicy} object to Security Policy TLV value.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ @NonNull
+ public byte[] toTlvValue() {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ result.write(mRotationTimeHours >> 8);
+ result.write(mRotationTimeHours);
+ result.write(mFlags, 0, mFlags.length);
+ return result.toByteArray();
+ }
+
+ /** Returns the Security Policy Rotation Time in hours. */
+ @IntRange(from = 0x1, to = 0xffff)
+ public int getRotationTimeHours() {
+ return mRotationTimeHours;
+ }
+
+ /** Returns 1 byte flags for Thread 1.1 or 2 bytes flags for Thread 1.2. */
+ @NonNull
+ @Size(min = LENGTH_MIN_SECURITY_POLICY_FLAGS)
+ public byte[] getFlags() {
+ return mFlags.clone();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof SecurityPolicy)) {
+ return false;
+ } else {
+ SecurityPolicy otherSecurityPolicy = (SecurityPolicy) other;
+ return mRotationTimeHours == otherSecurityPolicy.mRotationTimeHours
+ && Arrays.equals(mFlags, otherSecurityPolicy.mFlags);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return deepHashCode(mRotationTimeHours, mFlags);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{rotation=")
+ .append(mRotationTimeHours)
+ .append(", flags=")
+ .append(dumpHexString(mFlags))
+ .append("}");
+ return sb.toString();
+ }
+ }
+}
diff --git a/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl b/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl
new file mode 100644
index 0000000..aba54eb
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IActiveOperationalDatasetReceiver.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import android.net.thread.ActiveOperationalDataset;
+
+/** Receives the result of an operation which returns an Active Operational Dataset. @hide */
+oneway interface IActiveOperationalDatasetReceiver {
+ void onSuccess(in ActiveOperationalDataset dataset);
+ void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IOperationReceiver.aidl b/thread/framework/java/android/net/thread/IOperationReceiver.aidl
new file mode 100644
index 0000000..42e157b
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IOperationReceiver.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+/** Receives the result of a Thread network operation. @hide */
+oneway interface IOperationReceiver {
+ void onSuccess();
+ void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
new file mode 100644
index 0000000..b576b33
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IOperationalDatasetCallback.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.PendingOperationalDataset;
+
+/**
+ * @hide
+ */
+oneway interface IOperationalDatasetCallback {
+ void onActiveOperationalDatasetChanged(in @nullable ActiveOperationalDataset activeOpDataset);
+ void onPendingOperationalDatasetChanged(in @nullable PendingOperationalDataset pendingOpDataset);
+}
diff --git a/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl b/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl
new file mode 100644
index 0000000..c45d463
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IScheduleMigrationReceiver.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+/** Receives the result of {@link ThreadNetworkManager#scheduleMigration}. @hide */
+oneway interface IScheduleMigrationReceiver {
+ void onScheduled(long delayTimerMillis);
+ void onMigrated();
+ void onError(int errorCode, String errorMessage);
+}
diff --git a/thread/framework/java/android/net/thread/IStateCallback.aidl b/thread/framework/java/android/net/thread/IStateCallback.aidl
new file mode 100644
index 0000000..d7cbda9
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IStateCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+/**
+ * @hide
+ */
+oneway interface IStateCallback {
+ void onDeviceRoleChanged(int deviceRole);
+ void onPartitionIdChanged(long partitionId);
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index 0219beb..89dcd39 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -16,10 +16,28 @@
package android.net.thread;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.IScheduleMigrationReceiver;
+import android.net.thread.IStateCallback;
+import android.net.thread.PendingOperationalDataset;
+
/**
* Interface for communicating with ThreadNetworkControllerService.
* @hide
*/
interface IThreadNetworkController {
+ void registerStateCallback(in IStateCallback callback);
+ void unregisterStateCallback(in IStateCallback callback);
+ void registerOperationalDatasetCallback(in IOperationalDatasetCallback callback);
+ void unregisterOperationalDatasetCallback(in IOperationalDatasetCallback callback);
+
+ void join(in ActiveOperationalDataset activeOpDataset, in IOperationReceiver receiver);
+ void scheduleMigration(in PendingOperationalDataset pendingOpDataset, in IOperationReceiver receiver);
+ void leave(in IOperationReceiver receiver);
+
int getThreadVersion();
+ void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
}
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
new file mode 100644
index 0000000..520acbd
--- /dev/null
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * The timestamp of Thread Operational Dataset.
+ *
+ * @see ActiveOperationalDataset
+ * @see PendingOperationalDataset
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class OperationalDatasetTimestamp {
+ /** @hide */
+ public static final int LENGTH_TIMESTAMP = Long.BYTES;
+
+ private static final int TICKS_UPPER_BOUND = 0x8000;
+
+ private final long mSeconds;
+ private final int mTicks;
+ private final boolean mIsAuthoritativeSource;
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
+ *
+ * <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
+ * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
+ * is set to {@code true}.
+ *
+ * <p>Note that this conversion can lose precision and a value returned by {@link #toInstant}
+ * may not equal exactly the {@code instant}.
+ *
+ * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+ * 0xffffffffffffL}
+ * @see toInstant
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
+ int ticks = getRoundedTicks(instant.getNano());
+ long seconds = instant.getEpochSecond() + ticks / TICKS_UPPER_BOUND;
+ // the rounded ticks can be 0x8000 if instant.getNano() >= 999984742
+ ticks = ticks % TICKS_UPPER_BOUND;
+ return new OperationalDatasetTimestamp(seconds, ticks, true /* isAuthoritativeSource */);
+ }
+
+ /**
+ * Converts this {@link OperationalDatasetTimestamp} object to an {@link Instant}.
+ *
+ * <p>Note that the return value may not equal exactly the {@code instant} if this object is
+ * created with {@link #fromInstant}.
+ *
+ * @see fromInstant
+ */
+ @NonNull
+ public Instant toInstant() {
+ long nanos = Math.round((double) mTicks * 1000000000L / TICKS_UPPER_BOUND);
+ return Instant.ofEpochSecond(mSeconds, nanos);
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object from the OperationalDatasetTimestamp
+ * TLV value.
+ *
+ * @hide
+ */
+ @NonNull
+ public static OperationalDatasetTimestamp fromTlvValue(@NonNull byte[] encodedTimestamp) {
+ requireNonNull(encodedTimestamp, "encodedTimestamp cannot be null");
+ checkArgument(
+ encodedTimestamp.length == LENGTH_TIMESTAMP,
+ "Invalid Thread OperationalDatasetTimestamp length (length = %d,"
+ + " expectedLength=%d)",
+ encodedTimestamp.length,
+ LENGTH_TIMESTAMP);
+ long longTimestamp = ByteBuffer.wrap(encodedTimestamp).getLong();
+ return new OperationalDatasetTimestamp(
+ (longTimestamp >> 16) & 0x0000ffffffffffffL,
+ (int) ((longTimestamp >> 1) & 0x7fffL),
+ (longTimestamp & 0x01) != 0);
+ }
+
+ /**
+ * Converts this {@link OperationalDatasetTimestamp} object to Thread TLV value.
+ *
+ * @hide
+ */
+ @NonNull
+ public byte[] toTlvValue() {
+ byte[] tlv = new byte[LENGTH_TIMESTAMP];
+ ByteBuffer buffer = ByteBuffer.wrap(tlv);
+ long encodedValue = (mSeconds << 16) | (mTicks << 1) | (mIsAuthoritativeSource ? 1 : 0);
+ buffer.putLong(encodedValue);
+ return tlv;
+ }
+
+ /**
+ * Creates a new {@link OperationalDatasetTimestamp} object.
+ *
+ * @param seconds the value encodes a Unix Time value. Must be in the range of
+ * 0x0-0xffffffffffffL
+ * @param ticks the value encodes the fractional Unix Time value in 32.768 kHz resolution. Must
+ * be in the range of 0x0-0x7fff
+ * @param isAuthoritativeSource the flag indicates the time was obtained from an authoritative
+ * source: either NTP (Network Time Protocol), GPS (Global Positioning System), cell
+ * network, or other method
+ * @throws IllegalArgumentException if the {@code seconds} is not in range of
+ * 0x0-0xffffffffffffL or {@code ticks} is not in range of 0x0-0x7fff
+ */
+ public OperationalDatasetTimestamp(
+ @IntRange(from = 0x0, to = 0xffffffffffffL) long seconds,
+ @IntRange(from = 0x0, to = 0x7fff) int ticks,
+ boolean isAuthoritativeSource) {
+ checkArgument(
+ seconds >= 0 && seconds <= 0xffffffffffffL,
+ "seconds exceeds allowed range (seconds = %d,"
+ + " allowedRange = [0x0, 0xffffffffffffL])",
+ seconds);
+ checkArgument(
+ ticks >= 0 && ticks <= 0x7fff,
+ "ticks exceeds allowed ranged (ticks = %d, allowedRange" + " = [0x0, 0x7fff])",
+ ticks);
+ mSeconds = seconds;
+ mTicks = ticks;
+ mIsAuthoritativeSource = isAuthoritativeSource;
+ }
+
+ /**
+ * Returns the rounded ticks converted from the nano seconds.
+ *
+ * <p>Note that rhe return value can be as large as {@code TICKS_UPPER_BOUND}.
+ */
+ private static int getRoundedTicks(long nanos) {
+ return (int) Math.round((double) nanos * TICKS_UPPER_BOUND / 1000000000L);
+ }
+
+ /** Returns the seconds portion of the timestamp. */
+ public @IntRange(from = 0x0, to = 0xffffffffffffL) long getSeconds() {
+ return mSeconds;
+ }
+
+ /** Returns the ticks portion of the timestamp. */
+ public @IntRange(from = 0x0, to = 0x7fff) int getTicks() {
+ return mTicks;
+ }
+
+ /** Returns {@code true} if the timestamp comes from an authoritative source. */
+ public boolean isAuthoritativeSource() {
+ return mIsAuthoritativeSource;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{seconds=")
+ .append(getSeconds())
+ .append(", ticks=")
+ .append(getTicks())
+ .append(", isAuthoritativeSource=")
+ .append(isAuthoritativeSource())
+ .append(", instant=")
+ .append(toInstant())
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof OperationalDatasetTimestamp)) {
+ return false;
+ } else {
+ OperationalDatasetTimestamp otherTimestamp = (OperationalDatasetTimestamp) other;
+ return mSeconds == otherTimestamp.mSeconds
+ && mTicks == otherTimestamp.mTicks
+ && mIsAuthoritativeSource == otherTimestamp.mIsAuthoritativeSource;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSeconds, mTicks, mIsAuthoritativeSource);
+ }
+}
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
new file mode 100644
index 0000000..e5bc05e
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+parcelable PendingOperationalDataset;
diff --git a/thread/framework/java/android/net/thread/PendingOperationalDataset.java b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
new file mode 100644
index 0000000..c1351af
--- /dev/null
+++ b/thread/framework/java/android/net/thread/PendingOperationalDataset.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Data interface for managing a Thread Pending Operational Dataset.
+ *
+ * <p>The Pending Operational Dataset represents an Operational Dataset which will become Active in
+ * a given delay. This is typically used to deploy new network parameters (e.g. Network Key or
+ * Channel) to all devices in the network.
+ *
+ * @see ThreadNetworkController#scheduleMigration
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public final class PendingOperationalDataset implements Parcelable {
+ // Value defined in Thread spec 8.10.1.16
+ private static final int TYPE_PENDING_TIMESTAMP = 51;
+
+ // Values defined in Thread spec 8.10.1.17
+ private static final int TYPE_DELAY_TIMER = 52;
+ private static final int LENGTH_DELAY_TIMER_BYTES = 4;
+
+ @NonNull
+ public static final Creator<PendingOperationalDataset> CREATOR =
+ new Creator<>() {
+ @Override
+ public PendingOperationalDataset createFromParcel(Parcel in) {
+ return PendingOperationalDataset.fromThreadTlvs(in.createByteArray());
+ }
+
+ @Override
+ public PendingOperationalDataset[] newArray(int size) {
+ return new PendingOperationalDataset[size];
+ }
+ };
+
+ @NonNull private final ActiveOperationalDataset mActiveOpDataset;
+ @NonNull private final OperationalDatasetTimestamp mPendingTimestamp;
+ @NonNull private final Duration mDelayTimer;
+
+ /**
+ * Creates a new {@link PendingOperationalDataset} object.
+ *
+ * @param activeOpDataset the included Active Operational Dataset
+ * @param pendingTimestamp the Pending Timestamp which represents the version of this Pending
+ * Dataset
+ * @param delayTimer the delay after when {@code activeOpDataset} will be committed on this
+ * device; use {@link Duration#ZERO} to tell the system to choose a reasonable value
+ * automatically
+ */
+ public PendingOperationalDataset(
+ @NonNull ActiveOperationalDataset activeOpDataset,
+ @NonNull OperationalDatasetTimestamp pendingTimestamp,
+ @NonNull Duration delayTimer) {
+ requireNonNull(activeOpDataset, "activeOpDataset cannot be null");
+ requireNonNull(pendingTimestamp, "pendingTimestamp cannot be null");
+ requireNonNull(delayTimer, "delayTimer cannot be null");
+ this.mActiveOpDataset = activeOpDataset;
+ this.mPendingTimestamp = pendingTimestamp;
+ this.mDelayTimer = delayTimer;
+ }
+
+ /**
+ * Creates a new {@link PendingOperationalDataset} object from a series of Thread TLVs.
+ *
+ * <p>{@code tlvs} can be obtained from the value of a Thread Pending Operational Dataset TLV
+ * (see the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition) or the return value of {@link #toThreadTlvs}.
+ *
+ * @throws IllegalArgumentException if {@code tlvs} is malformed or contains an invalid Thread
+ * TLV
+ */
+ @NonNull
+ public static PendingOperationalDataset fromThreadTlvs(@NonNull byte[] tlvs) {
+ requireNonNull(tlvs, "tlvs cannot be null");
+
+ SparseArray<byte[]> newUnknownTlvs = new SparseArray<>();
+ OperationalDatasetTimestamp pendingTimestamp = null;
+ Duration delayTimer = null;
+ ActiveOperationalDataset activeDataset = ActiveOperationalDataset.fromThreadTlvs(tlvs);
+ SparseArray<byte[]> unknownTlvs = activeDataset.getUnknownTlvs();
+ for (int i = 0; i < unknownTlvs.size(); i++) {
+ int key = unknownTlvs.keyAt(i);
+ byte[] value = unknownTlvs.valueAt(i);
+ switch (key) {
+ case TYPE_PENDING_TIMESTAMP:
+ pendingTimestamp = OperationalDatasetTimestamp.fromTlvValue(value);
+ break;
+ case TYPE_DELAY_TIMER:
+ checkArgument(
+ value.length == LENGTH_DELAY_TIMER_BYTES,
+ "Invalid delay timer (length = %d, expectedLength = %d)",
+ value.length,
+ LENGTH_DELAY_TIMER_BYTES);
+ int millis = ByteBuffer.wrap(value).getInt();
+ delayTimer = Duration.ofMillis(Integer.toUnsignedLong(millis));
+ break;
+ default:
+ newUnknownTlvs.put(key, value);
+ break;
+ }
+ }
+
+ if (pendingTimestamp == null) {
+ throw new IllegalArgumentException("Pending Timestamp is missing");
+ }
+ if (delayTimer == null) {
+ throw new IllegalArgumentException("Delay Timer is missing");
+ }
+
+ activeDataset =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setUnknownTlvs(newUnknownTlvs)
+ .build();
+ return new PendingOperationalDataset(activeDataset, pendingTimestamp, delayTimer);
+ }
+
+ /** Returns the Active Operational Dataset. */
+ @NonNull
+ public ActiveOperationalDataset getActiveOperationalDataset() {
+ return mActiveOpDataset;
+ }
+
+ /** Returns the Pending Timestamp. */
+ @NonNull
+ public OperationalDatasetTimestamp getPendingTimestamp() {
+ return mPendingTimestamp;
+ }
+
+ /** Returns the Delay Timer. */
+ @NonNull
+ public Duration getDelayTimer() {
+ return mDelayTimer;
+ }
+
+ /**
+ * Converts this {@link PendingOperationalDataset} object to a series of Thread TLVs.
+ *
+ * <p>See the <a href="https://www.threadgroup.org/support#specifications">Thread
+ * specification</a> for the definition of the Thread TLV format.
+ */
+ @NonNull
+ public byte[] toThreadTlvs() {
+ ByteArrayOutputStream dataset = new ByteArrayOutputStream();
+
+ byte[] activeDatasetBytes = mActiveOpDataset.toThreadTlvs();
+ dataset.write(activeDatasetBytes, 0, activeDatasetBytes.length);
+
+ dataset.write(TYPE_PENDING_TIMESTAMP);
+ byte[] pendingTimestampBytes = mPendingTimestamp.toTlvValue();
+ dataset.write(pendingTimestampBytes.length);
+ dataset.write(pendingTimestampBytes, 0, pendingTimestampBytes.length);
+
+ dataset.write(TYPE_DELAY_TIMER);
+ byte[] delayTimerBytes = new byte[LENGTH_DELAY_TIMER_BYTES];
+ ByteBuffer.wrap(delayTimerBytes).putInt((int) mDelayTimer.toMillis());
+ dataset.write(delayTimerBytes.length);
+ dataset.write(delayTimerBytes, 0, delayTimerBytes.length);
+
+ return dataset.toByteArray();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof PendingOperationalDataset)) {
+ return false;
+ } else {
+ PendingOperationalDataset otherDataset = (PendingOperationalDataset) other;
+ return mActiveOpDataset.equals(otherDataset.mActiveOpDataset)
+ && mPendingTimestamp.equals(otherDataset.mPendingTimestamp)
+ && mDelayTimer.equals(otherDataset.mDelayTimer);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mActiveOpDataset, mPendingTimestamp, mDelayTimer);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{activeDataset=")
+ .append(getActiveOperationalDataset())
+ .append(", pendingTimestamp=")
+ .append(getPendingTimestamp())
+ .append(", delayTimer=")
+ .append(getDelayTimer())
+ .append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeByteArray(toThreadTlvs());
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index fe189c2..34b0b06 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -18,21 +18,65 @@
import static java.util.Objects.requireNonNull;
+import android.Manifest.permission;
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
+import android.os.Binder;
+import android.os.OutcomeReceiver;
import android.os.RemoteException;
+import com.android.internal.annotations.GuardedBy;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
/**
- * Provides the primary API for controlling all aspects of a Thread network.
+ * Provides the primary APIs for controlling all aspects of a Thread network.
+ *
+ * <p>For example, join this device to a Thread network with given Thread Operational Dataset, or
+ * migrate an existing network.
*
* @hide
*/
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
-public class ThreadNetworkController {
+public final class ThreadNetworkController {
+ private static final String TAG = "ThreadNetworkController";
+
+ /** The Thread stack is stopped. */
+ public static final int DEVICE_ROLE_STOPPED = 0;
+
+ /** The device is not currently participating in a Thread network/partition. */
+ public static final int DEVICE_ROLE_DETACHED = 1;
+
+ /** The device is a Thread Child. */
+ public static final int DEVICE_ROLE_CHILD = 2;
+
+ /** The device is a Thread Router. */
+ public static final int DEVICE_ROLE_ROUTER = 3;
+
+ /** The device is a Thread Leader. */
+ public static final int DEVICE_ROLE_LEADER = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DEVICE_ROLE_STOPPED,
+ DEVICE_ROLE_DETACHED,
+ DEVICE_ROLE_CHILD,
+ DEVICE_ROLE_ROUTER,
+ DEVICE_ROLE_LEADER
+ })
+ public @interface DeviceRole {}
/** Thread standard version 1.3. */
public static final int THREAD_VERSION_1_3 = 4;
@@ -44,9 +88,20 @@
private final IThreadNetworkController mControllerService;
- ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
- requireNonNull(controllerService, "controllerService cannot be null");
+ private final Object mStateCallbackMapLock = new Object();
+ @GuardedBy("mStateCallbackMapLock")
+ private final Map<StateCallback, StateCallbackProxy> mStateCallbackMap = new HashMap<>();
+
+ private final Object mOpDatasetCallbackMapLock = new Object();
+
+ @GuardedBy("mOpDatasetCallbackMapLock")
+ private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
+ mOpDatasetCallbackMap = new HashMap<>();
+
+ /** @hide */
+ public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
+ requireNonNull(controllerService, "controllerService cannot be null");
mControllerService = controllerService;
}
@@ -59,4 +114,457 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Creates a new Active Operational Dataset with randomized parameters.
+ *
+ * <p>This method is the recommended way to create a randomized dataset which can be used with
+ * {@link #join} to securely join this device to the specified network . It's highly discouraged
+ * to change the randomly generated Extended PAN ID, Network Key or PSKc, as it will compromise
+ * the security of a Thread network.
+ *
+ * @throws IllegalArgumentException if length of the UTF-8 representation of {@code networkName}
+ * isn't in range of [{@link #LENGTH_MIN_NETWORK_NAME_BYTES}, {@link
+ * #LENGTH_MAX_NETWORK_NAME_BYTES}]
+ */
+ public void createRandomizedDataset(
+ @NonNull String networkName,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> receiver) {
+ ActiveOperationalDataset.checkNetworkName(networkName);
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.createRandomizedDataset(
+ networkName, new ActiveDatasetReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Returns {@code true} if {@code deviceRole} indicates an attached state. */
+ public static boolean isAttached(@DeviceRole int deviceRole) {
+ return deviceRole == DEVICE_ROLE_CHILD
+ || deviceRole == DEVICE_ROLE_ROUTER
+ || deviceRole == DEVICE_ROLE_LEADER;
+ }
+
+ /**
+ * Callback to receive notifications when the Thread network states are changed.
+ *
+ * <p>Applications which are interested in monitoring Thread network states should implement
+ * this interface and register the callback with {@link #registerStateCallback}.
+ */
+ public interface StateCallback {
+ /**
+ * The Thread device role has changed.
+ *
+ * @param deviceRole the new Thread device role
+ */
+ void onDeviceRoleChanged(@DeviceRole int deviceRole);
+
+ /**
+ * The Thread network partition ID has changed.
+ *
+ * @param partitionId the new Thread partition ID
+ */
+ default void onPartitionIdChanged(long partitionId) {}
+ }
+
+ private static final class StateCallbackProxy extends IStateCallback.Stub {
+ private final Executor mExecutor;
+ private final StateCallback mCallback;
+
+ StateCallbackProxy(@CallbackExecutor Executor executor, StateCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onDeviceRoleChanged(@DeviceRole int deviceRole) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onDeviceRoleChanged(deviceRole));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onPartitionIdChanged(long partitionId) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onPartitionIdChanged(partitionId));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ /**
+ * Registers a callback to be called when Thread network states are changed.
+ *
+ * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
+ * existing states.
+ *
+ * @param executor the executor to execute the {@code callback}
+ * @param callback the callback to receive Thread network state changes
+ * @throws IllegalArgumentException if {@code callback} has already been registered
+ */
+ @RequiresPermission(permission.ACCESS_NETWORK_STATE)
+ public void registerStateCallback(
+ @NonNull @CallbackExecutor Executor executor, @NonNull StateCallback callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mStateCallbackMapLock) {
+ if (mStateCallbackMap.containsKey(callback)) {
+ throw new IllegalArgumentException("callback has already been registered");
+ }
+ StateCallbackProxy callbackProxy = new StateCallbackProxy(executor, callback);
+ mStateCallbackMap.put(callback, callbackProxy);
+
+ try {
+ mControllerService.registerStateCallback(callbackProxy);
+ } catch (RemoteException e) {
+ mStateCallbackMap.remove(callback);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the Thread state changed callback.
+ *
+ * @param callback the callback which has been registered with {@link #registerStateCallback}
+ * @throws IllegalArgumentException if {@code callback} hasn't been registered
+ */
+ @RequiresPermission(permission.ACCESS_NETWORK_STATE)
+ public void unregisterStateCallback(@NonNull StateCallback callback) {
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mStateCallbackMapLock) {
+ StateCallbackProxy callbackProxy = mStateCallbackMap.get(callback);
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("callback hasn't been registered");
+ }
+ try {
+ mControllerService.unregisterStateCallback(callbackProxy);
+ mStateCallbackMap.remove(callback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Callback to receive notifications when the Thread Operational Datasets are changed.
+ *
+ * <p>Applications which are interested in monitoring Thread network datasets should implement
+ * this interface and register the callback with {@link #registerOperationalDatasetCallback}.
+ */
+ public interface OperationalDatasetCallback {
+ /**
+ * Called when the Active Operational Dataset is changed.
+ *
+ * @param activeDataset the new Active Operational Dataset or {@code null} if the dataset is
+ * absent
+ */
+ void onActiveOperationalDatasetChanged(@Nullable ActiveOperationalDataset activeDataset);
+
+ /**
+ * Called when the Pending Operational Dataset is changed.
+ *
+ * @param pendingDataset the new Pending Operational Dataset or {@code null} if the dataset
+ * has been committed and removed
+ */
+ default void onPendingOperationalDatasetChanged(
+ @Nullable PendingOperationalDataset pendingDataset) {}
+ }
+
+ private static final class OperationalDatasetCallbackProxy
+ extends IOperationalDatasetCallback.Stub {
+ private final Executor mExecutor;
+ private final OperationalDatasetCallback mCallback;
+
+ OperationalDatasetCallbackProxy(
+ @CallbackExecutor Executor executor, OperationalDatasetCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ @Nullable ActiveOperationalDataset activeDataset) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onActiveOperationalDatasetChanged(activeDataset));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ @Nullable PendingOperationalDataset pendingDataset) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(
+ () -> mCallback.onPendingOperationalDatasetChanged(pendingDataset));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ /**
+ * Registers a callback to be called when Thread Operational Datasets are changed.
+ *
+ * <p>Upon return of this method, methods of {@code callback} will be invoked immediately with
+ * existing Operational Datasets.
+ *
+ * @param executor the executor to execute {@code callback}
+ * @param callback the callback to receive Operational Dataset changes
+ * @throws IllegalArgumentException if {@code callback} has already been registered
+ */
+ @RequiresPermission(
+ allOf = {
+ permission.ACCESS_NETWORK_STATE,
+ "android.permission.THREAD_NETWORK_PRIVILEGED"
+ })
+ public void registerOperationalDatasetCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OperationalDatasetCallback callback) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mOpDatasetCallbackMapLock) {
+ if (mOpDatasetCallbackMap.containsKey(callback)) {
+ throw new IllegalArgumentException("callback has already been registered");
+ }
+ OperationalDatasetCallbackProxy callbackProxy =
+ new OperationalDatasetCallbackProxy(executor, callback);
+ mOpDatasetCallbackMap.put(callback, callbackProxy);
+
+ try {
+ mControllerService.registerOperationalDatasetCallback(callbackProxy);
+ } catch (RemoteException e) {
+ mOpDatasetCallbackMap.remove(callback);
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the Thread Operational Dataset callback.
+ *
+ * @param callback the callback which has been registered with {@link
+ * #registerOperationalDatasetCallback}
+ * @throws IllegalArgumentException if {@code callback} hasn't been registered
+ */
+ @RequiresPermission(
+ allOf = {
+ permission.ACCESS_NETWORK_STATE,
+ "android.permission.THREAD_NETWORK_PRIVILEGED"
+ })
+ public void unregisterOperationalDatasetCallback(@NonNull OperationalDatasetCallback callback) {
+ requireNonNull(callback, "callback cannot be null");
+ synchronized (mOpDatasetCallbackMapLock) {
+ OperationalDatasetCallbackProxy callbackProxy = mOpDatasetCallbackMap.get(callback);
+ if (callbackProxy == null) {
+ throw new IllegalArgumentException("callback hasn't been registered");
+ }
+ try {
+ mControllerService.unregisterOperationalDatasetCallback(callbackProxy);
+ mOpDatasetCallbackMap.remove(callback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Joins to a Thread network with given Active Operational Dataset.
+ *
+ * <p>This method does nothing if this device has already joined to the same network specified
+ * by {@code activeDataset}. If this device has already joined to a different network, this
+ * device will first leave from that network and then join the new network. This method changes
+ * only this device and all other connected devices will stay in the old network. To change the
+ * network for all connected devices together, use {@link #scheduleMigration}.
+ *
+ * <p>On success, {@link OutcomeReceiver#onResult} of {@code receiver} is called and the Dataset
+ * will be persisted on this device; this device will try to attach to the Thread network and
+ * the state changes can be observed by {@link #registerStateCallback}. On failure, {@link
+ * OutcomeReceiver#onError} of {@code receiver} will be invoked with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code activeDataset}
+ * specifies a channel which is not supported in the current country or region; the {@code
+ * activeDataset} is rejected and not persisted so this device won't auto re-join the next
+ * time
+ * <li>{@link ThreadNetworkException#ERROR_ABORTED} this operation is aborted by another
+ * {@code join} or {@code leave} operation
+ * </ul>
+ *
+ * @param activeDataset the Active Operational Dataset represents the Thread network to join
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void join(
+ @NonNull ActiveOperationalDataset activeDataset,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(activeDataset, "activeDataset cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.join(activeDataset, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Schedules a network migration which moves all devices in the current connected network to a
+ * new network or updates parameters of the current connected network.
+ *
+ * <p>The migration doesn't happen immediately but is registered to the Leader device so that
+ * all devices in the current Thread network can be scheduled to apply the new dataset together.
+ *
+ * <p>On success, the Pending Dataset is successfully registered and persisted on the Leader and
+ * {@link OutcomeReceiver#onResult} of {@code receiver} will be called; Operational Dataset
+ * changes will be asynchronously delivered via {@link OperationalDatasetCallback} if a callback
+ * has been registered with {@link #registerOperationalDatasetCallback}. When failed, {@link
+ * OutcomeReceiver#onError} will be called with a specific error:
+ *
+ * <ul>
+ * <li>{@link ThreadNetworkException#ERROR_FAILED_PRECONDITION} the migration is rejected
+ * because this device is not attached
+ * <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_CHANNEL} {@code pendingDataset}
+ * specifies a channel which is not supported in the current country or region; the {@code
+ * pendingDataset} is rejected and not persisted
+ * <li>{@link ThreadNetworkException#ERROR_REJECTED_BY_PEER} the Pending Dataset is rejected
+ * by the Leader device
+ * <li>{@link ThreadNetworkException#ERROR_BUSY} another {@code scheduleMigration} request is
+ * being processed
+ * <li>{@link ThreadNetworkException#ERROR_TIMEOUT} response from the Leader device hasn't
+ * been received before deadline
+ * </ul>
+ *
+ * <p>The Delay Timer of {@code pendingDataset} can vary from several minutes to a few days.
+ * It's important to select a proper value to safely migrate all devices in the network without
+ * leaving sleepy end devices orphaned. Apps are not suggested to specify the Delay Timer value
+ * if it's unclear how long it can take to propagate the {@code pendingDataset} to the whole
+ * network. Instead, use {@link Duration#ZERO} to use the default value suggested by the system.
+ *
+ * @param pendingDataset the Pending Operational Dataset
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void scheduleMigration(
+ @NonNull PendingOperationalDataset pendingDataset,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(pendingDataset, "pendingDataset cannot be null");
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.scheduleMigration(
+ pendingDataset, new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Leaves from the Thread network.
+ *
+ * <p>This undoes a {@link join} operation. On success, this device is disconnected from the
+ * joined network and will not automatically join a network before {@link #join} is called
+ * again. Active and Pending Operational Dataset configured and persisted on this device will be
+ * removed too.
+ *
+ * @param executor the executor to execute {@code receiver}
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED")
+ public void leave(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+ requireNonNull(executor, "executor cannot be null");
+ requireNonNull(receiver, "receiver cannot be null");
+ try {
+ mControllerService.leave(new OperationReceiverProxy(executor, receiver));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private static <T> void propagateError(
+ Executor executor,
+ OutcomeReceiver<T, ThreadNetworkException> receiver,
+ int errorCode,
+ String errorMsg) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ executor.execute(
+ () -> receiver.onError(new ThreadNetworkException(errorCode, errorMsg)));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ private static final class ActiveDatasetReceiverProxy
+ extends IActiveOperationalDatasetReceiver.Stub {
+ final Executor mExecutor;
+ final OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> mResultReceiver;
+
+ ActiveDatasetReceiverProxy(
+ @CallbackExecutor Executor executor,
+ OutcomeReceiver<ActiveOperationalDataset, ThreadNetworkException> resultReceiver) {
+ this.mExecutor = executor;
+ this.mResultReceiver = resultReceiver;
+ }
+
+ @Override
+ public void onSuccess(ActiveOperationalDataset dataset) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mResultReceiver.onResult(dataset));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
+ }
+ }
+
+ private static final class OperationReceiverProxy extends IOperationReceiver.Stub {
+ final Executor mExecutor;
+ final OutcomeReceiver<Void, ThreadNetworkException> mResultReceiver;
+
+ OperationReceiverProxy(
+ @CallbackExecutor Executor executor,
+ OutcomeReceiver<Void, ThreadNetworkException> resultReceiver) {
+ this.mExecutor = executor;
+ this.mResultReceiver = resultReceiver;
+ }
+
+ @Override
+ public void onSuccess() {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mResultReceiver.onResult(null));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) {
+ propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
+ }
+ }
}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
new file mode 100644
index 0000000..c5e1e97
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a Thread network specific failure.
+ *
+ * @hide
+ */
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
+@SystemApi
+public class ThreadNetworkException extends Exception {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ ERROR_INTERNAL_ERROR,
+ ERROR_ABORTED,
+ ERROR_TIMEOUT,
+ ERROR_UNAVAILABLE,
+ ERROR_BUSY,
+ ERROR_FAILED_PRECONDITION,
+ ERROR_UNSUPPORTED_CHANNEL,
+ ERROR_REJECTED_BY_PEER,
+ ERROR_RESPONSE_BAD_FORMAT,
+ ERROR_RESOURCE_EXHAUSTED,
+ })
+ public @interface ErrorCode {}
+
+ /**
+ * The operation failed because some invariants expected by the underlying system have been
+ * broken. This error code is reserved for serious errors. The caller can do nothing to recover
+ * from this error. A bugreport should be created and sent to the Android community if this
+ * error is ever returned.
+ */
+ public static final int ERROR_INTERNAL_ERROR = 1;
+
+ /**
+ * The operation failed because concurrent operations are overriding this one. Retrying an
+ * aborted operation has the risk of aborting another ongoing operation again. So the caller
+ * should retry at a higher level where it knows there won't be race conditions.
+ */
+ public static final int ERROR_ABORTED = 2;
+
+ /**
+ * The operation failed because a deadline expired before the operation could complete. This may
+ * be caused by connectivity unavailability and the caller can retry the same operation when the
+ * connectivity issue is fixed.
+ */
+ public static final int ERROR_TIMEOUT = 3;
+
+ /**
+ * The operation failed because the service is currently unavailable and that this is most
+ * likely a transient condition. The caller can recover from this error by retrying with a
+ * back-off scheme. Note that it is not always safe to retry non-idempotent operations.
+ */
+ public static final int ERROR_UNAVAILABLE = 4;
+
+ /**
+ * The operation failed because this device is currently busy processing concurrent requests.
+ * The caller may recover from this error when the current operations has been finished.
+ */
+ public static final int ERROR_BUSY = 5;
+
+ /**
+ * The operation failed because required preconditions were not satisfied. For example, trying
+ * to schedule a network migration when this device is not attached will receive this error. The
+ * caller should not retry the same operation before the precondition is satisfied.
+ */
+ public static final int ERROR_FAILED_PRECONDITION = 6;
+
+ /**
+ * The operation was rejected because the specified channel is currently not supported by this
+ * device in this country. For example, trying to join or migrate to a network with channel
+ * which is not supported. The caller should should change the channel or return an error to the
+ * user if the channel cannot be changed.
+ */
+ public static final int ERROR_UNSUPPORTED_CHANNEL = 7;
+
+ /**
+ * The operation failed because a request is rejected by the peer device. This happens because
+ * the peer device is not capable of processing the request, or a request from another device
+ * has already been accepted by the peer device. The caller may not be able to recover from this
+ * error by retrying the same operation.
+ */
+ public static final int ERROR_REJECTED_BY_PEER = 8;
+
+ /**
+ * The operation failed because the received response is malformed. This is typically because
+ * the peer device is misbehaving. The caller may only recover from this error by retrying with
+ * a different peer device.
+ */
+ public static final int ERROR_RESPONSE_BAD_FORMAT = 9;
+
+ /**
+ * The operation failed because some resource has been exhausted. For example, no enough
+ * allocated memory buffers, or maximum number of supported operations has been exceeded. The
+ * caller may retry and recover from this error when the resource has been freed.
+ */
+ public static final int ERROR_RESOURCE_EXHAUSTED = 10;
+
+ private final int mErrorCode;
+
+ /** Creates a new {@link ThreadNetworkException} object with given error code and message. */
+ public ThreadNetworkException(@ErrorCode int errorCode, @NonNull String errorMessage) {
+ super(requireNonNull(errorMessage, "errorMessage cannot be null"));
+ this.mErrorCode = errorCode;
+ }
+
+ /** Returns the error code. */
+ public @ErrorCode int getErrorCode() {
+ return mErrorCode;
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
new file mode 100644
index 0000000..e6ab988
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+/**
+ * Container for flag constants defined in the "thread_network" namespace.
+ *
+ * @hide
+ */
+// TODO: replace this class with auto-generated "com.android.net.thread.flags.Flags" once the
+// flagging infra is fully supported for mainline modules.
+public final class ThreadNetworkFlags {
+ /** @hide */
+ public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
+
+ private ThreadNetworkFlags() {}
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 2a253a1..28012a7 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -18,6 +18,7 @@
import static java.util.Objects.requireNonNull;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -34,9 +35,10 @@
*
* @hide
*/
+@FlaggedApi(ThreadNetworkFlags.FLAG_THREAD_ENABLED)
@SystemApi
@SystemService(ThreadNetworkManager.SERVICE_NAME)
-public class ThreadNetworkManager {
+public final class ThreadNetworkManager {
/**
* This value tracks {@link Context#THREAD_NETWORK_SERVICE}.
*
@@ -64,6 +66,19 @@
*/
public static final String FEATURE_NAME = "android.hardware.thread_network";
+ /**
+ * Permission allows changing Thread network state and access to Thread network credentials such
+ * as Network Key and PSKc.
+ *
+ * <p>This is the same value as android.Manifest.permission.THREAD_NETWORK_PRIVILEGED. That
+ * symbol is not available on U while this feature needs to support Android U TV devices, so
+ * here is making a copy of android.Manifest.permission.THREAD_NETWORK_PRIVILEGED.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+ "android.permission.THREAD_NETWORK_PRIVILEGED";
+
@NonNull private final Context mContext;
@NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
diff --git a/thread/scripts/make-pretty.sh b/thread/scripts/make-pretty.sh
new file mode 100755
index 0000000..e4bd459
--- /dev/null
+++ b/thread/scripts/make-pretty.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+GOOGLE_JAVA_FORMAT=$SCRIPT_DIR/../../../../../prebuilts/tools/common/google-java-format/google-java-format
+
+$GOOGLE_JAVA_FORMAT --aosp -i $(find $SCRIPT_DIR/../ -name "*.java")
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index f1af653..35ae3c2 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -33,10 +33,38 @@
min_sdk_version: "30",
srcs: [":service-thread-sources"],
libs: [
+ "framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "service-connectivity-pre-jarjar",
],
static_libs: [
"net-utils-device-common",
+ "net-utils-device-common-netlink",
+ "ot-daemon-aidl-java",
+ ],
+ apex_available: ["com.android.tethering"],
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
+}
+
+cc_library_shared {
+ name: "libservice-thread-jni",
+ min_sdk_version: "30",
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ "-Wthread-safety",
+ ],
+ srcs: [
+ "jni/**/*.cpp",
+ ],
+ shared_libs: [
+ "libbase",
+ "libcutils",
+ "liblog",
+ "libnativehelper",
],
apex_available: ["com.android.tethering"],
}
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
new file mode 100644
index 0000000..d7c49a0
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.os.ParcelFileDescriptor;
+
+import java.io.IOException;
+
+/** Controller for the infrastructure network interface. */
+public class InfraInterfaceController {
+ private static final String TAG = "InfraIfController";
+
+ static {
+ System.loadLibrary("service-thread-jni");
+ }
+
+ /**
+ * Creates a socket on the infrastructure network interface for sending/receiving ICMPv6
+ * Neighbor Discovery messages.
+ *
+ * @param infraInterfaceName the infrastructure network interface name.
+ * @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
+ * @throws IOException when fails to create the socket.
+ */
+ public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
+ throws IOException {
+ return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
+ }
+
+ private static native int nativeCreateIcmp6Socket(String interfaceName) throws IOException;
+}
diff --git a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
new file mode 100644
index 0000000..a8909bc
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+
+import android.net.thread.IOperationReceiver;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */
+final class OperationReceiverWrapper {
+ private final IOperationReceiver mReceiver;
+
+ private static final Object sPendingReceiversLock = new Object();
+
+ @GuardedBy("sPendingReceiversLock")
+ private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
+
+ public OperationReceiverWrapper(IOperationReceiver receiver) {
+ this.mReceiver = receiver;
+
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.add(this);
+ }
+ }
+
+ public static void onOtDaemonDied() {
+ synchronized (sPendingReceiversLock) {
+ for (OperationReceiverWrapper receiver : sPendingReceivers) {
+ try {
+ receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died");
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+ sPendingReceivers.clear();
+ }
+ }
+
+ public void onSuccess() {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onSuccess();
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+
+ public void onError(int errorCode, String errorMessage, Object... messageArgs) {
+ synchronized (sPendingReceiversLock) {
+ sPendingReceivers.remove(this);
+ }
+
+ try {
+ mReceiver.onError(errorCode, String.format(errorMessage, messageArgs));
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index e8b95bc..60c97bf 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -1,31 +1,1097 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
*/
package com.android.server.thread;
+import static android.net.MulticastRoutingConfig.CONFIG_FORWARD_NONE;
+import static android.net.MulticastRoutingConfig.FORWARD_NONE;
+import static android.net.MulticastRoutingConfig.FORWARD_SELECTED;
+import static android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE;
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_EXTENDED_PAN_ID;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_MESH_LOCAL_PREFIX_BITS;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_NETWORK_KEY;
+import static android.net.thread.ActiveOperationalDataset.LENGTH_PSKC;
+import static android.net.thread.ActiveOperationalDataset.MESH_LOCAL_PREFIX_FIRST_BYTE;
+import static android.net.thread.ActiveOperationalDataset.SecurityPolicy.DEFAULT_ROTATION_TIME_HOURS;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_DETACHED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_BUSY;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+import static android.net.thread.ThreadNetworkException.ERROR_RESOURCE_EXHAUSTED;
+import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
+import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkException.ErrorCode;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_BUSY;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_DETACHED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_NO_BUFS;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_PARSE;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REASSEMBLY_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_REJECTED;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_RESPONSE_TIMEOUT;
+import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_UNSUPPORTED_CHANNEL;
+import static com.android.server.thread.openthread.IOtDaemon.TUN_IF_NAME;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.LocalNetworkConfig;
+import android.net.MulticastRoutingConfig;
+import android.net.LocalNetworkInfo;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.RouteInfo;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IStateCallback;
import android.net.thread.IThreadNetworkController;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
-/** Implementation of the {@link ThreadNetworkController} API. */
-public final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceManagerWrapper;
+import com.android.server.thread.openthread.IOtDaemon;
+import com.android.server.thread.openthread.IOtDaemonCallback;
+import com.android.server.thread.openthread.IOtStatusReceiver;
+import com.android.server.thread.openthread.Ipv6AddressInfo;
+import com.android.server.thread.openthread.OtDaemonState;
+import com.android.server.thread.openthread.BorderRouterConfigurationParcel;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Random;
+import java.util.function.Supplier;
+
+/**
+ * Implementation of the {@link ThreadNetworkController} API.
+ *
+ * <p>Threading model: This class is not Thread-safe and should only be accessed from the
+ * ThreadNetworkService class. Additional attention should be paid to handle the threading code
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
+ * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * HandlerThread except for arguments or permissions checking
+ */
+final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+ private static final String TAG = "ThreadNetworkService";
+
+ // Below member fields can be accessed from both the binder and handler threads
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+ // particular, the constructor does not run on the handler thread, so it must not touch any of
+ // the non-final fields, nor must it mutate any of the non-final fields inside these objects.
+
+ private final HandlerThread mHandlerThread;
+ private final NetworkProvider mNetworkProvider;
+ private final Supplier<IOtDaemon> mOtDaemonSupplier;
+ private final ConnectivityManager mConnectivityManager;
+ private final TunInterfaceController mTunIfController;
+ private final LinkProperties mLinkProperties = new LinkProperties();
+ private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
+
+ // TODO(b/308310823): read supported channel from Thread dameon
+ private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
+
+ private IOtDaemon mOtDaemon;
+ private NetworkAgent mNetworkAgent;
+ private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ private Network mUpstreamNetwork;
+ private final NetworkRequest mUpstreamNetworkRequest;
+ private final HashMap<Network, String> mNetworkToInterface;
+ private final LocalNetworkConfig mLocalNetworkConfig;
+
+ private BorderRouterConfigurationParcel mBorderRouterConfig;
+
+ @VisibleForTesting
+ ThreadNetworkControllerService(
+ Context context,
+ HandlerThread handlerThread,
+ NetworkProvider networkProvider,
+ Supplier<IOtDaemon> otDaemonSupplier,
+ ConnectivityManager connectivityManager,
+ TunInterfaceController tunIfController) {
+ mContext = context;
+ mHandlerThread = handlerThread;
+ mHandler = new Handler(handlerThread.getLooper());
+ mNetworkProvider = networkProvider;
+ mOtDaemonSupplier = otDaemonSupplier;
+ mConnectivityManager = connectivityManager;
+ mTunIfController = tunIfController;
+ mUpstreamNetworkRequest =
+ new NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+ .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+ .build();
+ mLocalNetworkConfig =
+ new LocalNetworkConfig.Builder()
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
+ mNetworkToInterface = new HashMap<Network, String>();
+ mBorderRouterConfig = new BorderRouterConfigurationParcel();
+ }
+
+ public static ThreadNetworkControllerService newInstance(Context context) {
+ HandlerThread handlerThread = new HandlerThread("ThreadHandlerThread");
+ handlerThread.start();
+ NetworkProvider networkProvider =
+ new NetworkProvider(context, handlerThread.getLooper(), "ThreadNetworkProvider");
+
+ return new ThreadNetworkControllerService(
+ context,
+ handlerThread,
+ networkProvider,
+ () -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
+ context.getSystemService(ConnectivityManager.class),
+ new TunInterfaceController(TUN_IF_NAME));
+ }
+
+ private static NetworkCapabilities newNetworkCapabilities() {
+ return new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ }
+
+ private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
+ try {
+ return (Inet6Address) Inet6Address.getByAddress(addressBytes);
+ } catch (UnknownHostException e) {
+ // This is unlikely to happen unless the Thread daemon is critically broken
+ return null;
+ }
+ }
+
+ private static InetAddress addressInfoToInetAddress(Ipv6AddressInfo addressInfo) {
+ return bytesToInet6Address(addressInfo.address);
+ }
+
+ private static LinkAddress newLinkAddress(Ipv6AddressInfo addressInfo) {
+ long deprecationTimeMillis =
+ addressInfo.isPreferred
+ ? LinkAddress.LIFETIME_PERMANENT
+ : SystemClock.elapsedRealtime();
+
+ InetAddress address = addressInfoToInetAddress(addressInfo);
+
+ // flags and scope will be adjusted automatically depending on the address and
+ // its lifetimes.
+ return new LinkAddress(
+ address,
+ addressInfo.prefixLength,
+ 0 /* flags */,
+ 0 /* scope */,
+ deprecationTimeMillis,
+ LinkAddress.LIFETIME_PERMANENT /* expirationTime */);
+ }
+
+ private void initializeOtDaemon() {
+ try {
+ getOtDaemon();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to initialize ot-daemon");
+ }
+ }
+
+ private IOtDaemon getOtDaemon() throws RemoteException {
+ if (mOtDaemon != null) {
+ return mOtDaemon;
+ }
+
+ IOtDaemon otDaemon = mOtDaemonSupplier.get();
+ if (otDaemon == null) {
+ throw new RemoteException("Internal error: failed to start OT daemon");
+ }
+ otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
+ otDaemon.initialize(mTunIfController.getTunFd());
+ otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+ mOtDaemon = otDaemon;
+ return mOtDaemon;
+ }
+
+ // TODO(b/309792480): restarts the OT daemon service
+ private void onOtDaemonDied() {
+ Log.w(TAG, "OT daemon became dead, clean up...");
+ OperationReceiverWrapper.onOtDaemonDied();
+ mOtDaemonCallbackProxy.onOtDaemonDied();
+ mOtDaemon = null;
+ }
+
+ public void initialize() {
+ mHandler.post(
+ () -> {
+ Log.d(TAG, "Initializing Thread system service...");
+ try {
+ mTunIfController.createTunInterface();
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Failed to create Thread tunnel interface", e);
+ }
+ mLinkProperties.setInterfaceName(TUN_IF_NAME);
+ mLinkProperties.setMtu(TunInterfaceController.MTU);
+ mConnectivityManager.registerNetworkProvider(mNetworkProvider);
+ requestUpstreamNetwork();
+
+ initializeOtDaemon();
+ });
+ }
+
+ private void requestUpstreamNetwork() {
+ mConnectivityManager.registerNetworkCallback(
+ mUpstreamNetworkRequest,
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ Log.i(TAG, "onAvailable: " + network);
+ }
+
+ @Override
+ public void onLost(@NonNull Network network) {
+ Log.i(TAG, "onLost: " + network);
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(
+ @NonNull Network network, @NonNull LinkProperties linkProperties) {
+ Log.i(
+ TAG,
+ String.format(
+ "onLinkPropertiesChanged: {network: %s, interface: %s}",
+ network, linkProperties.getInterfaceName()));
+ mNetworkToInterface.put(network, linkProperties.getInterfaceName());
+ if (network.equals(mUpstreamNetwork)) {
+ enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ }
+ }
+ },
+ mHandler);
+ }
+
+ private final class ThreadNetworkCallback extends ConnectivityManager.NetworkCallback {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ Log.i(TAG, "onAvailable: Thread network Available");
+ }
+
+ @Override
+ public void onLocalNetworkInfoChanged(
+ @NonNull Network network, @NonNull LocalNetworkInfo localNetworkInfo) {
+ Log.i(TAG, "onLocalNetworkInfoChanged: " + localNetworkInfo);
+ if (localNetworkInfo.getUpstreamNetwork() == null) {
+ mUpstreamNetwork = null;
+ return;
+ }
+ if (!localNetworkInfo.getUpstreamNetwork().equals(mUpstreamNetwork)) {
+ mUpstreamNetwork = localNetworkInfo.getUpstreamNetwork();
+ if (mNetworkToInterface.containsKey(mUpstreamNetwork)) {
+ enableBorderRouting(mNetworkToInterface.get(mUpstreamNetwork));
+ }
+ }
+ }
+ }
+
+ private void requestThreadNetwork() {
+ mConnectivityManager.registerNetworkCallback(
+ new NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .removeForbiddenCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .build(),
+ new ThreadNetworkCallback(),
+ mHandler);
+ }
+
+ private void registerThreadNetwork() {
+ if (mNetworkAgent != null) {
+ return;
+ }
+ NetworkCapabilities netCaps = newNetworkCapabilities();
+ NetworkScore score =
+ new NetworkScore.Builder()
+ .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+ .build();
+ requestThreadNetwork();
+ mNetworkAgent =
+ new NetworkAgent(
+ mContext,
+ mHandlerThread.getLooper(),
+ TAG,
+ netCaps,
+ mLinkProperties,
+ mLocalNetworkConfig,
+ score,
+ new NetworkAgentConfig.Builder().build(),
+ mNetworkProvider) {};
+ mNetworkAgent.register();
+ mNetworkAgent.markConnected();
+ Log.i(TAG, "Registered Thread network");
+ }
+
+ private void unregisterThreadNetwork() {
+ if (mNetworkAgent == null) {
+ // unregisterThreadNetwork can be called every time this device becomes detached or
+ // disabled and the mNetworkAgent may not be created in this cases
+ return;
+ }
+
+ Log.d(TAG, "Unregistering Thread network agent");
+
+ mNetworkAgent.unregister();
+ mNetworkAgent = null;
+ }
+
+ private void updateTunInterfaceAddress(LinkAddress linkAddress, boolean isAdded) {
+ try {
+ if (isAdded) {
+ mTunIfController.addAddress(linkAddress);
+ } else {
+ mTunIfController.removeAddress(linkAddress);
+ }
+ } catch (IOException e) {
+ Log.e(
+ TAG,
+ String.format(
+ "Failed to %s Thread tun interface address %s",
+ (isAdded ? "add" : "remove"), linkAddress),
+ e);
+ }
+ }
+
+ private void updateNetworkLinkProperties(LinkAddress linkAddress, boolean isAdded) {
+ RouteInfo routeInfo =
+ new RouteInfo(
+ new IpPrefix(linkAddress.getAddress(), 64),
+ null,
+ TUN_IF_NAME,
+ RouteInfo.RTN_UNICAST,
+ TunInterfaceController.MTU);
+ if (isAdded) {
+ mLinkProperties.addLinkAddress(linkAddress);
+ mLinkProperties.addRoute(routeInfo);
+ } else {
+ mLinkProperties.removeLinkAddress(linkAddress);
+ mLinkProperties.removeRoute(routeInfo);
+ }
+
+ // The Thread daemon can send link property updates before the networkAgent is
+ // registered
+ if (mNetworkAgent != null) {
+ mNetworkAgent.sendLinkProperties(mLinkProperties);
+ }
+ }
@Override
public int getThreadVersion() {
return THREAD_VERSION_1_3;
}
+
+ @Override
+ public void createRandomizedDataset(
+ String networkName, IActiveOperationalDatasetReceiver receiver) {
+ mHandler.post(
+ () -> {
+ ActiveOperationalDataset dataset =
+ createRandomizedDatasetInternal(
+ networkName,
+ mSupportedChannelMask,
+ Instant.now(),
+ new Random(),
+ new SecureRandom());
+ try {
+ receiver.onSuccess(dataset);
+ } catch (RemoteException e) {
+ // The client is dead, do nothing
+ }
+ });
+ }
+
+ private static ActiveOperationalDataset createRandomizedDatasetInternal(
+ String networkName,
+ int supportedChannelMask,
+ Instant now,
+ Random random,
+ SecureRandom secureRandom) {
+ int panId = random.nextInt(/* bound= */ 0xffff);
+ final byte[] meshLocalPrefix = newRandomBytes(random, LENGTH_MESH_LOCAL_PREFIX_BITS / 8);
+ meshLocalPrefix[0] = MESH_LOCAL_PREFIX_FIRST_BYTE;
+
+ final SparseArray<byte[]> channelMask = new SparseArray<>(1);
+ channelMask.put(CHANNEL_PAGE_24_GHZ, channelMaskToByteArray(supportedChannelMask));
+
+ final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
+
+ return new ActiveOperationalDataset.Builder()
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ now.getEpochSecond() & 0xffffffffffffL, 0, false))
+ .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
+ .setPanId(panId)
+ .setNetworkName(networkName)
+ .setChannel(CHANNEL_PAGE_24_GHZ, selectRandomChannel(supportedChannelMask, random))
+ .setChannelMask(channelMask)
+ .setPskc(newRandomBytes(secureRandom, LENGTH_PSKC))
+ .setNetworkKey(newRandomBytes(secureRandom, LENGTH_NETWORK_KEY))
+ .setMeshLocalPrefix(meshLocalPrefix)
+ .setSecurityPolicy(new SecurityPolicy(DEFAULT_ROTATION_TIME_HOURS, securityFlags))
+ .build();
+ }
+
+ private static byte[] newRandomBytes(Random random, int length) {
+ byte[] result = new byte[length];
+ random.nextBytes(result);
+ return result;
+ }
+
+ private static byte[] channelMaskToByteArray(int channelMask) {
+ // Per Thread spec, a Channel Mask is:
+ // A variable-length bit mask that identifies the channels within the channel page
+ // (1 = selected, 0 = unselected). The channels are represented in most significant bit
+ // order. For example, the most significant bit of the left-most byte indicates channel 0.
+ // If channel 0 and channel 10 are selected, the mask would be: 80 20 00 00. For IEEE
+ // 802.15.4-2006 2.4 GHz PHY, the ChannelMask is 27 bits and MaskLength is 4.
+ //
+ // The pass-in channelMask represents a channel K by (channelMask & (1 << K)), so here
+ // needs to do bit-wise reverse to convert it to the Thread spec format in bytes.
+ channelMask = Integer.reverse(channelMask);
+ return new byte[] {
+ (byte) (channelMask >>> 24),
+ (byte) (channelMask >>> 16),
+ (byte) (channelMask >>> 8),
+ (byte) channelMask
+ };
+ }
+
+ private static int selectRandomChannel(int supportedChannelMask, Random random) {
+ int num = random.nextInt(Integer.bitCount(supportedChannelMask));
+ for (int i = 0; i < 32; i++) {
+ if ((supportedChannelMask & 1) == 1 && (num--) == 0) {
+ return i;
+ }
+ supportedChannelMask >>>= 1;
+ }
+ return -1;
+ }
+
+ private void enforceAllCallingPermissionsGranted(String... permissions) {
+ for (String permission : permissions) {
+ mContext.enforceCallingPermission(
+ permission, "Permission " + permission + " is missing");
+ }
+ }
+
+ @Override
+ public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
+ enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
+ }
+
+ @Override
+ public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
+ enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
+ }
+
+ @Override
+ public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
+ throws RemoteException {
+ enforceAllCallingPermissionsGranted(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
+ }
+
+ @Override
+ public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
+ throws RemoteException {
+ enforceAllCallingPermissionsGranted(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
+ }
+
+ private void checkOnHandlerThread() {
+ if (Looper.myLooper() != mHandlerThread.getLooper()) {
+ Log.wtf(TAG, "Must be on the handler thread!");
+ }
+ }
+
+ private IOtStatusReceiver newOtStatusReceiver(OperationReceiverWrapper receiver) {
+ return new IOtStatusReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ receiver.onSuccess();
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ receiver.onError(otErrorToAndroidError(otError), message);
+ }
+ };
+ }
+
+ @ErrorCode
+ private static int otErrorToAndroidError(int otError) {
+ // See external/openthread/include/openthread/error.h for OT error definition
+ switch (otError) {
+ case OT_ERROR_ABORT:
+ return ERROR_ABORTED;
+ case OT_ERROR_BUSY:
+ return ERROR_BUSY;
+ case OT_ERROR_DETACHED:
+ case OT_ERROR_INVALID_STATE:
+ return ERROR_FAILED_PRECONDITION;
+ case OT_ERROR_NO_BUFS:
+ return ERROR_RESOURCE_EXHAUSTED;
+ case OT_ERROR_PARSE:
+ return ERROR_RESPONSE_BAD_FORMAT;
+ case OT_ERROR_REASSEMBLY_TIMEOUT:
+ case OT_ERROR_RESPONSE_TIMEOUT:
+ return ERROR_TIMEOUT;
+ case OT_ERROR_REJECTED:
+ return ERROR_REJECTED_BY_PEER;
+ case OT_ERROR_UNSUPPORTED_CHANNEL:
+ return ERROR_UNSUPPORTED_CHANNEL;
+ default:
+ return ERROR_INTERNAL_ERROR;
+ }
+ }
+
+ @Override
+ public void join(
+ @NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
+ }
+
+ private void joinInternal(
+ @NonNull ActiveOperationalDataset activeDataset,
+ @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ // The otDaemon.join() will leave first if this device is currently attached
+ getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.join failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ @Override
+ public void scheduleMigration(
+ @NonNull PendingOperationalDataset pendingDataset,
+ @NonNull IOperationReceiver receiver) {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
+ }
+
+ public void scheduleMigrationInternal(
+ @NonNull PendingOperationalDataset pendingDataset,
+ @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon()
+ .scheduleMigration(
+ pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.scheduleMigration failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ @Override
+ public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
+ enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
+ }
+
+ private void leaveInternal(@NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().leave(newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ // Oneway AIDL API should never throw?
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
+ private void enableBorderRouting(String infraIfName) {
+ if (mBorderRouterConfig.isBorderRoutingEnabled
+ && infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
+ return;
+ }
+ Log.i(TAG, "enableBorderRouting on AIL: " + infraIfName);
+ try {
+ mBorderRouterConfig.infraInterfaceName = infraIfName;
+ mBorderRouterConfig.infraInterfaceIcmp6Socket =
+ InfraInterfaceController.createIcmp6Socket(infraIfName);
+ mBorderRouterConfig.isBorderRoutingEnabled = true;
+
+ mOtDaemon.configureBorderRouter(
+ mBorderRouterConfig,
+ new IOtStatusReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ Log.i(TAG, "configure border router successfully");
+ }
+
+ @Override
+ public void onError(int i, String s) {
+ Log.w(
+ TAG,
+ String.format(
+ "failed to configure border router: %d %s", i, s));
+ }
+ });
+ } catch (Exception e) {
+ Log.w(TAG, "enableBorderRouting failed: " + e);
+ }
+ }
+
+ private void handleThreadInterfaceStateChanged(boolean isUp) {
+ try {
+ mTunIfController.setInterfaceUp(isUp);
+ Log.d(TAG, "Thread network interface becomes " + (isUp ? "up" : "down"));
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to handle Thread interface state changes", e);
+ }
+ }
+
+ private void handleDeviceRoleChanged(@DeviceRole int deviceRole) {
+ if (ThreadNetworkController.isAttached(deviceRole)) {
+ Log.d(TAG, "Attached to the Thread network");
+
+ // This is an idempotent method which can be called for multiple times when the device
+ // is already attached (e.g. going from Child to Router)
+ registerThreadNetwork();
+ } else {
+ Log.d(TAG, "Detached from the Thread network");
+
+ // This is an idempotent method which can be called for multiple times when the device
+ // is already detached or stopped
+ unregisterThreadNetwork();
+ }
+ }
+
+ private void handleAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+ checkOnHandlerThread();
+ InetAddress address = addressInfoToInetAddress(addressInfo);
+ if (address.isMulticastAddress()) {
+ Log.i(TAG, "Ignoring multicast address " + address.getHostAddress());
+ return;
+ }
+
+ LinkAddress linkAddress = newLinkAddress(addressInfo);
+ Log.d(TAG, (isAdded ? "Adding" : "Removing") + " address " + linkAddress);
+
+ updateTunInterfaceAddress(linkAddress, isAdded);
+ updateNetworkLinkProperties(linkAddress, isAdded);
+ }
+
+ private boolean isMulticastForwardingEnabled() {
+ return !(mUpstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE
+ && mDownstreamMulticastRoutingConfig.getForwardingMode() == FORWARD_NONE);
+ }
+
+ private void sendLocalNetworkConfig() {
+ if (mNetworkAgent == null) {
+ return;
+ }
+ final LocalNetworkConfig.Builder configBuilder = new LocalNetworkConfig.Builder();
+ LocalNetworkConfig localNetworkConfig =
+ configBuilder
+ .setUpstreamMulticastRoutingConfig(mUpstreamMulticastRoutingConfig)
+ .setDownstreamMulticastRoutingConfig(mDownstreamMulticastRoutingConfig)
+ .setUpstreamSelector(mUpstreamNetworkRequest)
+ .build();
+ mNetworkAgent.sendLocalNetworkConfig(localNetworkConfig);
+ Log.d(
+ TAG,
+ "Sent localNetworkConfig with upstreamConfig "
+ + mUpstreamMulticastRoutingConfig
+ + " downstreamConfig"
+ + mDownstreamMulticastRoutingConfig);
+ }
+
+ private void handleMulticastForwardingStateChanged(boolean isEnabled) {
+ if (isMulticastForwardingEnabled() == isEnabled) {
+ return;
+ }
+ if (isEnabled) {
+ // When multicast forwarding is enabled, setup upstream forwarding to any address
+ // with minimal scope 4
+ // setup downstream forwarding with addresses subscribed from Thread network
+ mUpstreamMulticastRoutingConfig =
+ new MulticastRoutingConfig.Builder(FORWARD_WITH_MIN_SCOPE, 4).build();
+ mDownstreamMulticastRoutingConfig =
+ new MulticastRoutingConfig.Builder(FORWARD_SELECTED).build();
+ } else {
+ // When multicast forwarding is disabled, set both upstream and downstream
+ // forwarding config to FORWARD_NONE.
+ mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
+ }
+ sendLocalNetworkConfig();
+ Log.d(
+ TAG,
+ "Sent updated localNetworkConfig with multicast forwarding "
+ + (isEnabled ? "enabled" : "disabled"));
+ }
+
+ private void handleMulticastForwardingAddressChanged(byte[] addressBytes, boolean isAdded) {
+ Inet6Address address = bytesToInet6Address(addressBytes);
+ MulticastRoutingConfig newDownstreamConfig;
+ MulticastRoutingConfig.Builder builder;
+
+ if (mDownstreamMulticastRoutingConfig.getForwardingMode() !=
+ MulticastRoutingConfig.FORWARD_SELECTED) {
+ Log.e(
+ TAG,
+ "Ignore multicast listening address updates when downstream multicast "
+ + "forwarding mode is not FORWARD_SELECTED");
+ // Don't update the address set if downstream multicast forwarding is disabled.
+ return;
+ }
+ if (isAdded ==
+ mDownstreamMulticastRoutingConfig.getListeningAddresses().contains(address)) {
+ return;
+ }
+
+ builder = new MulticastRoutingConfig.Builder(FORWARD_SELECTED);
+ for (Inet6Address listeningAddress :
+ mDownstreamMulticastRoutingConfig.getListeningAddresses()) {
+ builder.addListeningAddress(listeningAddress);
+ }
+
+ if (isAdded) {
+ builder.addListeningAddress(address);
+ } else {
+ builder.clearListeningAddress(address);
+ }
+
+ newDownstreamConfig = builder.build();
+ if (!newDownstreamConfig.equals(mDownstreamMulticastRoutingConfig)) {
+ Log.d(
+ TAG,
+ "Multicast listening address "
+ + address.getHostAddress()
+ + " is "
+ + (isAdded ? "added" : "removed"));
+ mDownstreamMulticastRoutingConfig = newDownstreamConfig;
+ sendLocalNetworkConfig();
+ }
+ }
+
+ private static final class CallbackMetadata {
+ private static long gId = 0;
+
+ // The unique ID
+ final long id;
+
+ final IBinder.DeathRecipient deathRecipient;
+
+ CallbackMetadata(IBinder.DeathRecipient deathRecipient) {
+ this.id = allocId();
+ this.deathRecipient = deathRecipient;
+ }
+
+ private static long allocId() {
+ if (gId == Long.MAX_VALUE) {
+ gId = 0;
+ }
+ return gId++;
+ }
+ }
+
+ /**
+ * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
+ * mHandlerThread}.
+ */
+ private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
+ private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
+ private final Map<IOperationalDatasetCallback, CallbackMetadata> mOpDatasetCallbacks =
+ new HashMap<>();
+
+ private OtDaemonState mState;
+ private ActiveOperationalDataset mActiveDataset;
+ private PendingOperationalDataset mPendingDataset;
+
+ public void registerStateCallback(IStateCallback callback) {
+ checkOnHandlerThread();
+ if (mStateCallbacks.containsKey(callback)) {
+ throw new IllegalStateException("Registering the same IStateCallback twice");
+ }
+
+ IBinder.DeathRecipient deathRecipient =
+ () -> mHandler.post(() -> unregisterStateCallback(callback));
+ CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ mStateCallbacks.put(callback, callbackMetadata);
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ mStateCallbacks.remove(callback);
+ // This is thrown when the client is dead, do nothing
+ }
+
+ try {
+ getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+ } catch (RemoteException e) {
+ // oneway operation should never fail
+ }
+ }
+
+ public void unregisterStateCallback(IStateCallback callback) {
+ checkOnHandlerThread();
+ if (!mStateCallbacks.containsKey(callback)) {
+ return;
+ }
+ callback.asBinder().unlinkToDeath(mStateCallbacks.remove(callback).deathRecipient, 0);
+ }
+
+ public void registerDatasetCallback(IOperationalDatasetCallback callback) {
+ checkOnHandlerThread();
+ if (mOpDatasetCallbacks.containsKey(callback)) {
+ throw new IllegalStateException(
+ "Registering the same IOperationalDatasetCallback twice");
+ }
+
+ IBinder.DeathRecipient deathRecipient =
+ () -> mHandler.post(() -> unregisterDatasetCallback(callback));
+ CallbackMetadata callbackMetadata = new CallbackMetadata(deathRecipient);
+ mOpDatasetCallbacks.put(callback, callbackMetadata);
+ try {
+ callback.asBinder().linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ mOpDatasetCallbacks.remove(callback);
+ }
+
+ try {
+ getOtDaemon().registerStateCallback(this, callbackMetadata.id);
+ } catch (RemoteException e) {
+ // oneway operation should never fail
+ }
+ }
+
+ public void unregisterDatasetCallback(IOperationalDatasetCallback callback) {
+ checkOnHandlerThread();
+ if (!mOpDatasetCallbacks.containsKey(callback)) {
+ return;
+ }
+ callback.asBinder()
+ .unlinkToDeath(mOpDatasetCallbacks.remove(callback).deathRecipient, 0);
+ }
+
+ public void onOtDaemonDied() {
+ checkOnHandlerThread();
+ if (mState == null) {
+ return;
+ }
+
+ // If this device is already STOPPED or DETACHED, do nothing
+ if (!ThreadNetworkController.isAttached(mState.deviceRole)) {
+ return;
+ }
+
+ // The Thread device role is considered DETACHED when the OT daemon process is dead
+ handleDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+ for (IStateCallback callback : mStateCallbacks.keySet()) {
+ try {
+ callback.onDeviceRoleChanged(DEVICE_ROLE_DETACHED);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ @Override
+ public void onStateChanged(OtDaemonState newState, long listenerId) {
+ mHandler.post(() -> onStateChangedInternal(newState, listenerId));
+ }
+
+ private void onStateChangedInternal(OtDaemonState newState, long listenerId) {
+ checkOnHandlerThread();
+ onInterfaceStateChanged(newState.isInterfaceUp);
+ onDeviceRoleChanged(newState.deviceRole, listenerId);
+ onPartitionIdChanged(newState.partitionId, listenerId);
+ onMulticastForwardingStateChanged(newState.multicastForwardingEnabled);
+ mState = newState;
+
+ ActiveOperationalDataset newActiveDataset;
+ try {
+ if (newState.activeDatasetTlvs.length != 0) {
+ newActiveDataset =
+ ActiveOperationalDataset.fromThreadTlvs(newState.activeDatasetTlvs);
+ } else {
+ newActiveDataset = null;
+ }
+ onActiveOperationalDatasetChanged(newActiveDataset, listenerId);
+ mActiveDataset = newActiveDataset;
+ } catch (IllegalArgumentException e) {
+ // Is unlikely that OT will generate invalid Operational Dataset
+ Log.wtf(TAG, "Invalid Active Operational Dataset from OpenThread", e);
+ }
+
+ PendingOperationalDataset newPendingDataset;
+ try {
+ if (newState.pendingDatasetTlvs.length != 0) {
+ newPendingDataset =
+ PendingOperationalDataset.fromThreadTlvs(newState.pendingDatasetTlvs);
+ } else {
+ newPendingDataset = null;
+ }
+ onPendingOperationalDatasetChanged(newPendingDataset, listenerId);
+ mPendingDataset = newPendingDataset;
+ } catch (IllegalArgumentException e) {
+ // Is unlikely that OT will generate invalid Operational Dataset
+ Log.wtf(TAG, "Invalid Pending Operational Dataset from OpenThread", e);
+ }
+ }
+
+ private void onInterfaceStateChanged(boolean isUp) {
+ checkOnHandlerThread();
+ if (mState == null || mState.isInterfaceUp != isUp) {
+ handleThreadInterfaceStateChanged(isUp);
+ }
+ }
+
+ private void onDeviceRoleChanged(@DeviceRole int deviceRole, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = (mState == null || mState.deviceRole != deviceRole);
+ if (hasChange) {
+ handleDeviceRoleChanged(deviceRole);
+ }
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onDeviceRoleChanged(deviceRole);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onPartitionIdChanged(long partitionId, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = (mState == null || mState.partitionId != partitionId);
+
+ for (var callbackEntry : mStateCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onPartitionIdChanged(partitionId);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = !Objects.equals(mActiveDataset, activeDataset);
+
+ for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onActiveOperationalDatasetChanged(activeDataset);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset, long listenerId) {
+ checkOnHandlerThread();
+ boolean hasChange = !Objects.equals(mPendingDataset, pendingDataset);
+ for (var callbackEntry : mOpDatasetCallbacks.entrySet()) {
+ if (!hasChange && callbackEntry.getValue().id != listenerId) {
+ continue;
+ }
+ try {
+ callbackEntry.getKey().onPendingOperationalDatasetChanged(pendingDataset);
+ } catch (RemoteException ignored) {
+ // do nothing if the client is dead
+ }
+ }
+ }
+
+ private void onMulticastForwardingStateChanged(boolean isEnabled) {
+ checkOnHandlerThread();
+ handleMulticastForwardingStateChanged(isEnabled);
+ }
+
+ @Override
+ public void onAddressChanged(Ipv6AddressInfo addressInfo, boolean isAdded) {
+ mHandler.post(() -> handleAddressChanged(addressInfo, isAdded));
+ }
+
+ @Override
+ public void onMulticastForwardingAddressChanged(byte[] address, boolean isAdded) {
+ mHandler.post(() -> handleMulticastForwardingAddressChanged(address, isAdded));
+ }
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index c6d47df..cc694a1 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,6 +16,7 @@
package com.android.server.thread;
+import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
@@ -29,16 +30,12 @@
* Implementation of the Thread network service. This is the entry point of Android Thread feature.
*/
public class ThreadNetworkService extends IThreadNetworkManager.Stub {
- private final ThreadNetworkControllerService mControllerService;
+ private final Context mContext;
+ @Nullable private ThreadNetworkControllerService mControllerService;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
- this(context, new ThreadNetworkControllerService());
- }
-
- private ThreadNetworkService(
- Context context, ThreadNetworkControllerService controllerService) {
- mControllerService = controllerService;
+ mContext = context;
}
/**
@@ -48,12 +45,16 @@
*/
public void onBootPhase(int phase) {
if (phase == SystemService.PHASE_BOOT_COMPLETED) {
- // TODO: initialize ThreadNetworkManagerService
+ mControllerService = ThreadNetworkControllerService.newInstance(mContext);
+ mControllerService.initialize();
}
}
@Override
public List<IThreadNetworkController> getAllThreadNetworkControllers() {
+ if (mControllerService == null) {
+ return Collections.emptyList();
+ }
return Collections.singletonList(mControllerService);
}
}
diff --git a/thread/service/java/com/android/server/thread/TunInterfaceController.java b/thread/service/java/com/android/server/thread/TunInterfaceController.java
new file mode 100644
index 0000000..7223b2a
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/TunInterfaceController.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.thread;
+
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.util.SocketUtils;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/** Controller for virtual/tunnel network interfaces. */
+public class TunInterfaceController {
+ private static final String TAG = "TunIfController";
+ private static final long INFINITE_LIFETIME = 0xffffffffL;
+ static final int MTU = 1280;
+
+ static {
+ System.loadLibrary("service-thread-jni");
+ }
+
+ private final String mIfName;
+ private ParcelFileDescriptor mParcelTunFd;
+ private FileDescriptor mNetlinkSocket;
+ private static int sNetlinkSeqNo = 0;
+
+ /** Creates a new {@link TunInterfaceController} instance for given interface. */
+ public TunInterfaceController(String interfaceName) {
+ this.mIfName = interfaceName;
+ }
+
+ /**
+ * Creates the tunnel interface.
+ *
+ * @throws IOException if failed to create the interface
+ */
+ public void createTunInterface() throws IOException {
+ mParcelTunFd = ParcelFileDescriptor.adoptFd(nativeCreateTunInterface(mIfName, MTU));
+ try {
+ mNetlinkSocket = NetlinkUtils.netlinkSocketForProto(OsConstants.NETLINK_ROUTE);
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to create netlink socket", e);
+ }
+ }
+
+ public void destroyTunInterface() {
+ try {
+ mParcelTunFd.close();
+ SocketUtils.closeSocket(mNetlinkSocket);
+ } catch (IOException e) {
+ // Should never fail
+ }
+ mParcelTunFd = null;
+ mNetlinkSocket = null;
+ }
+
+ /** Returns the FD of the tunnel interface. */
+ @Nullable
+ public ParcelFileDescriptor getTunFd() {
+ return mParcelTunFd;
+ }
+
+ private native int nativeCreateTunInterface(String interfaceName, int mtu) throws IOException;
+
+ /** Sets the interface up or down according to {@code isUp}. */
+ public void setInterfaceUp(boolean isUp) throws IOException {
+ nativeSetInterfaceUp(mIfName, isUp);
+ }
+
+ private native void nativeSetInterfaceUp(String interfaceName, boolean isUp) throws IOException;
+
+ /** Adds a new address to the interface. */
+ public void addAddress(LinkAddress address) throws IOException {
+ Log.d(TAG, "Adding address " + address + " with flags: " + address.getFlags());
+
+ long validLifetimeSeconds;
+ long preferredLifetimeSeconds;
+
+ if (address.getDeprecationTime() == LinkAddress.LIFETIME_PERMANENT
+ || address.getDeprecationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+ validLifetimeSeconds = INFINITE_LIFETIME;
+ } else {
+ validLifetimeSeconds =
+ Math.max(
+ (address.getDeprecationTime() - SystemClock.elapsedRealtime()) / 1000L,
+ 0L);
+ }
+
+ if (address.getExpirationTime() == LinkAddress.LIFETIME_PERMANENT
+ || address.getExpirationTime() == LinkAddress.LIFETIME_UNKNOWN) {
+ preferredLifetimeSeconds = INFINITE_LIFETIME;
+ } else {
+ preferredLifetimeSeconds =
+ Math.max(
+ (address.getExpirationTime() - SystemClock.elapsedRealtime()) / 1000L,
+ 0L);
+ }
+
+ byte[] message =
+ RtNetlinkAddressMessage.newRtmNewAddressMessage(
+ sNetlinkSeqNo,
+ address.getAddress(),
+ (short) address.getPrefixLength(),
+ address.getFlags(),
+ (byte) address.getScope(),
+ Os.if_nametoindex(mIfName),
+ validLifetimeSeconds,
+ preferredLifetimeSeconds);
+ try {
+ Os.write(mNetlinkSocket, message, 0, message.length);
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to send netlink message", e);
+ }
+ }
+
+ /** Removes an address from the interface. */
+ public void removeAddress(LinkAddress address) throws IOException {
+ // TODO(b/263222068): remove address with netlink
+ }
+}
diff --git a/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
new file mode 100644
index 0000000..5d24eab
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_InfraInterfaceController.cpp
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "jniThreadInfra"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <netdb.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <private/android_filesystem_config.h>
+#include <signal.h>
+#include <spawn.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint
+com_android_server_thread_InfraInterfaceController_createIcmp6Socket(JNIEnv *env, jobject clazz,
+ jstring interfaceName) {
+ ScopedUtfChars ifName(env, interfaceName);
+
+ struct icmp6_filter filter;
+ constexpr int kEnable = 1;
+ constexpr int kIpv6ChecksumOffset = 2;
+ constexpr int kHopLimit = 255;
+
+ // Initializes the ICMPv6 socket.
+ int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ if (sock == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to create the socket (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ // Only accept Router Advertisements, Router Solicitations and Neighbor
+ // Advertisements.
+ ICMP6_FILTER_SETBLOCKALL(&filter);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filter);
+ ICMP6_FILTER_SETPASS(ND_NEIGHBOR_ADVERT, &filter);
+
+ if (setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt ICMP6_FILTER (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ // We want a source address and interface index.
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &kEnable, sizeof(kEnable)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVPKTINFO (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &kIpv6ChecksumOffset,
+ sizeof(kIpv6ChecksumOffset)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_CHECKSUM (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ // We need to be able to reject RAs arriving from off-link.
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &kEnable, sizeof(kEnable)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_RECVHOPLIMIT (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt IPV6_UNICAST_HOPS (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &kHopLimit, sizeof(kHopLimit)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "failed to create the setsockopt IPV6_MULTICAST_HOPS (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifName.c_str(), strlen(ifName.c_str()))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "failed to setsockopt SO_BINDTODEVICE (%s)",
+ strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ return sock;
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreateIcmp6Socket", "(Ljava/lang/String;)I",
+ (void *)com_android_server_thread_InfraInterfaceController_createIcmp6Socket},
+};
+
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv *env) {
+ return jniRegisterNativeMethods(env, "com/android/server/thread/InfraInterfaceController",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
new file mode 100644
index 0000000..c56bc0b
--- /dev/null
+++ b/thread/service/jni/com_android_server_thread_TunInterfaceController.cpp
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "jniThreadTun"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/if_arp.h>
+#include <linux/if_tun.h>
+#include <linux/ioctl.h>
+#include <log/log.h>
+#include <net/if.h>
+#include <spawn.h>
+#include <sys/wait.h>
+#include <string>
+
+#include <private/android_filesystem_config.h>
+
+#include "jni.h"
+#include "nativehelper/JNIHelp.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+namespace android {
+static jint com_android_server_thread_TunInterfaceController_createTunInterface(
+ JNIEnv* env, jobject clazz, jstring interfaceName, jint mtu) {
+ ScopedUtfChars ifName(env, interfaceName);
+
+ int fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (fd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "open tun device failed (%s)",
+ strerror(errno));
+ return -1;
+ }
+
+ struct ifreq ifr = {
+ .ifr_flags = IFF_TUN | IFF_NO_PI | static_cast<short>(IFF_TUN_EXCL),
+ };
+ strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+ if (ioctl(fd, TUNSETIFF, &ifr, sizeof(ifr)) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(TUNSETIFF) failed (%s)",
+ strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+ if (inet6 == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+ strerror(errno));
+ close(fd);
+ return -1;
+ }
+ ifr.ifr_mtu = mtu;
+ if (ioctl(inet6, SIOCSIFMTU, &ifr) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFMTU) failed (%s)",
+ strerror(errno));
+ close(fd);
+ close(inet6);
+ return -1;
+ }
+
+ close(inet6);
+ return fd;
+}
+
+static void com_android_server_thread_TunInterfaceController_setInterfaceUp(
+ JNIEnv* env, jobject clazz, jstring interfaceName, jboolean isUp) {
+ struct ifreq ifr;
+ ScopedUtfChars ifName(env, interfaceName);
+
+ ifr.ifr_flags = isUp ? IFF_UP : 0;
+ strlcpy(ifr.ifr_name, ifName.c_str(), sizeof(ifr.ifr_name));
+
+ int inet6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, IPPROTO_IP);
+ if (inet6 == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "create inet6 socket failed (%s)",
+ strerror(errno));
+ }
+
+ if (ioctl(inet6, SIOCSIFFLAGS, &ifr) != 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "ioctl(SIOCSIFFLAGS) failed (%s)",
+ strerror(errno));
+ }
+
+ close(inet6);
+}
+
+/*
+ * JNI registration.
+ */
+
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeCreateTunInterface",
+ "(Ljava/lang/String;I)I",
+ (void*)com_android_server_thread_TunInterfaceController_createTunInterface},
+ {"nativeSetInterfaceUp",
+ "(Ljava/lang/String;Z)V",
+ (void*)com_android_server_thread_TunInterfaceController_setInterfaceUp},
+};
+
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/server/thread/TunInterfaceController",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/thread/service/jni/onload.cpp b/thread/service/jni/onload.cpp
new file mode 100644
index 0000000..66add74
--- /dev/null
+++ b/thread/service/jni/onload.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "jni.h"
+#include "utils/Log.h"
+
+namespace android {
+int register_com_android_server_thread_TunInterfaceController(JNIEnv* env);
+int register_com_android_server_thread_InfraInterfaceController(JNIEnv* env);
+}
+
+using namespace android;
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
+ JNIEnv* env = NULL;
+
+ if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+ ALOGE("GetEnv failed!");
+ return -1;
+ }
+ ALOG_ASSERT(env != NULL, "Could not retrieve the env!");
+
+ register_com_android_server_thread_TunInterfaceController(env);
+ register_com_android_server_thread_InfraInterfaceController(env);
+ return JNI_VERSION_1_4;
+}
diff --git a/thread/service/proguard.flags b/thread/service/proguard.flags
new file mode 100644
index 0000000..5028982
--- /dev/null
+++ b/thread/service/proguard.flags
@@ -0,0 +1,4 @@
+# Ensure the callback methods are not stripped
+-keepclassmembers class **.ThreadNetworkControllerService$ThreadNetworkCallback {
+ *;
+}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
index 96056c6..3cf31e5 100644
--- a/thread/tests/cts/Android.bp
+++ b/thread/tests/cts/Android.bp
@@ -37,12 +37,15 @@
"androidx.test.ext.junit",
"compatibility-device-util-axt",
"ctstestrunner-axt",
+ "guava",
+ "guava-android-testlib",
"net-tests-utils",
- "truth-prebuilt",
+ "truth",
],
libs: [
"android.test.base",
"android.test.runner",
+ "framework-connectivity-module-api-stubs-including-flagged"
],
// Test coverage system runs on different devices. Need to
// compile for all architectures.
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
index 5ba605f..ffc181c 100644
--- a/thread/tests/cts/AndroidTest.xml
+++ b/thread/tests/cts/AndroidTest.xml
@@ -47,5 +47,7 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.thread.cts" />
+ <!-- Ignores tests introduced by guava-android-testlib -->
+ <option name="exclude-annotation" value="org.junit.Ignore"/>
</test>
</configuration>
diff --git a/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..0e76930
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ActiveOperationalDatasetTest.java
@@ -0,0 +1,730 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static android.net.thread.ActiveOperationalDataset.CHANNEL_PAGE_24_GHZ;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+/** CTS tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ActiveOperationalDatasetTest {
+ private static final int TYPE_ACTIVE_TIMESTAMP = 14;
+ private static final int TYPE_CHANNEL = 0;
+ private static final int TYPE_CHANNEL_MASK = 53;
+ private static final int TYPE_EXTENDED_PAN_ID = 2;
+ private static final int TYPE_MESH_LOCAL_PREFIX = 7;
+ private static final int TYPE_NETWORK_KEY = 5;
+ private static final int TYPE_NETWORK_NAME = 3;
+ private static final int TYPE_PAN_ID = 1;
+ private static final int TYPE_PSKC = 4;
+ private static final int TYPE_SECURITY_POLICY = 12;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] VALID_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
+
+ private static byte[] removeTlv(byte[] dataset, int type) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream(dataset.length);
+ int i = 0;
+ while (i < dataset.length) {
+ int ty = dataset[i++] & 0xff;
+ byte length = dataset[i++];
+ if (ty != type) {
+ byte[] value = Arrays.copyOfRange(dataset, i, i + length);
+ os.write(ty);
+ os.write(length);
+ os.writeBytes(value);
+ }
+ i += length;
+ }
+ return os.toByteArray();
+ }
+
+ private static byte[] addTlv(byte[] dataset, String tlvHex) {
+ return Bytes.concat(dataset, base16().decode(tlvHex));
+ }
+
+ private static byte[] replaceTlv(byte[] dataset, int type, String newTlvHex) {
+ return addTlv(removeTlv(dataset, type), newTlvHex);
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() {
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
+
+ assertParcelingIsLossless(dataset);
+ }
+
+ @Test
+ public void fromThreadTlvs_tooLongTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = new byte[255];
+ invalidTlv[0] = (byte) 0xff;
+
+ // This is invalid because the TLV has max total length of 254 bytes and the value length
+ // can't exceeds 252 ( = 254 - 1 - 1)
+ invalidTlv[1] = (byte) 253;
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkKeyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_NETWORK_KEY, "05080000000000000000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noNetworkKeyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_NETWORK_KEY);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidActiveTimestampTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_ACTIVE_TIMESTAMP, "0E0700000000010000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noActiveTimestampTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_ACTIVE_TIMESTAMP);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkNameTlv_emptyName_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_NETWORK_NAME, "0300");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidNetworkNameTlv_tooLongName_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(
+ VALID_DATASET_TLVS,
+ TYPE_NETWORK_NAME,
+ "03114142434445464748494A4B4C4D4E4F5051");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noNetworkNameTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_NETWORK_NAME);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidChannelTlv_channelMissing_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000100");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_undefinedChannelPage_success() {
+ byte[] datasetTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "0003010020");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(datasetTlv);
+
+ assertThat(dataset.getChannelPage()).isEqualTo(0x01);
+ assertThat(dataset.getChannel()).isEqualTo(0x20);
+ }
+
+ @Test
+ public void fromThreadTlvs_invalid2P4GhzChannel_throwsIllegalArgument() {
+ byte[] invalidTlv1 = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000300000A");
+ byte[] invalidTlv2 = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "000300001B");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv1));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv2));
+ }
+
+ @Test
+ public void fromThreadTlvs_valid2P4GhzChannelTlv_success() {
+ byte[] validTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL, "0003000010");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validTlv);
+
+ assertThat(dataset.getChannel()).isEqualTo(16);
+ }
+
+ @Test
+ public void fromThreadTlvs_noChannelTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_CHANNEL);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_prematureEndOfChannelMaskEntry_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "350100");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_inconsistentChannelMaskLength_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "3506000500010000");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_unsupportedChannelMaskLength_success() {
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(
+ replaceTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK, "350700050001000000"));
+
+ SparseArray<byte[]> channelMask = dataset.getChannelMask();
+ assertThat(channelMask.size()).isEqualTo(1);
+ assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(new byte[] {0x00, 0x01, 0x00, 0x00, 0x00});
+ }
+
+ @Test
+ public void fromThreadTlvs_noChannelMaskTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_CHANNEL_MASK);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_PAN_ID, "010101");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_PAN_ID);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidExtendedPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_EXTENDED_PAN_ID, "020700010203040506");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noExtendedPanIdTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_EXTENDED_PAN_ID);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidPskcTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_PSKC, "0411000102030405060708090A0B0C0D0E0F10");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noPskcTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_PSKC);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_invalidMeshLocalPrefixTlv_throwsIllegalArgument() {
+ byte[] invalidTlv =
+ replaceTlv(VALID_DATASET_TLVS, TYPE_MESH_LOCAL_PREFIX, "0709FD0001020304050607");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noMeshLocalPrefixTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_MESH_LOCAL_PREFIX);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_tooShortSecurityPolicyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = replaceTlv(VALID_DATASET_TLVS, TYPE_SECURITY_POLICY, "0C0101");
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_noSecurityPolicyTlv_throwsIllegalArgument() {
+ byte[] invalidTlv = removeTlv(VALID_DATASET_TLVS, TYPE_SECURITY_POLICY);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(invalidTlv));
+ }
+
+ @Test
+ public void fromThreadTlvs_lengthAndDataMissing_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {(byte) 0x00}));
+ }
+
+ @Test
+ public void fromThreadTlvs_prematureEndOfData_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ActiveOperationalDataset.fromThreadTlvs(new byte[] {0x00, 0x03, 0x00, 0x00}));
+ }
+
+ @Test
+ public void fromThreadTlvs_validFullDataset_success() {
+ // A valid Thread active operational dataset:
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ byte[] validDatasetTlv =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ ActiveOperationalDataset dataset = ActiveOperationalDataset.fromThreadTlvs(validDatasetTlv);
+
+ assertThat(dataset.getNetworkKey())
+ .isEqualTo(base16().decode("F26B3153760F519A63BAFDDFFC80D2AF"));
+ assertThat(dataset.getPanId()).isEqualTo(0xd9a0);
+ assertThat(dataset.getExtendedPanId()).isEqualTo(base16().decode("ACC214689BC40BDF"));
+ assertThat(dataset.getChannel()).isEqualTo(19);
+ assertThat(dataset.getNetworkName()).isEqualTo("OpenThread-d9a0");
+ assertThat(dataset.getPskc())
+ .isEqualTo(base16().decode("A245479C836D551B9CA557F7B9D351B4"));
+ assertThat(dataset.getActiveTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ SparseArray<byte[]> channelMask = dataset.getChannelMask();
+ assertThat(channelMask.size()).isEqualTo(1);
+ assertThat(channelMask.get(CHANNEL_PAGE_24_GHZ))
+ .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+ assertThat(dataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd64:db12:25f4:7e0b::/64"));
+ assertThat(dataset.getSecurityPolicy())
+ .isEqualTo(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+ final byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET_TLVS, "AA01FFBB020102");
+
+ ActiveOperationalDataset dataset =
+ ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+
+ byte[] newDatasetTlvs = dataset.toThreadTlvs();
+ String newDatasetTlvsHex = base16().encode(newDatasetTlvs);
+ assertThat(newDatasetTlvs.length).isEqualTo(datasetWithUnknownTlvs.length);
+ assertThat(newDatasetTlvsHex).contains("AA01FF");
+ assertThat(newDatasetTlvsHex).contains("BB020102");
+ }
+
+ @Test
+ public void toThreadTlvs_conversionIsLossLess() {
+ ActiveOperationalDataset dataset1 = DEFAULT_DATASET;
+
+ ActiveOperationalDataset dataset2 =
+ ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+
+ @Test
+ public void builder_buildWithdefaultValues_throwsIllegalState() {
+ assertThrows(IllegalStateException.class, () -> new Builder().build());
+ }
+
+ @Test
+ public void builder_setValidNetworkKey_success() {
+ final byte[] networkKey =
+ new byte[] {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
+ 0x0d, 0x0e, 0x0f
+ };
+
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setNetworkKey(networkKey).build();
+
+ assertThat(dataset.getNetworkKey()).isEqualTo(networkKey);
+ }
+
+ @Test
+ public void builder_setInvalidNetworkKey_throwsIllegalArgument() {
+ byte[] invalidNetworkKey = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+ Builder builder = new Builder();
+
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setNetworkKey(invalidNetworkKey));
+ }
+
+ @Test
+ public void builder_setValidExtendedPanId_success() {
+ byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
+
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setExtendedPanId(extendedPanId).build();
+
+ assertThat(dataset.getExtendedPanId()).isEqualTo(extendedPanId);
+ }
+
+ @Test
+ public void builder_setInvalidExtendedPanId_throwsIllegalArgument() {
+ byte[] extendedPanId = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+ Builder builder = new Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setExtendedPanId(extendedPanId));
+ }
+
+ @Test
+ public void builder_setValidPanId_success() {
+ ActiveOperationalDataset dataset = new Builder(DEFAULT_DATASET).setPanId(0xfffe).build();
+
+ assertThat(dataset.getPanId()).isEqualTo(0xfffe);
+ }
+
+ @Test
+ public void builder_setInvalidPanId_throwsIllegalArgument() {
+ Builder builder = new Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setPanId(0xffff));
+ }
+
+ @Test
+ public void builder_setInvalidChannel_throwsIllegalArgument() {
+ Builder builder = new Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 0));
+ assertThrows(IllegalArgumentException.class, () -> builder.setChannel(0, 27));
+ }
+
+ @Test
+ public void builder_setValid2P4GhzChannel_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setChannel(CHANNEL_PAGE_24_GHZ, 16).build();
+
+ assertThat(dataset.getChannel()).isEqualTo(16);
+ assertThat(dataset.getChannelPage()).isEqualTo(CHANNEL_PAGE_24_GHZ);
+ }
+
+ @Test
+ public void builder_setValidNetworkName_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setNetworkName("ot-network").build();
+
+ assertThat(dataset.getNetworkName()).isEqualTo("ot-network");
+ }
+
+ @Test
+ public void builder_setEmptyNetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName(""));
+ }
+
+ @Test
+ public void builder_setTooLongNetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder();
+
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setNetworkName("openthread-network"));
+ }
+
+ @Test
+ public void builder_setTooLongUtf8NetworkName_throwsIllegalArgument() {
+ Builder builder = new Builder();
+
+ // UTF-8 encoded length of "我的线程网络" is 18 bytes which exceeds the max length
+ assertThrows(IllegalArgumentException.class, () -> builder.setNetworkName("我的线程网络"));
+ }
+
+ @Test
+ public void builder_setValidUtf8NetworkName_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setNetworkName("我的网络").build();
+
+ assertThat(dataset.getNetworkName()).isEqualTo("我的网络");
+ }
+
+ @Test
+ public void builder_setValidPskc_success() {
+ byte[] pskc = base16().decode("A245479C836D551B9CA557F7B9D351B4");
+
+ ActiveOperationalDataset dataset = new Builder(DEFAULT_DATASET).setPskc(pskc).build();
+
+ assertThat(dataset.getPskc()).isEqualTo(pskc);
+ }
+
+ @Test
+ public void builder_setTooLongPskc_throwsIllegalArgument() {
+ byte[] tooLongPskc = base16().decode("A245479C836D551B9CA557F7B9D351B400");
+ Builder builder = new Builder();
+
+ assertThrows(IllegalArgumentException.class, () -> builder.setPskc(tooLongPskc));
+ }
+
+ @Test
+ public void builder_setValidChannelMask_success() {
+ SparseArray<byte[]> channelMask = new SparseArray<byte[]>(1);
+ channelMask.put(0, new byte[] {0x00, 0x00, 0x01, 0x00});
+
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setChannelMask(channelMask).build();
+
+ SparseArray<byte[]> resultChannelMask = dataset.getChannelMask();
+ assertThat(resultChannelMask.size()).isEqualTo(1);
+ assertThat(resultChannelMask.get(0)).isEqualTo(new byte[] {0x00, 0x00, 0x01, 0x00});
+ }
+
+ @Test
+ public void builder_setEmptyChannelMask_throwsIllegalArgument() {
+ Builder builder = new Builder();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setChannelMask(new SparseArray<byte[]>()));
+ }
+
+ @Test
+ public void builder_setValidActiveTimestamp_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET)
+ .setActiveTimestamp(
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 1,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ true))
+ .build();
+
+ assertThat(dataset.getActiveTimestamp().getSeconds()).isEqualTo(1);
+ assertThat(dataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+ assertThat(dataset.getActiveTimestamp().isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void builder_wrongMeshLocalPrefixLength_throwsIllegalArguments() {
+ Builder builder = new Builder();
+
+ // The Mesh-Local Prefix length must be 64 bits
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/32")));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fd00::/96")));
+
+ // The Mesh-Local Prefix must start with 0xfd
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+ }
+
+ @Test
+ public void builder_meshLocalPrefixNotStartWith0xfd_throwsIllegalArguments() {
+ Builder builder = new Builder();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMeshLocalPrefix(new IpPrefix("fc00::/64")));
+ }
+
+ @Test
+ public void builder_setValidMeshLocalPrefix_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET).setMeshLocalPrefix(new IpPrefix("fd00::/64")).build();
+
+ assertThat(dataset.getMeshLocalPrefix()).isEqualTo(new IpPrefix("fd00::/64"));
+ }
+
+ @Test
+ public void builder_setValid1P2SecurityPolicy_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET)
+ .setSecurityPolicy(
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+
+ assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(dataset.getSecurityPolicy().getFlags())
+ .isEqualTo(new byte[] {(byte) 0xff, (byte) 0xf8});
+ }
+
+ @Test
+ public void builder_setValid1P1SecurityPolicy_success() {
+ ActiveOperationalDataset dataset =
+ new Builder(DEFAULT_DATASET)
+ .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff}))
+ .build();
+
+ assertThat(dataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(dataset.getSecurityPolicy().getFlags()).isEqualTo(new byte[] {(byte) 0xff});
+ }
+
+ @Test
+ public void securityPolicy_invalidRotationTime_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new SecurityPolicy(0, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new SecurityPolicy(0x1ffff, new byte[] {(byte) 0xff, (byte) 0xf8}));
+ }
+
+ @Test
+ public void securityPolicy_emptyFlags_throwsIllegalArguments() {
+ assertThrows(IllegalArgumentException.class, () -> new SecurityPolicy(672, new byte[] {}));
+ }
+
+ @Test
+ public void securityPolicy_tooLongFlags_success() {
+ SecurityPolicy securityPolicy =
+ new SecurityPolicy(672, new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+
+ assertThat(securityPolicy.getFlags()).isEqualTo(new byte[] {0, 1, 2, 3, 4, 5, 6, 7});
+ }
+
+ @Test
+ public void securityPolicy_equals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}),
+ new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .addEqualityGroup(
+ new SecurityPolicy(1, new byte[] {(byte) 0xff}),
+ new SecurityPolicy(1, new byte[] {(byte) 0xff}))
+ .addEqualityGroup(
+ new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}),
+ new SecurityPolicy(1, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .testEquals();
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..9be3d56
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/OperationalDatasetTimestampTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.OperationalDatasetTimestamp;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+/** Tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+ @Test
+ public void fromInstant_tooLargeInstant_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ OperationalDatasetTimestamp.fromInstant(
+ Instant.ofEpochSecond(0xffffffffffffL + 1L)));
+ }
+
+ @Test
+ public void fromInstant_ticksIsRounded() {
+ Instant instant = Instant.ofEpochSecond(100L);
+
+ // 32767.5 / 32768 * 1000000000 = 999984741.2109375 and given the `ticks` is rounded, so
+ // the `ticks` should be 32767 for 999984741 and 0 (carried over to seconds) for 999984742.
+ OperationalDatasetTimestamp timestampTicks32767 =
+ OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984741));
+ OperationalDatasetTimestamp timestampTicks0 =
+ OperationalDatasetTimestamp.fromInstant(instant.plusNanos(999984742));
+
+ assertThat(timestampTicks32767.getSeconds()).isEqualTo(100L);
+ assertThat(timestampTicks0.getSeconds()).isEqualTo(101L);
+ assertThat(timestampTicks32767.getTicks()).isEqualTo(32767);
+ assertThat(timestampTicks0.getTicks()).isEqualTo(0);
+ assertThat(timestampTicks32767.isAuthoritativeSource()).isTrue();
+ assertThat(timestampTicks0.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void toInstant_nanosIsRounded() {
+ // 32767 / 32768 * 1000000000 = 999969482.421875
+ assertThat(new OperationalDatasetTimestamp(100L, 32767, false).toInstant().getNano())
+ .isEqualTo(999969482);
+
+ // 32766 / 32768 * 1000000000 = 999938964.84375
+ assertThat(new OperationalDatasetTimestamp(100L, 32766, false).toInstant().getNano())
+ .isEqualTo(999938965);
+ }
+
+ @Test
+ public void toInstant_onlyAuthoritativeSourceDiscarded() {
+ OperationalDatasetTimestamp timestamp1 =
+ new OperationalDatasetTimestamp(100L, 0x7fff, false);
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromInstant(timestamp1.toInstant());
+
+ assertThat(timestamp2.getSeconds()).isEqualTo(100L);
+ assertThat(timestamp2.getTicks()).isEqualTo(0x7fff);
+ assertThat(timestamp2.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void constructor_tooLargeSeconds_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 0x0001112233445566L,
+ /* ticks= */ 0,
+ /* isAuthoritativeSource= */ true));
+ }
+
+ @Test
+ public void constructor_tooLargeTicks_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new OperationalDatasetTimestamp(
+ /* seconds= */ 0x01L,
+ /* ticks= */ 0x8000,
+ /* isAuthoritativeSource= */ true));
+ }
+
+ @Test
+ public void equalityTests() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(100, 100, false),
+ new OperationalDatasetTimestamp(100, 100, false))
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(0, 0, false),
+ new OperationalDatasetTimestamp(0, 0, false))
+ .addEqualityGroup(
+ new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true),
+ new OperationalDatasetTimestamp(0xffffffffffffL, 0x7fff, true))
+ .testEquals();
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
new file mode 100644
index 0000000..0bb18ce
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/PendingOperationalDatasetTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread.cts;
+
+import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.IpPrefix;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.time.Duration;
+
+/** Tests for {@link PendingOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class PendingOperationalDatasetTest {
+ private static ActiveOperationalDataset createActiveDataset() throws Exception {
+ SparseArray<byte[]> channelMask = new SparseArray<>(1);
+ channelMask.put(0, new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+
+ return new ActiveOperationalDataset.Builder()
+ .setActiveTimestamp(new OperationalDatasetTimestamp(100, 10, false))
+ .setExtendedPanId(new byte[] {0, 1, 2, 3, 4, 5, 6, 7})
+ .setPanId(12345)
+ .setNetworkName("defaultNet")
+ .setChannel(0, 18)
+ .setChannelMask(channelMask)
+ .setPskc(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15})
+ .setNetworkKey(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
+ .setMeshLocalPrefix(new IpPrefix(InetAddress.getByName("fd00::1"), 64))
+ .setSecurityPolicy(new SecurityPolicy(672, new byte[] {(byte) 0xff, (byte) 0xf8}))
+ .build();
+ }
+
+ @Test
+ public void parcelable_parcelingIsLossLess() throws Exception {
+ PendingOperationalDataset dataset =
+ new PendingOperationalDataset(
+ createActiveDataset(),
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ assertParcelingIsLossless(dataset);
+ }
+
+ @Test
+ public void equalityTests() throws Exception {
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(createActiveDataset())
+ .setNetworkName("net1")
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(createActiveDataset())
+ .setNetworkName("net2")
+ .build();
+
+ new EqualsTester()
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(31536000, 100, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(0)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(0)))
+ .addEqualityGroup(
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(100)),
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(15768000, 0, false),
+ Duration.ofMillis(100)))
+ .testEquals();
+ }
+
+ @Test
+ public void constructor_correctValuesAreSet() throws Exception {
+ final ActiveOperationalDataset activeDataset = createActiveDataset();
+ PendingOperationalDataset dataset =
+ new PendingOperationalDataset(
+ activeDataset,
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(activeDataset);
+ assertThat(dataset.getPendingTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(31536000, 200, false));
+ assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofHours(100));
+ }
+
+ @Test
+ public void fromThreadTlvs_openthreadTlvs_success() {
+ // An example Pending Operational Dataset which is generated with OpenThread CLI:
+ // Pending Timestamp: 2
+ // Active Timestamp: 1
+ // Channel: 26
+ // Channel Mask: 0x07fff800
+ // Delay: 46354
+ // Ext PAN ID: a74182f4d3f4de41
+ // Mesh Local Prefix: fd46:c1b9:e159:5574::/64
+ // Network Key: ed916e454d96fd00184f10a6f5c9e1d3
+ // Network Name: OpenThread-bff8
+ // PAN ID: 0xbff8
+ // PSKc: 264f78414adc683191863d968f72d1b7
+ // Security Policy: 672 onrc
+ final byte[] OPENTHREAD_PENDING_DATASET_TLVS =
+ base16().lowerCase()
+ .decode(
+ "0e0800000000000100003308000000000002000034040000b51200030000"
+ + "1a35060004001fffe00208a74182f4d3f4de410708fd46c1b9"
+ + "e15955740510ed916e454d96fd00184f10a6f5c9e1d3030f4f"
+ + "70656e5468726561642d626666380102bff80410264f78414a"
+ + "dc683191863d968f72d1b70c0402a0f7f8");
+
+ PendingOperationalDataset pendingDataset =
+ PendingOperationalDataset.fromThreadTlvs(OPENTHREAD_PENDING_DATASET_TLVS);
+
+ ActiveOperationalDataset activeDataset = pendingDataset.getActiveOperationalDataset();
+ assertThat(pendingDataset.getPendingTimestamp().getSeconds()).isEqualTo(2L);
+ assertThat(activeDataset.getActiveTimestamp().getSeconds()).isEqualTo(1L);
+ assertThat(activeDataset.getChannel()).isEqualTo(26);
+ assertThat(activeDataset.getChannelMask().get(0))
+ .isEqualTo(new byte[] {0x00, 0x1f, (byte) 0xff, (byte) 0xe0});
+ assertThat(pendingDataset.getDelayTimer().toMillis()).isEqualTo(46354);
+ assertThat(activeDataset.getExtendedPanId())
+ .isEqualTo(base16().lowerCase().decode("a74182f4d3f4de41"));
+ assertThat(activeDataset.getMeshLocalPrefix())
+ .isEqualTo(new IpPrefix("fd46:c1b9:e159:5574::/64"));
+ assertThat(activeDataset.getNetworkKey())
+ .isEqualTo(base16().lowerCase().decode("ed916e454d96fd00184f10a6f5c9e1d3"));
+ assertThat(activeDataset.getNetworkName()).isEqualTo("OpenThread-bff8");
+ assertThat(activeDataset.getPanId()).isEqualTo(0xbff8);
+ assertThat(activeDataset.getPskc())
+ .isEqualTo(base16().lowerCase().decode("264f78414adc683191863d968f72d1b7"));
+ assertThat(activeDataset.getSecurityPolicy().getRotationTimeHours()).isEqualTo(672);
+ assertThat(activeDataset.getSecurityPolicy().getFlags())
+ .isEqualTo(new byte[] {(byte) 0xf7, (byte) 0xf8});
+ }
+
+ @Test
+ public void fromThreadTlvs_completePendingDatasetTlvs_success() throws Exception {
+ final ActiveOperationalDataset activeDataset = createActiveDataset();
+
+ // Type Length Value
+ // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+ // 0x34 0x04 0x0000012C (Delay Timer TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs =
+ base16().decode("3308000000000001000034040000012C");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(pendingTimestampAndDelayTimerTlvs, activeDataset.toThreadTlvs());
+
+ PendingOperationalDataset dataset =
+ PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs);
+
+ assertThat(dataset.getActiveOperationalDataset()).isEqualTo(activeDataset);
+ assertThat(dataset.getPendingTimestamp())
+ .isEqualTo(new OperationalDatasetTimestamp(1, 0, false));
+ assertThat(dataset.getDelayTimer()).isEqualTo(Duration.ofMillis(300));
+ }
+
+ @Test
+ public void fromThreadTlvs_PendingTimestampTlvIsMissing_throwsIllegalArgument()
+ throws Exception {
+ // Type Length Value
+ // 0x34 0x04 0x00000064 (Delay Timer TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("34040000012C");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, createActiveDataset().toThreadTlvs());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_delayTimerTlvIsMissing_throwsIllegalArgument() throws Exception {
+ // Type Length Value
+ // 0x33 0x08 0x0000000000010000 (Pending Timestamp TLV)
+ final byte[] pendingTimestampAndDelayTimerTlvs = base16().decode("33080000000000010000");
+ final byte[] pendingDatasetTlvs =
+ Bytes.concat(
+ pendingTimestampAndDelayTimerTlvs, createActiveDataset().toThreadTlvs());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(pendingDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_activeDatasetTlvs_throwsIllegalArgument() throws Exception {
+ final byte[] activeDatasetTlvs = createActiveDataset().toThreadTlvs();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(activeDatasetTlvs));
+ }
+
+ @Test
+ public void fromThreadTlvs_malformedTlvs_throwsIllegalArgument() {
+ final byte[] invalidTlvs = new byte[] {0x00};
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> PendingOperationalDataset.fromThreadTlvs(invalidTlvs));
+ }
+
+ @Test
+ public void toThreadTlvs_conversionIsLossLess() throws Exception {
+ PendingOperationalDataset dataset1 =
+ new PendingOperationalDataset(
+ createActiveDataset(),
+ new OperationalDatasetTimestamp(31536000, 200, false),
+ Duration.ofHours(100));
+
+ PendingOperationalDataset dataset2 =
+ PendingOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index b3118f4..362ff39 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -16,58 +16,695 @@
package android.net.thread.cts;
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_STOPPED;
import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+import static android.net.thread.ThreadNetworkException.ERROR_ABORTED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.net.thread.ThreadNetworkException.ERROR_REJECTED_BY_PEER;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeNotNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.Manifest.permission;
import android.content.Context;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.OperationalDatasetTimestamp;
+import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.net.thread.ThreadNetworkException;
import android.net.thread.ThreadNetworkManager;
import android.os.Build;
+import android.os.OutcomeReceiver;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.google.common.util.concurrent.SettableFuture;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
/** CTS tests for {@link ThreadNetworkController}. */
-@SmallTest
+@LargeTest
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
public class ThreadNetworkControllerTest {
+ private static final int CALLBACK_TIMEOUT_MILLIS = 1000;
+ private static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
+ "android.permission.THREAD_NETWORK_PRIVILEGED";
+
@Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ExecutorService mExecutor;
private ThreadNetworkManager mManager;
+ private Set<String> mGrantedPermissions;
+
@Before
public void setUp() {
+ mExecutor = Executors.newSingleThreadExecutor();
mManager = mContext.getSystemService(ThreadNetworkManager.class);
+ mGrantedPermissions = new HashSet<String>();
// TODO: we will also need it in tearDown(), it's better to have a Rule to skip
// tests if a feature is not available.
assumeNotNull(mManager);
}
+ @After
+ public void tearDown() throws Exception {
+ if (mManager != null) {
+ leaveAndWait();
+ dropAllPermissions();
+ }
+ }
+
private List<ThreadNetworkController> getAllControllers() {
return mManager.getAllThreadNetworkControllers();
}
+ private void leaveAndWait() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Void> future = SettableFuture.create();
+ controller.leave(mExecutor, future::set);
+ future.get();
+ }
+ }
+
+ private void grantPermissions(String... permissions) {
+ for (String permission : permissions) {
+ mGrantedPermissions.add(permission);
+ }
+ String[] allPermissions = new String[mGrantedPermissions.size()];
+ mGrantedPermissions.toArray(allPermissions);
+ getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(allPermissions);
+ }
+
+ private static void dropAllPermissions() {
+ getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ private static ActiveOperationalDataset newRandomizedDataset(
+ String networkName, ThreadNetworkController controller) throws Exception {
+ SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
+ controller.createRandomizedDataset(networkName, directExecutor(), future::set);
+ return future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ }
+
+ private static boolean isAttached(ThreadNetworkController controller) throws Exception {
+ return ThreadNetworkController.isAttached(getDeviceRole(controller));
+ }
+
+ private static int getDeviceRole(ThreadNetworkController controller) throws Exception {
+ SettableFuture<Integer> future = SettableFuture.create();
+ StateCallback callback = future::set;
+ controller.registerStateCallback(directExecutor(), callback);
+ int role = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ controller.unregisterStateCallback(callback);
+ return role;
+ }
+
+ private static int waitForStateAnyOf(
+ ThreadNetworkController controller, List<Integer> deviceRoles) throws Exception {
+ SettableFuture<Integer> future = SettableFuture.create();
+ StateCallback callback =
+ newRole -> {
+ if (deviceRoles.contains(newRole)) {
+ future.set(newRole);
+ }
+ };
+ controller.registerStateCallback(directExecutor(), callback);
+ int role = future.get();
+ controller.unregisterStateCallback(callback);
+ return role;
+ }
+
+ private static ActiveOperationalDataset getActiveOperationalDataset(
+ ThreadNetworkController controller) throws Exception {
+ SettableFuture<ActiveOperationalDataset> future = SettableFuture.create();
+ OperationalDatasetCallback callback = future::set;
+ controller.registerOperationalDatasetCallback(directExecutor(), callback);
+ ActiveOperationalDataset dataset = future.get(CALLBACK_TIMEOUT_MILLIS, MILLISECONDS);
+ controller.unregisterOperationalDatasetCallback(callback);
+ return dataset;
+ }
+
+ private static PendingOperationalDataset getPendingOperationalDataset(
+ ThreadNetworkController controller) throws Exception {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ controller.registerOperationalDatasetCallback(
+ directExecutor(), newDatasetCallback(activeFuture, pendingFuture));
+ return pendingFuture.get();
+ }
+
+ private static OperationalDatasetCallback newDatasetCallback(
+ SettableFuture<ActiveOperationalDataset> activeFuture,
+ SettableFuture<PendingOperationalDataset> pendingFuture) {
+ return new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeOpDataset) {
+ activeFuture.set(activeOpDataset);
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingOpDataset) {
+ pendingFuture.set(pendingOpDataset);
+ }
+ };
+ }
+
@Test
public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
for (ThreadNetworkController controller : getAllControllers()) {
assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
}
}
+
+ @Test
+ public void registerStateCallback_permissionsGranted_returnsCurrentStates() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = deviceRole::set;
+
+ try {
+ controller.registerStateCallback(mExecutor, callback);
+
+ assertThat(deviceRole.get()).isEqualTo(DEVICE_ROLE_STOPPED);
+ } finally {
+ controller.unregisterStateCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void registerStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThrows(
+ SecurityException.class,
+ () -> controller.registerStateCallback(mExecutor, role -> {}));
+ }
+ }
+
+ @Test
+ public void registerStateCallback_alreadyRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ controller.registerStateCallback(mExecutor, callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.registerStateCallback(mExecutor, callback));
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_noPermissions_throwsSecurityException() throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ controller.registerStateCallback(mExecutor, callback);
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> controller.unregisterStateCallback(callback));
+ } finally {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ controller.unregisterStateCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+ controller.registerStateCallback(mExecutor, callback);
+
+ controller.unregisterStateCallback(callback);
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_callbackNotRegistered_throwsIllegalArgumentException()
+ throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = role -> deviceRole.set(role);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ public void unregisterStateCallback_alreadyUnregistered_throwsIllegalArgumentException()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<Integer> deviceRole = SettableFuture.create();
+ StateCallback callback = deviceRole::set;
+ controller.registerStateCallback(mExecutor, callback);
+ controller.unregisterStateCallback(callback);
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.unregisterStateCallback(callback));
+ }
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_permissionsGranted_returnsCurrentStates()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ try {
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ assertThat(activeFuture.get()).isNull();
+ assertThat(pendingFuture.get()).isNull();
+ } finally {
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+
+ assertThrows(
+ SecurityException.class,
+ () -> controller.registerOperationalDatasetCallback(mExecutor, callback));
+ }
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_callbackRegistered_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+
+ @Test
+ public void unregisterOperationalDatasetCallback_noPermissions_throwsSecurityException()
+ throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ SettableFuture<ActiveOperationalDataset> activeFuture = SettableFuture.create();
+ SettableFuture<PendingOperationalDataset> pendingFuture = SettableFuture.create();
+ var callback = newDatasetCallback(activeFuture, pendingFuture);
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ controller.registerOperationalDatasetCallback(mExecutor, callback);
+
+ try {
+ dropAllPermissions();
+ assertThrows(
+ SecurityException.class,
+ () -> controller.unregisterOperationalDatasetCallback(callback));
+ } finally {
+ grantPermissions(
+ permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ controller.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+ }
+
+ private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
+ SettableFuture<V> future) {
+ return new OutcomeReceiver<V, ThreadNetworkException>() {
+ @Override
+ public void onResult(V result) {
+ future.set(result);
+ }
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ future.setException(e);
+ }
+ };
+ }
+
+ @Test
+ public void join_withPrivilegedPermission_success() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(isAttached(controller)).isTrue();
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset);
+ }
+ }
+
+ @Test
+ public void join_withoutPrivilegedPermission_throwsSecurityException() throws Exception {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+
+ assertThrows(
+ SecurityException.class,
+ () -> controller.join(activeDataset, mExecutor, v -> {}));
+ }
+ }
+
+ @Test
+ public void join_concurrentRequests_firstOneIsAborted() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ final byte[] KEY_1 = new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+ final byte[] KEY_2 = new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2};
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("TestNet", controller))
+ .setNetworkKey(KEY_1)
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1)
+ .setNetworkKey(KEY_2)
+ .build();
+ SettableFuture<Void> joinFuture1 = SettableFuture.create();
+ SettableFuture<Void> joinFuture2 = SettableFuture.create();
+
+ controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture1));
+ controller.join(activeDataset2, mExecutor, newOutcomeReceiver(joinFuture2));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, joinFuture1::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_ABORTED);
+ joinFuture2.get();
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(isAttached(controller)).isTrue();
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
+ }
+ }
+
+ @Test
+ public void leave_withPrivilegedPermission_success() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> leaveFuture = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.leave(mExecutor, newOutcomeReceiver(leaveFuture));
+ leaveFuture.get();
+
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+ }
+
+ @Test
+ public void leave_withoutPrivilegedPermission_throwsSecurityException() {
+ dropAllPermissions();
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThrows(SecurityException.class, () -> controller.leave(mExecutor, v -> {}));
+ }
+ }
+
+ @Test
+ public void leave_concurrentRequests_bothSuccess() throws Exception {
+ grantPermissions(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset = newRandomizedDataset("TestNet", controller);
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> leaveFuture1 = SettableFuture.create();
+ SettableFuture<Void> leaveFuture2 = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.leave(mExecutor, newOutcomeReceiver(leaveFuture1));
+ controller.leave(mExecutor, newOutcomeReceiver(leaveFuture2));
+
+ leaveFuture1.get();
+ leaveFuture2.get();
+ grantPermissions(permission.ACCESS_NETWORK_STATE);
+ assertThat(getDeviceRole(controller)).isEqualTo(DEVICE_ROLE_STOPPED);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("TestNet", controller))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .setExtendedPanId(new byte[] {1, 1, 1, 1, 1, 1, 1, 1})
+ .build();
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset1)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("ThreadNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> migrateFuture = SettableFuture.create();
+ controller.join(activeDataset1, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
+
+ migrateFuture.get();
+ Thread.sleep(35 * 1000);
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
+ assertThat(getPendingOperationalDataset(controller)).isNull();
+ }
+ }
+
+ @Test
+ public void scheduleMigration_whenNotAttached_failWithPreconditionError() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ newRandomizedDataset("TestNet", controller),
+ OperationalDatasetTimestamp.fromInstant(Instant.now()),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> migrateFuture = SettableFuture.create();
+
+ controller.scheduleMigration(
+ pendingDataset, mExecutor, newOutcomeReceiver(migrateFuture));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasSmallerTimestamp_rejectedByLeader()
+ throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("testNet", controller))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(20, 0, false),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> migrateFuture1 = SettableFuture.create();
+ SettableFuture<Void> migrateFuture2 = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get();
+ controller.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+ ThreadNetworkException thrown =
+ (ThreadNetworkException)
+ assertThrows(ExecutionException.class, migrateFuture2::get).getCause();
+ assertThat(thrown.getErrorCode()).isEqualTo(ERROR_REJECTED_BY_PEER);
+ }
+ }
+
+ @Test
+ public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception {
+ grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ for (ThreadNetworkController controller : getAllControllers()) {
+ final ActiveOperationalDataset activeDataset =
+ new ActiveOperationalDataset.Builder(
+ newRandomizedDataset("validName", controller))
+ .setActiveTimestamp(new OperationalDatasetTimestamp(1L, 0, false))
+ .build();
+ ActiveOperationalDataset activeDataset1 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(2L, 0, false))
+ .setNetworkName("testNet1")
+ .build();
+ PendingOperationalDataset pendingDataset1 =
+ new PendingOperationalDataset(
+ activeDataset1,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ofSeconds(30));
+ ActiveOperationalDataset activeDataset2 =
+ new ActiveOperationalDataset.Builder(activeDataset)
+ .setActiveTimestamp(new OperationalDatasetTimestamp(3L, 0, false))
+ .setNetworkName("testNet2")
+ .build();
+ PendingOperationalDataset pendingDataset2 =
+ new PendingOperationalDataset(
+ activeDataset2,
+ new OperationalDatasetTimestamp(200, 0, false),
+ Duration.ofSeconds(30));
+ SettableFuture<Void> joinFuture = SettableFuture.create();
+ SettableFuture<Void> migrateFuture1 = SettableFuture.create();
+ SettableFuture<Void> migrateFuture2 = SettableFuture.create();
+ controller.join(activeDataset, mExecutor, newOutcomeReceiver(joinFuture));
+ joinFuture.get();
+
+ controller.scheduleMigration(
+ pendingDataset1, mExecutor, newOutcomeReceiver(migrateFuture1));
+ migrateFuture1.get();
+ controller.scheduleMigration(
+ pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
+
+ migrateFuture2.get();
+ Thread.sleep(35 * 1000);
+ assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
+ assertThat(getPendingOperationalDataset(controller)).isNull();
+ }
+ }
+
+ @Test
+ public void createRandomizedDataset_wrongNetworkNameLength_throwsIllegalArgumentException() {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> controller.createRandomizedDataset("", mExecutor, dataset -> {}));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ controller.createRandomizedDataset(
+ "ANetNameIs17Bytes", mExecutor, dataset -> {}));
+ }
+ }
+
+ @Test
+ public void createRandomizedDataset_validNetworkName_success() throws Exception {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ ActiveOperationalDataset dataset = newRandomizedDataset("validName", controller);
+
+ assertThat(dataset.getNetworkName()).isEqualTo("validName");
+ assertThat(dataset.getPanId()).isLessThan(0xffff);
+ assertThat(dataset.getChannelMask().size()).isAtLeast(1);
+ assertThat(dataset.getExtendedPanId()).hasLength(8);
+ assertThat(dataset.getNetworkKey()).hasLength(16);
+ assertThat(dataset.getPskc()).hasLength(16);
+ assertThat(dataset.getMeshLocalPrefix().getPrefixLength()).isEqualTo(64);
+ assertThat(dataset.getMeshLocalPrefix().getRawAddress()[0]).isEqualTo((byte) 0xfd);
+ }
+ }
}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
new file mode 100644
index 0000000..8092693
--- /dev/null
+++ b/thread/tests/unit/Android.bp
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "ThreadNetworkUnitTests",
+ min_sdk_version: "33",
+ sdk_version: "module_current",
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
+ srcs: [
+ "src/**/*.java",
+ ],
+ test_suites: [
+ "general-tests",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "framework-connectivity-pre-jarjar",
+ "framework-connectivity-t-pre-jarjar",
+ "guava",
+ "guava-android-testlib",
+ "mockito-target-minus-junit4",
+ "net-tests-utils",
+ "truth",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ jarjar_rules: ":connectivity-jarjar-rules",
+ // Test coverage system runs on different devices. Need to
+ // compile for all architectures.
+ compile_multilib: "both",
+}
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..ace7c52
--- /dev/null
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.thread.unittests">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.thread.unittests"
+ android:label="Unit tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..597c6a8
--- /dev/null
+++ b/thread/tests/unit/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<configuration description="Config for Thread network unit test cases">
+ <option name="test-tag" value="ThreadNetworkUnitTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="ThreadNetworkUnitTests.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.thread.unittests" />
+ <option name="hidden-api-checks" value="false"/>
+ <!-- Ignores tests introduced by guava-android-testlib -->
+ <option name="exclude-annotation" value="org.junit.Ignore"/>
+ </test>
+</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
new file mode 100644
index 0000000..7284968
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.thread.ActiveOperationalDataset.Builder;
+import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/** Unit tests for {@link ActiveOperationalDataset}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActiveOperationalDatasetTest {
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] VALID_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ @Mock private Random mockRandom;
+ @Mock private SecureRandom mockSecureRandom;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private static byte[] addTlv(byte[] dataset, String tlvHex) {
+ return Bytes.concat(dataset, base16().decode(tlvHex));
+ }
+
+ @Test
+ public void fromThreadTlvs_containsUnknownTlvs_unknownTlvsRetained() {
+ byte[] datasetWithUnknownTlvs = addTlv(VALID_DATASET_TLVS, "AA01FFBB020102");
+
+ ActiveOperationalDataset dataset1 =
+ ActiveOperationalDataset.fromThreadTlvs(datasetWithUnknownTlvs);
+ ActiveOperationalDataset dataset2 =
+ ActiveOperationalDataset.fromThreadTlvs(dataset1.toThreadTlvs());
+
+ SparseArray<byte[]> unknownTlvs = dataset2.getUnknownTlvs();
+ assertThat(unknownTlvs.size()).isEqualTo(2);
+ assertThat(unknownTlvs.get(0xAA)).isEqualTo(new byte[] {(byte) 0xFF});
+ assertThat(unknownTlvs.get(0xBB)).isEqualTo(new byte[] {0x01, 0x02});
+ assertThat(dataset2).isEqualTo(dataset1);
+ }
+
+ @Test
+ public void builder_buildWithTooLongTlvs_throwsIllegalState() {
+ Builder builder = new Builder(ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS));
+ for (int i = 0; i < 10; i++) {
+ builder.addUnknownTlv(i, new byte[20]);
+ }
+
+ assertThrows(IllegalStateException.class, () -> new Builder().build());
+ }
+
+ @Test
+ public void builder_setUnknownTlvs_success() {
+ ActiveOperationalDataset dataset1 =
+ ActiveOperationalDataset.fromThreadTlvs(VALID_DATASET_TLVS);
+ SparseArray<byte[]> unknownTlvs = new SparseArray<>(2);
+ unknownTlvs.put(0x33, new byte[] {1, 2, 3});
+ unknownTlvs.put(0x44, new byte[] {1, 2, 3, 4});
+
+ ActiveOperationalDataset dataset2 =
+ new ActiveOperationalDataset.Builder(dataset1).setUnknownTlvs(unknownTlvs).build();
+
+ assertThat(dataset1.getUnknownTlvs().size()).isEqualTo(0);
+ assertThat(dataset2.getUnknownTlvs().size()).isEqualTo(2);
+ assertThat(dataset2.getUnknownTlvs().get(0x33)).isEqualTo(new byte[] {1, 2, 3});
+ assertThat(dataset2.getUnknownTlvs().get(0x44)).isEqualTo(new byte[] {1, 2, 3, 4});
+ }
+
+ @Test
+ public void securityPolicy_fromTooShortTlvValue_throwsIllegalArgument() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SecurityPolicy.fromTlvValue(new byte[] {0x01}));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> SecurityPolicy.fromTlvValue(new byte[] {0x01, 0x02}));
+ }
+
+ @Test
+ public void securityPolicy_toTlvValue_conversionIsLossLess() {
+ SecurityPolicy policy1 = new SecurityPolicy(200, new byte[] {(byte) 0xFF, (byte) 0xF8});
+
+ SecurityPolicy policy2 = SecurityPolicy.fromTlvValue(policy1.toTlvValue());
+
+ assertThat(policy2).isEqualTo(policy1);
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
new file mode 100644
index 0000000..2244a89
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Instant;
+
+/** Unit tests for {@link OperationalDatasetTimestamp}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class OperationalDatasetTimestampTest {
+ @Test
+ public void fromTlvValue_invalidTimestamp_throwsIllegalArguments() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> OperationalDatasetTimestamp.fromTlvValue(new byte[7]));
+ }
+
+ @Test
+ public void fromTlvValue_goodValue_success() {
+ OperationalDatasetTimestamp timestamp =
+ OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
+
+ assertThat(timestamp.getSeconds()).isEqualTo(0xFFEEDDCCBBAAL);
+ // 0x9989 is 0x4CC4 << 1 + 1
+ assertThat(timestamp.getTicks()).isEqualTo(0x4CC4);
+ assertThat(timestamp.isAuthoritativeSource()).isTrue();
+ }
+
+ @Test
+ public void toTlvValue_conversionIsLossLess() {
+ OperationalDatasetTimestamp timestamp1 = new OperationalDatasetTimestamp(100L, 10, true);
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+ assertThat(timestamp2).isEqualTo(timestamp1);
+ }
+
+ @Test
+ public void toTlvValue_timestampFromInstant_conversionIsLossLess() {
+ // This results in ticks = 999938900 / 1000000000 * 32768 = 32765.9978752 ~= 32766.
+ // The ticks 32766 is then converted back to 999938964.84375 ~= 999938965 nanoseconds.
+ // A wrong implementation may save Instant.getNano() and compare against the nanoseconds
+ // and results in precision loss when converted between OperationalDatasetTimestamp and the
+ // TLV values.
+ OperationalDatasetTimestamp timestamp1 =
+ OperationalDatasetTimestamp.fromInstant(Instant.ofEpochSecond(100, 999938900));
+
+ OperationalDatasetTimestamp timestamp2 =
+ OperationalDatasetTimestamp.fromTlvValue(timestamp1.toTlvValue());
+
+ assertThat(timestamp2.getSeconds()).isEqualTo(100);
+ assertThat(timestamp2.getTicks()).isEqualTo(32766);
+ assertThat(timestamp2).isEqualTo(timestamp1);
+ }
+}
diff --git a/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..2f120b2
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadNetworkControllerTest.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import static android.net.thread.ThreadNetworkController.DEVICE_ROLE_CHILD;
+import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE;
+import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+
+import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IStateCallback;
+import android.net.thread.IThreadNetworkController;
+import android.net.thread.ThreadNetworkController.OperationalDatasetCallback;
+import android.net.thread.ThreadNetworkController.StateCallback;
+import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.Process;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+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;
+
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Unit tests for {@link ThreadNetworkController}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkControllerTest {
+
+ @Mock private IThreadNetworkController mMockService;
+ private ThreadNetworkController mController;
+
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] DEFAULT_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+
+ private static final ActiveOperationalDataset DEFAULT_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS);
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mController = new ThreadNetworkController(mMockService);
+ }
+
+ private static void setBinderUid(int uid) {
+ // TODO: generally, it's not a good practice to depend on the implementation detail to set
+ // a custom UID, but Connectivity, Wifi, UWB and etc modules are using this trick. Maybe
+ // define a interface (e.b. CallerIdentityInjector) for easier mocking.
+ Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+ }
+
+ private static IStateCallback getStateCallback(InvocationOnMock invocation) {
+ return (IStateCallback) invocation.getArguments()[0];
+ }
+
+ private static IOperationReceiver getOperationReceiver(InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[0];
+ }
+
+ private static IOperationReceiver getJoinReceiver(InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
+ private static IOperationReceiver getScheduleMigrationReceiver(InvocationOnMock invocation) {
+ return (IOperationReceiver) invocation.getArguments()[1];
+ }
+
+ private static IActiveOperationalDatasetReceiver getCreateDatasetReceiver(
+ InvocationOnMock invocation) {
+ return (IActiveOperationalDatasetReceiver) invocation.getArguments()[1];
+ }
+
+ private static IOperationalDatasetCallback getOperationalDatasetCallback(
+ InvocationOnMock invocation) {
+ return (IOperationalDatasetCallback) invocation.getArguments()[0];
+ }
+
+ @Test
+ public void registerStateCallback_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+ doAnswer(
+ invoke -> {
+ getStateCallback(invoke).onDeviceRoleChanged(DEVICE_ROLE_CHILD);
+ return null;
+ })
+ .when(mMockService)
+ .registerStateCallback(any(IStateCallback.class));
+ AtomicInteger callbackUid = new AtomicInteger(0);
+ StateCallback callback = state -> callbackUid.set(Binder.getCallingUid());
+
+ try {
+ mController.registerStateCallback(Runnable::run, callback);
+
+ assertThat(callbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(callbackUid.get()).isEqualTo(Process.myUid());
+ } finally {
+ mController.unregisterStateCallback(callback);
+ }
+ }
+
+ @Test
+ public void registerOperationalDatasetCallback_callbackIsInvokedWithCallingAppIdentity()
+ throws Exception {
+ setBinderUid(SYSTEM_UID);
+ doAnswer(
+ invoke -> {
+ getOperationalDatasetCallback(invoke)
+ .onActiveOperationalDatasetChanged(null);
+ getOperationalDatasetCallback(invoke)
+ .onPendingOperationalDatasetChanged(null);
+ return null;
+ })
+ .when(mMockService)
+ .registerOperationalDatasetCallback(any(IOperationalDatasetCallback.class));
+ AtomicInteger activeCallbackUid = new AtomicInteger(0);
+ AtomicInteger pendingCallbackUid = new AtomicInteger(0);
+ OperationalDatasetCallback callback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset dataset) {
+ activeCallbackUid.set(Binder.getCallingUid());
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset dataset) {
+ pendingCallbackUid.set(Binder.getCallingUid());
+ }
+ };
+
+ try {
+ mController.registerOperationalDatasetCallback(Runnable::run, callback);
+
+ assertThat(activeCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(activeCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(pendingCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(pendingCallbackUid.get()).isEqualTo(Process.myUid());
+ } finally {
+ mController.unregisterOperationalDatasetCallback(callback);
+ }
+ }
+
+ @Test
+ public void createRandomizedDataset_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getCreateDatasetReceiver(invoke).onSuccess(DEFAULT_DATASET);
+ return null;
+ })
+ .when(mMockService)
+ .createRandomizedDataset(anyString(), any(IActiveOperationalDatasetReceiver.class));
+ mController.createRandomizedDataset(
+ "TestNet",
+ Runnable::run,
+ dataset -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getCreateDatasetReceiver(invoke).onError(ERROR_UNSUPPORTED_CHANNEL, "");
+ return null;
+ })
+ .when(mMockService)
+ .createRandomizedDataset(anyString(), any(IActiveOperationalDatasetReceiver.class));
+ mController.createRandomizedDataset(
+ "TestNet",
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(ActiveOperationalDataset dataset) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void join_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getJoinReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .join(any(ActiveOperationalDataset.class), any(IOperationReceiver.class));
+ mController.join(
+ DEFAULT_DATASET,
+ Runnable::run,
+ v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getJoinReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+ return null;
+ })
+ .when(mMockService)
+ .join(any(ActiveOperationalDataset.class), any(IOperationReceiver.class));
+ mController.join(
+ DEFAULT_DATASET,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void scheduleMigration_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+ final PendingOperationalDataset pendingDataset =
+ new PendingOperationalDataset(
+ DEFAULT_DATASET,
+ new OperationalDatasetTimestamp(100, 0, false),
+ Duration.ZERO);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getScheduleMigrationReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .scheduleMigration(
+ any(PendingOperationalDataset.class), any(IOperationReceiver.class));
+ mController.scheduleMigration(
+ pendingDataset, Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getScheduleMigrationReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+ return null;
+ })
+ .when(mMockService)
+ .scheduleMigration(
+ any(PendingOperationalDataset.class), any(IOperationReceiver.class));
+ mController.scheduleMigration(
+ pendingDataset,
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+
+ @Test
+ public void leave_callbackIsInvokedWithCallingAppIdentity() throws Exception {
+ setBinderUid(SYSTEM_UID);
+ AtomicInteger successCallbackUid = new AtomicInteger(0);
+ AtomicInteger errorCallbackUid = new AtomicInteger(0);
+
+ doAnswer(
+ invoke -> {
+ getOperationReceiver(invoke).onSuccess();
+ return null;
+ })
+ .when(mMockService)
+ .leave(any(IOperationReceiver.class));
+ mController.leave(Runnable::run, v -> successCallbackUid.set(Binder.getCallingUid()));
+ doAnswer(
+ invoke -> {
+ getOperationReceiver(invoke).onError(ERROR_UNAVAILABLE, "");
+ return null;
+ })
+ .when(mMockService)
+ .leave(any(IOperationReceiver.class));
+ mController.leave(
+ Runnable::run,
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Void unused) {}
+
+ @Override
+ public void onError(ThreadNetworkException e) {
+ errorCallbackUid.set(Binder.getCallingUid());
+ }
+ });
+
+ assertThat(successCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(successCallbackUid.get()).isEqualTo(Process.myUid());
+ assertThat(errorCallbackUid.get()).isNotEqualTo(SYSTEM_UID);
+ assertThat(errorCallbackUid.get()).isEqualTo(Process.myUid());
+ }
+}