[automerger skipped] Import translations. DO NOT MERGE ANYWHERE am: 4b11658796 -s ours
am skip reason: subject contains skip directive
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/23667288
Change-Id: I291f2774f6cfe8a8e95e3436b66211dcea6d728b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index 5d2f6e5..e17081a 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -26,7 +26,6 @@
// go with merging NetHttp and Tethering targets.
android_test {
name: "NetHttpCoverageTests",
- defaults: ["CronetTestJavaDefaults"],
enforce_default_target_sdk_version: true,
min_sdk_version: "30",
test_suites: ["general-tests", "mts-tethering"],
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index f668807..a0b2434 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -18,38 +18,10 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// cronet_test_java_defaults can be used to specify a java_defaults target that
-// either enables or disables Cronet tests. This is used to disable Cronet
-// tests on tm-mainline-prod where the required APIs are not present.
-cronet_test_java_defaults = "CronetTestJavaDefaultsDisabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_test_java_defaults may have different values
-// depending on the branch
-
-java_defaults {
- name: "CronetTestJavaDefaultsEnabled",
- enabled: true,
- // TODO(danstahr): move to unconditional static_libs once the T branch is abandoned
- static_libs: [
- "truth",
- ],
-}
-
-java_defaults {
- name: "CronetTestJavaDefaultsDisabled",
- enabled: false,
-}
-
-java_defaults {
- name: "CronetTestJavaDefaults",
- defaults: [cronet_test_java_defaults],
-}
-
android_library {
name: "CtsNetHttpTestsLib",
defaults: [
"cts_defaults",
- "CronetTestJavaDefaults",
],
sdk_version: "test_current",
min_sdk_version: "30",
@@ -61,10 +33,12 @@
"androidx.test.ext.junit",
"ctstestrunner-axt",
"ctstestserver",
- "junit",
"hamcrest-library",
+ "junit",
"kotlin-test",
"mockito-target",
+ "net-tests-utils",
+ "truth",
],
libs: [
"android.test.base",
@@ -79,9 +53,10 @@
name: "CtsNetHttpTestCases",
defaults: [
"cts_defaults",
- "CronetTestJavaDefaults",
],
+ enforce_default_target_sdk_version: true,
sdk_version: "test_current",
+ min_sdk_version: "30",
static_libs: ["CtsNetHttpTestsLib"],
// Tag this as a cts test artifact
test_suites: [
diff --git a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt b/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
index bead1f8..0760e68 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
@@ -23,8 +23,10 @@
import android.net.http.cts.util.TestBidirectionalStreamCallback.ResponseStep
import android.net.http.cts.util.assumeOKStatusCode
import android.net.http.cts.util.skipIfNoInternetConnection
+import android.os.Build
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import org.hamcrest.MatcherAssert
@@ -39,7 +41,8 @@
* This tests uses a non-hermetic server. Instead of asserting, assume the next callback. This way,
* if the request were to fail, the test would just be skipped instead of failing.
*/
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class BidirectionalStreamTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val callback = TestBidirectionalStreamCallback()
diff --git a/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
index 749389e..1405ed9 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt
@@ -23,8 +23,10 @@
import android.net.http.cts.util.TestUrlRequestCallback
import android.net.http.cts.util.TestUrlRequestCallback.FailureType
import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep
+import android.os.Build
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
@@ -32,7 +34,8 @@
import kotlin.test.assertTrue
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class CallbackExceptionTest {
@Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
index 219db61..10c7f3c 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
@@ -19,12 +19,15 @@
import android.net.http.ConnectionMigrationOptions
import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED
import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class ConnectionMigrationOptionsTest {
@Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt
index 6f4a979..56802c6 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt
@@ -19,7 +19,9 @@
import android.net.http.DnsOptions
import android.net.http.DnsOptions.DNS_OPTION_ENABLED
import android.net.http.DnsOptions.DNS_OPTION_UNSPECIFIED
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import java.time.Duration
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -27,7 +29,8 @@
import kotlin.test.assertNull
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class DnsOptionsTest {
@Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
index 31990fb..9fc4389 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -40,9 +40,12 @@
import android.net.http.cts.util.HttpCtsTestServer;
import android.net.http.cts.util.TestUrlRequestCallback;
import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
+import android.os.Build;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.After;
import org.junit.Before;
@@ -55,7 +58,8 @@
import java.util.Calendar;
import java.util.Set;
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class HttpEngineTest {
private static final String HOST = "source.android.com";
private static final String URL = "https://" + HOST;
diff --git a/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
index dd4cf0d..cff54b3 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt
@@ -19,8 +19,10 @@
import android.net.http.HttpEngine
import android.net.http.NetworkException
import android.net.http.cts.util.TestUrlRequestCallback
+import android.os.Build
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertSame
@@ -28,7 +30,8 @@
import org.junit.Test
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class NetworkExceptionTest {
@Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt b/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt
index 4b7aa14..2705fc3 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt
@@ -17,12 +17,15 @@
package android.net.http.cts
import android.net.http.QuicException
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class QuicExceptionTest {
@Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
index 0b02aa7..da0b15c 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
@@ -16,7 +16,9 @@
package android.net.http.cts
import android.net.http.QuicOptions
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import com.google.common.truth.Truth.assertThat
import java.time.Duration
import kotlin.test.assertFailsWith
@@ -25,7 +27,8 @@
import org.junit.Test
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class QuicOptionsTest {
@Test
fun testQuicOptions_defaultValues() {
diff --git a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
index 422f4d5..07e7d45 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
@@ -42,12 +42,15 @@
import android.net.http.cts.util.TestUrlRequestCallback;
import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
import android.net.http.cts.util.UploadDataProviders;
+import android.os.Build;
import android.webkit.cts.CtsTestServer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
import com.google.common.base.Strings;
@@ -70,7 +73,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-@RunWith(AndroidJUnit4.class)
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class UrlRequestTest {
private static final Executor DIRECT_EXECUTOR = Runnable::run;
diff --git a/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt b/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt
index 38da9c5..f1b57c6 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt
@@ -21,15 +21,18 @@
import android.net.http.cts.util.HttpCtsTestServer
import android.net.http.cts.util.TestUrlRequestCallback
import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep
+import android.os.Build
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.runner.RunWith
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class UrlResponseInfoTest {
@Test
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index ecf4b7f..4e4251c 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -19,7 +19,6 @@
java_genrule {
name: "net-http-test-jarjar-rules",
- defaults: ["CronetTestJavaDefaults"],
tool_files: [
":NetHttpTestsLibPreJarJar{.jar}",
"jarjar_excludes.txt",
@@ -35,31 +34,18 @@
"--output $(out)",
}
+// Library to be used in coverage tests. cronet_java_tests can't be used directly because the common
+// tests need to inherit the NetHttpTests manifest.
android_library {
name: "NetHttpTestsLibPreJarJar",
- defaults: ["CronetTestJavaDefaults"],
- srcs: [":cronet_aml_javatests_sources"],
+ static_libs: ["cronet_java_tests"],
sdk_version: "module_current",
min_sdk_version: "30",
- static_libs: [
- "cronet_testserver_utils",
- "androidx.test.ext.junit",
- "androidx.test.rules",
- "junit",
- ],
- libs: [
- "android.test.base",
- "framework-connectivity-pre-jarjar",
- // android.net.TrafficStats apis
- "framework-connectivity-t",
- ],
- lint: { test: true }
}
android_test {
name: "NetHttpTests",
defaults: [
- "CronetTestJavaDefaults",
"mts-target-sdk-version-current",
],
static_libs: ["NetHttpTestsLibPreJarJar"],
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index cf3a017..a3e86de 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -5,6 +5,8 @@
# cronet_tests.so is not jarjared and uses base classes. We can remove this when there's a
# separate java base target to depend on.
org\.chromium\.base\..+
+J\.cronet_tests_N(\$.+)?
+
# Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so
org\.chromium\.net\..*Test.*(\$.+)?
org\.chromium\.net\.NativeTestServer(\$.+)?
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 83ca2b7..b88ec7f 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -119,7 +119,6 @@
name: "libcom_android_networkstack_tethering_util_jni",
sdk_version: "30",
apex_available: [
- "//apex_available:platform", // Used by InProcessTethering
"com.android.tethering",
],
min_sdk_version: "30",
@@ -188,24 +187,6 @@
lint: { strict_updatability_linting: true },
}
-// Non-updatable tethering running in the system server process for devices not using the module
-android_app {
- name: "InProcessTethering",
- defaults: [
- "TetheringAppDefaults",
- "TetheringApiLevel",
- "ConnectivityNextEnableDefaults",
- "TetheringReleaseTargetSdk"
- ],
- static_libs: ["TetheringApiCurrentLib"],
- certificate: "platform",
- manifest: "AndroidManifest_InProcess.xml",
- // InProcessTethering is a replacement for Tethering
- overrides: ["Tethering"],
- apex_available: ["com.android.tethering"],
- lint: { strict_updatability_linting: true },
-}
-
// Updatable tethering packaged for finalized API
android_app {
name: "Tethering",
diff --git a/Tethering/AndroidManifest_InProcess.xml b/Tethering/AndroidManifest_InProcess.xml
deleted file mode 100644
index b1f1240..0000000
--- a/Tethering/AndroidManifest_InProcess.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.networkstack.tethering.inprocess"
- android:sharedUserId="android.uid.system"
- android:process="system">
- <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
- <application>
- <service android:name="com.android.networkstack.tethering.TetheringService"
- android:process="system"
- android:permission="android.permission.MAINLINE_NETWORK_STACK"
- android:exported="true">
- <intent-filter>
- <action android:name="android.net.ITetheringConnector.InProcess"/>
- </intent-filter>
- </service>
- </application>
-</manifest>
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 8ddaa78..253fb00 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -19,13 +19,6 @@
}
prebuilt_etc {
- name: "TetheringInProcessFlag",
- src: "in-process",
- filename_from_src: true,
- sub_dir: "flag",
-}
-
-prebuilt_etc {
name: "TetheringOutOfProcessFlag",
src: "out-of-process",
filename_from_src: true,
@@ -36,43 +29,29 @@
// different value depending on the branch.
java_defaults {
name: "ConnectivityNextEnableDefaults",
- enabled: false,
+ enabled: true,
}
java_defaults {
name: "NetworkStackApiShimSettingsForCurrentBranch",
// API shims to include in the networking modules built from the branch. Branches that disable
// the "next" targets must use stable shims (latest stable API level) instead of current shims
// (X_current API level).
- static_libs: ["NetworkStackApiStableShims"],
+ static_libs: ["NetworkStackApiCurrentShims"],
}
apex_defaults {
name: "ConnectivityApexDefaults",
// Tethering app to include in the AOSP apex. Branches that disable the "next" targets may use
// a stable tethering app instead, but will generally override the AOSP apex to use updatable
// package names and keys, so that apex will be unused anyway.
- apps: ["Tethering"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
+ apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
}
-enable_tethering_next_apex = false
+enable_tethering_next_apex = true
// This is a placeholder comment to avoid merge conflicts
// as the above target may have different "enabled" values
// depending on the branch
-// cronet_in_tethering_apex_defaults can be used to specify an apex_defaults target that either
-// enables or disables inclusion of Cronet in the Tethering apex. This is used to disable Cronet
-// on tm-mainline-prod. Note: in order for Cronet APIs to work Cronet must also be enabled
-// by the cronet_java_*_defaults in common/TetheringLib/Android.bp.
-cronet_in_tethering_apex_defaults = "CronetInTetheringApexDefaultsDisabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_apex_defaults may have different values
-// depending on the branch
-
apex_defaults {
name: "CronetInTetheringApexDefaults",
- defaults: [cronet_in_tethering_apex_defaults],
-}
-
-apex_defaults {
- name: "CronetInTetheringApexDefaultsEnabled",
jni_libs: [
"cronet_aml_components_cronet_android_cronet",
"//external/cronet/third_party/boringssl:libcrypto",
@@ -90,10 +69,6 @@
},
}
-apex_defaults {
- name: "CronetInTetheringApexDefaultsDisabled",
-}
-
apex {
name: "com.android.tethering",
defaults: [
@@ -254,27 +229,3 @@
standalone_contents: ["service-connectivity"],
apex_available: ["com.android.tethering"],
}
-
-override_apex {
- name: "com.android.tethering.inprocess",
- base: "com.android.tethering",
- package_name: "com.android.tethering.inprocess",
- enabled: enable_tethering_next_apex,
- bpfs: [
- "block.o",
- "clatd.o",
- "dscpPolicy.o",
- "netd.o",
- "offload@inprocess.o",
- "test@inprocess.o",
- ],
- apps: [
- "ServiceConnectivityResources",
- "InProcessTethering",
- ],
- prebuilts: [
- "current_sdkinfo",
- "privapp_allowlist_com.android.tethering",
- "TetheringInProcessFlag",
- ],
-}
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index cb6783c..a957e23 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -17,16 +17,6 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-// Both cronet_java_defaults and cronet_java_prejarjar_defaults can be used to
-// specify a java_defaults target that either enables or disables Cronet. This
-// is used to disable Cronet on tm-mainline-prod.
-// Note: they must either both be enabled or disabled.
-cronet_java_defaults = "CronetJavaDefaultsDisabled"
-cronet_java_prejarjar_defaults = "CronetJavaPrejarjarDefaultsDisabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_defaults may have different values
-// depending on the branch
-
java_sdk_library {
name: "framework-tethering",
defaults: [
@@ -69,11 +59,6 @@
java_defaults {
name: "CronetJavaDefaults",
- defaults: [cronet_java_defaults],
-}
-
-java_defaults {
- name: "CronetJavaDefaultsEnabled",
srcs: [":cronet_aml_api_sources"],
libs: [
"androidx.annotation_annotation",
@@ -84,21 +69,7 @@
}
java_defaults {
- name: "CronetJavaDefaultsDisabled",
- api_dir: "cronet_disabled/api",
-}
-
-java_defaults {
name: "CronetJavaPrejarjarDefaults",
- defaults: [cronet_java_prejarjar_defaults],
-}
-
-java_defaults {
- name: "CronetJavaPrejarjarDefaultsDisabled",
-}
-
-java_defaults {
- name: "CronetJavaPrejarjarDefaultsEnabled",
static_libs: [
"cronet_aml_api_java",
"cronet_aml_java"
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 65ea8e5..6affb62 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -17,6 +17,8 @@
package android.net.ip;
import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR;
import static android.net.TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
@@ -405,8 +407,8 @@
/** Internals. */
- private boolean startIPv4() {
- return configureIPv4(true);
+ private boolean startIPv4(int scope) {
+ return configureIPv4(true, scope);
}
/**
@@ -616,7 +618,7 @@
}
private void stopIPv4() {
- configureIPv4(false);
+ configureIPv4(false /* enabled */, CONNECTIVITY_SCOPE_GLOBAL /* not used */);
// NOTE: All of configureIPv4() will be refactored out of existence
// into calls to InterfaceController, shared with startIPv4().
mInterfaceCtrl.clearIPv4Address();
@@ -627,11 +629,11 @@
mStaticIpv4ClientAddr = null;
}
- private boolean configureIPv4(boolean enabled) {
+ private boolean configureIPv4(boolean enabled, int scope) {
if (VDBG) Log.d(TAG, "configureIPv4(" + enabled + ")");
if (enabled) {
- mIpv4Address = requestIpv4Address(true /* useLastAddress */);
+ mIpv4Address = requestIpv4Address(scope, true /* useLastAddress */);
}
if (mIpv4Address == null) {
@@ -679,12 +681,12 @@
return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
}
- private LinkAddress requestIpv4Address(final boolean useLastAddress) {
+ private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR);
- return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress);
+ return mPrivateAddressCoordinator.requestDownstreamAddress(this, scope, useLastAddress);
}
private boolean startIPv6() {
@@ -998,67 +1000,6 @@
}
}
- private void handleNewPrefixRequest(@NonNull final IpPrefix currentPrefix) {
- if (!currentPrefix.contains(mIpv4Address.getAddress())
- || currentPrefix.getPrefixLength() != mIpv4Address.getPrefixLength()) {
- Log.e(TAG, "Invalid prefix: " + currentPrefix);
- return;
- }
-
- final LinkAddress deprecatedLinkAddress = mIpv4Address;
- mIpv4Address = requestIpv4Address(false);
- if (mIpv4Address == null) {
- mLog.e("Fail to request a new downstream prefix");
- return;
- }
- final Inet4Address srvAddr = (Inet4Address) mIpv4Address.getAddress();
-
- // Add new IPv4 address on the interface.
- if (!mInterfaceCtrl.addAddress(srvAddr, currentPrefix.getPrefixLength())) {
- mLog.e("Failed to add new IP " + srvAddr);
- return;
- }
-
- // Remove deprecated routes from local network.
- removeRoutesFromLocalNetwork(
- Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress)));
- mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
-
- // Add new routes to local network.
- addRoutesToLocalNetwork(
- Collections.singletonList(getDirectConnectedRoute(mIpv4Address)));
- mLinkProperties.addLinkAddress(mIpv4Address);
-
- // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
- // listen on the interface configured with new IPv4 address, that results DNS validation
- // failure of downstream client even if appropriate routes have been configured.
- try {
- mNetd.tetherApplyDnsInterfaces();
- } catch (ServiceSpecificException | RemoteException e) {
- mLog.e("Failed to update local DNS caching server");
- return;
- }
- sendLinkProperties();
-
- // Notify DHCP server that new prefix/route has been applied on IpServer.
- final Inet4Address clientAddr = mStaticIpv4ClientAddr == null ? null :
- (Inet4Address) mStaticIpv4ClientAddr.getAddress();
- final DhcpServingParamsParcel params = makeServingParams(srvAddr /* defaultRouter */,
- srvAddr /* dnsServer */, mIpv4Address /* serverLinkAddress */, clientAddr);
- try {
- mDhcpServer.updateParams(params, new OnHandlerStatusCallback() {
- @Override
- public void callback(int statusCode) {
- if (statusCode != STATUS_SUCCESS) {
- mLog.e("Error updating DHCP serving params: " + statusCode);
- }
- }
- });
- } catch (RemoteException e) {
- mLog.e("Error updating DHCP serving params", e);
- }
- }
-
private byte getHopLimit(String upstreamIface, int adjustTTL) {
try {
int upstreamHopLimit = Integer.parseUnsignedInt(
@@ -1173,12 +1114,37 @@
mBpfCoordinator.stopMonitoring(this);
}
- class BaseServingState extends State {
+ abstract class BaseServingState extends State {
+ private final int mDesiredInterfaceState;
+
+ BaseServingState(int interfaceState) {
+ mDesiredInterfaceState = interfaceState;
+ }
+
@Override
public void enter() {
startConntrackMonitoring();
- if (!startIPv4()) {
+ startServingInterface();
+
+ if (mLastError != TETHER_ERROR_NO_ERROR) {
+ transitionTo(mInitialState);
+ }
+
+ if (DBG) Log.d(TAG, getStateString(mDesiredInterfaceState) + " serve " + mIfaceName);
+ sendInterfaceState(mDesiredInterfaceState);
+ }
+
+ private int getScope() {
+ if (mDesiredInterfaceState == STATE_TETHERED) {
+ return CONNECTIVITY_SCOPE_GLOBAL;
+ }
+
+ return CONNECTIVITY_SCOPE_LOCAL;
+ }
+
+ private void startServingInterface() {
+ if (!startIPv4(getScope())) {
mLastError = TETHER_ERROR_IFACE_CFG_ERROR;
return;
}
@@ -1257,6 +1223,67 @@
}
return true;
}
+
+ private void handleNewPrefixRequest(@NonNull final IpPrefix currentPrefix) {
+ if (!currentPrefix.contains(mIpv4Address.getAddress())
+ || currentPrefix.getPrefixLength() != mIpv4Address.getPrefixLength()) {
+ Log.e(TAG, "Invalid prefix: " + currentPrefix);
+ return;
+ }
+
+ final LinkAddress deprecatedLinkAddress = mIpv4Address;
+ mIpv4Address = requestIpv4Address(getScope(), false);
+ if (mIpv4Address == null) {
+ mLog.e("Fail to request a new downstream prefix");
+ return;
+ }
+ final Inet4Address srvAddr = (Inet4Address) mIpv4Address.getAddress();
+
+ // Add new IPv4 address on the interface.
+ if (!mInterfaceCtrl.addAddress(srvAddr, currentPrefix.getPrefixLength())) {
+ mLog.e("Failed to add new IP " + srvAddr);
+ return;
+ }
+
+ // Remove deprecated routes from local network.
+ removeRoutesFromLocalNetwork(
+ Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress)));
+ mLinkProperties.removeLinkAddress(deprecatedLinkAddress);
+
+ // Add new routes to local network.
+ addRoutesToLocalNetwork(
+ Collections.singletonList(getDirectConnectedRoute(mIpv4Address)));
+ mLinkProperties.addLinkAddress(mIpv4Address);
+
+ // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't
+ // listen on the interface configured with new IPv4 address, that results DNS validation
+ // failure of downstream client even if appropriate routes have been configured.
+ try {
+ mNetd.tetherApplyDnsInterfaces();
+ } catch (ServiceSpecificException | RemoteException e) {
+ mLog.e("Failed to update local DNS caching server");
+ return;
+ }
+ sendLinkProperties();
+
+ // Notify DHCP server that new prefix/route has been applied on IpServer.
+ final Inet4Address clientAddr = mStaticIpv4ClientAddr == null ? null :
+ (Inet4Address) mStaticIpv4ClientAddr.getAddress();
+ final DhcpServingParamsParcel params = makeServingParams(srvAddr /* defaultRouter */,
+ srvAddr /* dnsServer */, mIpv4Address /* serverLinkAddress */, clientAddr);
+ try {
+ mDhcpServer.updateParams(params, new OnHandlerStatusCallback() {
+ @Override
+ public void callback(int statusCode) {
+ if (statusCode != STATUS_SUCCESS) {
+ mLog.e("Error updating DHCP serving params: " + statusCode);
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ mLog.e("Error updating DHCP serving params", e);
+ }
+ }
}
// Handling errors in BaseServingState.enter() by transitioning is
@@ -1265,15 +1292,8 @@
// and forwarding and NAT rules should be handled by a coordinating
// functional element outside of IpServer.
class LocalHotspotState extends BaseServingState {
- @Override
- public void enter() {
- super.enter();
- if (mLastError != TETHER_ERROR_NO_ERROR) {
- transitionTo(mInitialState);
- }
-
- if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
- sendInterfaceState(STATE_LOCAL_ONLY);
+ LocalHotspotState() {
+ super(STATE_LOCAL_ONLY);
}
@Override
@@ -1301,15 +1321,8 @@
// and forwarding and NAT rules should be handled by a coordinating
// functional element outside of IpServer.
class TetheredState extends BaseServingState {
- @Override
- public void enter() {
- super.enter();
- if (mLastError != TETHER_ERROR_NO_ERROR) {
- transitionTo(mInitialState);
- }
-
- if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
- sendInterfaceState(STATE_TETHERED);
+ TetheredState() {
+ super(STATE_TETHERED);
}
@Override
diff --git a/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
index 8a96988..5b19c4e 100644
--- a/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
+++ b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java
@@ -29,6 +29,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -49,6 +51,8 @@
@NonNull
private List<WifiClient> mLastWifiClients = Collections.emptyList();
@NonNull
+ private List<WifiClient> mLastLocalOnlyClients = Collections.emptyList();
+ @NonNull
private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
@VisibleForTesting
@@ -72,25 +76,44 @@
*
* <p>The new list can be obtained through {@link #getLastTetheredClients()}.
* @param ipServers The IpServers used to assign addresses to clients.
- * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
- * update.
+ * @param wifiClients The list of L2-connected WiFi clients that are connected to a global
+ * hotspot. Null for no change since last update.
+ * @param localOnlyClients The list of L2-connected WiFi clients that are connected to localOnly
+ * hotspot. Null for no change since last update.
* @return True if the list of clients changed since the last calculation.
*/
public boolean updateConnectedClients(
- Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
+ Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients,
+ @Nullable List<WifiClient> localOnlyClients) {
final long now = mClock.elapsedRealtime();
- if (wifiClients != null) {
- mLastWifiClients = wifiClients;
- }
+ if (wifiClients != null) mLastWifiClients = wifiClients;
+ if (localOnlyClients != null) mLastLocalOnlyClients = localOnlyClients;
+
final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
+ final Set<MacAddress> localOnlyClientMacs = getClientMacs(mLastLocalOnlyClients);
// Build the list of non-expired leases from all IpServers, grouped by mac address
final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
for (IpServer server : ipServers) {
+ final Set<MacAddress> connectedClientMacs;
+ switch (server.servingMode()) {
+ case IpServer.STATE_TETHERED:
+ connectedClientMacs = wifiClientMacs;
+ break;
+ case IpServer.STATE_LOCAL_ONLY:
+ // Before T, SAP and LOHS both use wifiClientMacs because
+ // registerLocalOnlyHotspotSoftApCallback didn't exist.
+ connectedClientMacs = SdkLevel.isAtLeastT()
+ ? localOnlyClientMacs : wifiClientMacs;
+ break;
+ default:
+ continue;
+ }
+
for (TetheredClient client : server.getAllLeases()) {
if (client.getTetheringType() == TETHERING_WIFI
- && !wifiClientMacs.contains(client.getMacAddress())) {
+ && !connectedClientMacs.contains(client.getMacAddress())) {
// Skip leases of WiFi clients that are not (or no longer) L2-connected
continue;
}
@@ -104,11 +127,8 @@
// TODO: add IPv6 addresses from netlink
// Add connected WiFi clients that do not have any known address
- for (MacAddress client : wifiClientMacs) {
- if (clientsMap.containsKey(client)) continue;
- clientsMap.put(client, new TetheredClient(
- client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
- }
+ addWifiClientsIfNoLeases(clientsMap, wifiClientMacs);
+ addWifiClientsIfNoLeases(clientsMap, localOnlyClientMacs);
final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
@@ -117,6 +137,15 @@
return clientsChanged;
}
+ private static void addWifiClientsIfNoLeases(
+ final Map<MacAddress, TetheredClient> clientsMap, final Set<MacAddress> clientMacs) {
+ for (MacAddress mac : clientMacs) {
+ if (clientsMap.containsKey(mac)) continue;
+ clientsMap.put(mac, new TetheredClient(
+ mac, Collections.emptyList() /* addresses */, TETHERING_WIFI));
+ }
+ }
+
private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
final TetheredClient aggregateClient = clientsMap.getOrDefault(
lease.getMacAddress(), lease);
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index 6d502ce..b88b13b 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -459,8 +459,9 @@
}
@VisibleForTesting
- PendingIntent createRecheckAlarmIntent() {
+ PendingIntent createRecheckAlarmIntent(final String pkgName) {
final Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
+ intent.setPackage(pkgName);
return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
@@ -470,7 +471,7 @@
final int period = config.provisioningCheckPeriod;
if (period <= 0) return;
- mProvisioningRecheckAlarm = createRecheckAlarmIntent();
+ mProvisioningRecheckAlarm = createRecheckAlarmIntent(mContext.getPackageName());
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
long triggerAtMillis = SystemClock.elapsedRealtime() + (period * MS_PER_HOUR);
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 41a10ae..6c0ca82 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -16,6 +16,8 @@
package com.android.networkstack.tethering;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_WIFI_P2P;
@@ -34,7 +36,6 @@
import android.net.ip.IpServer;
import android.util.ArrayMap;
import android.util.ArraySet;
-import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -77,7 +78,7 @@
private final ConnectivityManager mConnectivityMgr;
private final TetheringConfiguration mConfig;
// keyed by downstream type(TetheringManager.TETHERING_*).
- private final SparseArray<LinkAddress> mCachedAddresses;
+ private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
public PrivateAddressCoordinator(Context context, TetheringConfiguration config) {
mDownstreams = new ArraySet<>();
@@ -85,10 +86,12 @@
mConnectivityMgr = (ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE);
mConfig = config;
- mCachedAddresses = new SparseArray<>();
+ mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
// Reserved static addresses for bluetooth and wifi p2p.
- mCachedAddresses.put(TETHERING_BLUETOOTH, new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
- mCachedAddresses.put(TETHERING_WIFI_P2P, new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
+ mCachedAddresses.put(new AddressKey(TETHERING_BLUETOOTH, CONNECTIVITY_SCOPE_GLOBAL),
+ new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS));
+ mCachedAddresses.put(new AddressKey(TETHERING_WIFI_P2P, CONNECTIVITY_SCOPE_LOCAL),
+ new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS));
mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"),
new IpPrefix("172.16.0.0/12"), new IpPrefix("10.0.0.0/8")));
@@ -166,16 +169,18 @@
* returns null if there is no available address.
*/
@Nullable
- public LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) {
+ public LinkAddress requestDownstreamAddress(final IpServer ipServer, final int scope,
+ boolean useLastAddress) {
if (mConfig.shouldEnableWifiP2pDedicatedIp()
&& ipServer.interfaceType() == TETHERING_WIFI_P2P) {
return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
}
+ final AddressKey addrKey = new AddressKey(ipServer.interfaceType(), scope);
// This ensures that tethering isn't started on 2 different interfaces with the same type.
// Once tethering could support multiple interface with the same type,
// TetheringSoftApCallback would need to handle it among others.
- final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType());
+ final LinkAddress cachedAddress = mCachedAddresses.get(addrKey);
if (useLastAddress && cachedAddress != null
&& !isConflictWithUpstream(asIpPrefix(cachedAddress))) {
mDownstreams.add(ipServer);
@@ -186,7 +191,7 @@
final LinkAddress newAddress = chooseDownstreamAddress(prefixRange);
if (newAddress != null) {
mDownstreams.add(ipServer);
- mCachedAddresses.put(ipServer.interfaceType(), newAddress);
+ mCachedAddresses.put(addrKey, newAddress);
return newAddress;
}
}
@@ -384,6 +389,34 @@
return asIpPrefix(address);
}
+ private static class AddressKey {
+ private final int mTetheringType;
+ private final int mScope;
+
+ private AddressKey(int type, int scope) {
+ mTetheringType = type;
+ mScope = scope;
+ }
+
+ @Override
+ public int hashCode() {
+ return (mTetheringType << 16) + mScope;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (!(obj instanceof AddressKey)) return false;
+ final AddressKey other = (AddressKey) obj;
+
+ return mTetheringType == other.mTetheringType && mScope == other.mScope;
+ }
+
+ @Override
+ public String toString() {
+ return "AddressKey(" + mTetheringType + ", " + mScope + ")";
+ }
+ }
+
void dump(final IndentingPrintWriter pw) {
pw.println("mTetheringPrefixes:");
pw.increaseIndent();
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 4c5bf4e..e5f644e 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -58,6 +58,7 @@
import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY;
import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED;
import static android.net.wifi.WifiManager.IFACE_IP_MODE_UNSPECIFIED;
+import static android.net.wifi.WifiManager.SoftApCallback;
import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED;
import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -479,15 +480,15 @@
mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler);
final WifiManager wifiManager = getWifiManager();
- TetheringSoftApCallback softApCallback = new TetheringSoftApCallback();
if (wifiManager != null) {
- wifiManager.registerSoftApCallback(mExecutor, softApCallback);
- }
- if (SdkLevel.isAtLeastT() && wifiManager != null) {
- // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
- // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
- // or MAINLINE_NETWORK_STACK permission would also able to use this API.
- wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor, softApCallback);
+ wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback());
+ if (SdkLevel.isAtLeastT()) {
+ // Although WifiManager#registerLocalOnlyHotspotSoftApCallback document that it need
+ // NEARBY_WIFI_DEVICES permission, but actually a caller who have NETWORK_STACK
+ // or MAINLINE_NETWORK_STACK permission can also use this API.
+ wifiManager.registerLocalOnlyHotspotSoftApCallback(mExecutor,
+ new LocalOnlyHotspotCallback());
+ }
}
startTrackDefaultNetwork();
@@ -573,26 +574,17 @@
}
}
- private class TetheringSoftApCallback implements WifiManager.SoftApCallback {
- // TODO: Remove onStateChanged override when this method has default on
- // WifiManager#SoftApCallback interface.
- // Wifi listener for state change of the soft AP
- @Override
- public void onStateChanged(final int state, final int failureReason) {
- // Nothing
- }
-
- // Called by wifi when the number of soft AP clients changed.
- // Currently multiple softAp would not behave well in PrivateAddressCoordinator
- // (where it gets the address from cache), it ensure tethering only support one ipServer for
- // TETHERING_WIFI. Once tethering support multiple softAp enabled simultaneously,
- // onConnectedClientsChanged should also be updated to support tracking different softAp's
- // clients individually.
- // TODO: Add wtf log and have check to reject request duplicated type with different
- // interface.
+ private class TetheringSoftApCallback implements SoftApCallback {
@Override
public void onConnectedClientsChanged(final List<WifiClient> clients) {
- updateConnectedClients(clients);
+ updateConnectedClients(clients, null);
+ }
+ }
+
+ private class LocalOnlyHotspotCallback implements SoftApCallback {
+ @Override
+ public void onConnectedClientsChanged(final List<WifiClient> clients) {
+ updateConnectedClients(null, clients);
}
}
@@ -1968,7 +1960,7 @@
mIPv6TetheringCoordinator.removeActiveDownstream(who);
mOffload.excludeDownstreamInterface(who.interfaceName());
mForwardedDownstreams.remove(who);
- updateConnectedClients(null /* wifiClients */);
+ maybeDhcpLeasesChanged();
// If this is a Wi-Fi interface, tell WifiManager of any errors
// or the inactive serving state.
@@ -2710,9 +2702,15 @@
if (e != null) throw e;
}
- private void updateConnectedClients(final List<WifiClient> wifiClients) {
+ private void maybeDhcpLeasesChanged() {
+ // null means wifi clients did not change.
+ updateConnectedClients(null, null);
+ }
+
+ private void updateConnectedClients(final List<WifiClient> wifiClients,
+ final List<WifiClient> localOnlyClients) {
if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(),
- wifiClients)) {
+ wifiClients, localOnlyClients)) {
reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients());
}
}
@@ -2731,7 +2729,7 @@
@Override
public void dhcpLeasesChanged() {
- updateConnectedClients(null /* wifiClients */);
+ maybeDhcpLeasesChanged();
}
@Override
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index b6591a9..b0aa668 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -39,6 +39,8 @@
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
@@ -158,6 +160,8 @@
public final int activeDataSubId;
+ private final Dependencies mDeps;
+
private final boolean mEnableLegacyDhcpServer;
private final int mOffloadPollInterval;
// TODO: Add to TetheringConfigurationParcel if required.
@@ -170,7 +174,31 @@
private final int mUsbTetheringFunction;
protected final ContentResolver mContentResolver;
- public TetheringConfiguration(Context ctx, SharedLog log, int id) {
+ /**
+ * A class wrapping dependencies of {@link TetheringConfiguration}, useful for testing.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+ @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
+ return DeviceConfigUtils.isFeatureEnabled(context, namespace, name,
+ moduleName, defaultEnabled);
+ }
+
+ boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
+ boolean defaultValue) {
+ return DeviceConfig.getBoolean(namespace, name, defaultValue);
+ }
+ }
+
+ public TetheringConfiguration(@NonNull Context ctx, @NonNull SharedLog log, int id) {
+ this(ctx, log, id, new Dependencies());
+ }
+
+ @VisibleForTesting
+ public TetheringConfiguration(@NonNull Context ctx, @NonNull SharedLog log, int id,
+ @NonNull Dependencies deps) {
+ mDeps = deps;
final SharedLog configLog = log.forSubComponent("config");
activeDataSubId = id;
@@ -583,17 +611,7 @@
}
private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
- // Due to the limitation of static mock for testing, using #getDeviceConfigProperty instead
- // of DeviceConfig#getBoolean. If using #getBoolean here, the test can't know that the
- // returned boolean value comes from device config or default value (because of null
- // property string). See the test case testBpfOffload{*} in TetheringConfigurationTest.java.
- final String value = getDeviceConfigProperty(name);
- return value != null ? Boolean.parseBoolean(value) : defaultValue;
- }
-
- @VisibleForTesting
- protected String getDeviceConfigProperty(String name) {
- return DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, name);
+ return mDeps.getDeviceConfigBoolean(NAMESPACE_CONNECTIVITY, name, defaultValue);
}
/**
@@ -610,10 +628,9 @@
return isFeatureEnabled(ctx, NAMESPACE_TETHERING, featureVersionFlag);
}
- @VisibleForTesting
- protected boolean isFeatureEnabled(Context ctx, String namespace, String featureVersionFlag) {
- return DeviceConfigUtils.isFeatureEnabled(ctx, namespace, featureVersionFlag,
- TETHERING_MODULE_NAME, false /* defaultEnabled */);
+ private boolean isFeatureEnabled(Context ctx, String namespace, String featureVersionFlag) {
+ return mDeps.isFeatureEnabled(ctx, namespace, featureVersionFlag, TETHERING_MODULE_NAME,
+ false /* defaultEnabled */);
}
private Resources getResources(Context ctx, int subId) {
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index f0d9057..46e50ef 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -19,6 +19,8 @@
import static android.net.INetd.IF_STATE_DOWN;
import static android.net.INetd.IF_STATE_UP;
import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_BLUETOOTH;
import static android.net.TetheringManager.TETHERING_NCM;
import static android.net.TetheringManager.TETHERING_USB;
@@ -48,6 +50,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
@@ -271,8 +274,8 @@
dispatchTetherConnectionChanged(upstreamIface, lp, 0);
}
reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
- mTestAddress);
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+ anyBoolean())).thenReturn(mTestAddress);
}
@SuppressWarnings("DoNotCall") // Ignore warning for synchronous to call to Thread.run()
@@ -293,8 +296,8 @@
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
- mTestAddress);
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+ anyBoolean())).thenReturn(mTestAddress);
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(DEFAULT_USING_BPF_OFFLOAD);
when(mTetherConfig.useLegacyDhcpServer()).thenReturn(false /* default value */);
@@ -428,7 +431,8 @@
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
if (isAtLeastT()) {
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+ eq(CONNECTIVITY_SCOPE_GLOBAL), eq(true));
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
}
@@ -477,7 +481,8 @@
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+ eq(CONNECTIVITY_SCOPE_GLOBAL), eq(true));
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP)));
inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
@@ -498,7 +503,8 @@
dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+ eq(CONNECTIVITY_SCOPE_LOCAL), eq(true));
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP)));
inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
@@ -766,7 +772,8 @@
final ArgumentCaptor<LinkProperties> lpCaptor =
ArgumentCaptor.forClass(LinkProperties.class);
InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator);
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true));
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+ eq(CONNECTIVITY_SCOPE_LOCAL), eq(true));
inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
// One for ipv4 route, one for ipv6 link local route.
inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
@@ -779,12 +786,13 @@
// Simulate the DHCP server receives DHCPDECLINE on MirrorLink and then signals
// onNewPrefixRequest callback.
final LinkAddress newAddress = new LinkAddress("192.168.100.125/24");
- when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
- newAddress);
+ when(mAddressCoordinator.requestDownstreamAddress(any(), anyInt(),
+ anyBoolean())).thenReturn(newAddress);
eventCallbacks.onNewPrefixRequest(new IpPrefix("192.168.42.0/24"));
mLooper.dispatchAll();
- inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(false));
+ inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(),
+ eq(CONNECTIVITY_SCOPE_LOCAL), eq(false));
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture());
verifyNoMoreInteractions(mCallback);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
index d915354..2dd9f91 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt
@@ -24,19 +24,25 @@
import android.net.TetheringManager.TETHERING_WIFI
import android.net.ip.IpServer
import android.net.wifi.WifiClient
+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 kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class)
@SmallTest
class ConnectedClientsTrackerTest {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
private val server1 = mock(IpServer::class.java)
private val server2 = mock(IpServer::class.java)
@@ -70,55 +76,122 @@
@Test
fun testUpdateConnectedClients() {
+ doReturn(IpServer.STATE_TETHERED).`when`(server1).servingMode()
+ doReturn(IpServer.STATE_TETHERED).`when`(server2).servingMode()
+ runUpdateConnectedClientsTest(isGlobal = true)
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ fun testUpdateConnectedClients_LocalOnly() {
+ doReturn(IpServer.STATE_LOCAL_ONLY).`when`(server1).servingMode()
+ doReturn(IpServer.STATE_LOCAL_ONLY).`when`(server2).servingMode()
+ runUpdateConnectedClientsTest(isGlobal = false)
+ }
+
+ fun runUpdateConnectedClientsTest(isGlobal: Boolean) {
doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
val tracker = ConnectedClientsTracker(clock)
- assertFalse(tracker.updateConnectedClients(servers, null))
+ assertFalse(tracker.updateConnectedClients(servers, null, null))
// Obtain a lease for client 1
doReturn(listOf(client1)).`when`(server1).allLeases
- assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1)))
+ if (isGlobal) {
+ assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+ wifiClients = listOf(wifiClient1)))
+ } else {
+ assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+ localOnlyClients = listOf(wifiClient1)))
+ }
// Client 2 L2-connected, no lease yet
val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI)
- assertSameClients(listOf(client1, client2WithoutAddr),
- assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2)))
+ if (isGlobal) {
+ assertSameClients(listOf(client1, client2WithoutAddr), assertNewClients(
+ tracker, servers, wifiClients = listOf(wifiClient1, wifiClient2)))
+ } else {
+ assertSameClients(listOf(client1, client2WithoutAddr), assertNewClients(
+ tracker, servers, localOnlyClients = listOf(wifiClient1, wifiClient2)))
+ }
// Client 2 lease obtained
doReturn(listOf(client1, client2)).`when`(server1).allLeases
- assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null))
+ assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers))
// Client 3 lease obtained
doReturn(listOf(client3)).`when`(server2).allLeases
- assertSameClients(listOf(client1, client2, client3),
- assertNewClients(tracker, servers, null))
+ assertSameClients(listOf(client1, client2, client3), assertNewClients(tracker, servers))
- // Client 2 L2-disconnected
- assertSameClients(listOf(client1, client3),
- assertNewClients(tracker, servers, listOf(wifiClient1)))
-
- // Client 1 L2-disconnected
- assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList()))
-
- // Client 1 comes back
- assertSameClients(listOf(client1, client3),
- assertNewClients(tracker, servers, listOf(wifiClient1)))
+ if (isGlobal) {
+ // Client 2 L2-disconnected
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, wifiClients = listOf(wifiClient1)))
+ // Client 1 L2-disconnected
+ assertSameClients(listOf(client3), assertNewClients(tracker, servers,
+ wifiClients = emptyList()))
+ // Client 1 comes back
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, wifiClients = listOf(wifiClient1)))
+ } else {
+ // Client 2 L2-disconnected
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, localOnlyClients = listOf(wifiClient1)))
+ // Client 1 L2-disconnected
+ assertSameClients(listOf(client3),
+ assertNewClients(tracker, servers, localOnlyClients = emptyList()))
+ // Client 1 comes back
+ assertSameClients(listOf(client1, client3),
+ assertNewClients(tracker, servers, localOnlyClients = listOf(wifiClient1)))
+ }
// Leases lost, client 1 still L2-connected
doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)),
- assertNewClients(tracker, servers, null))
+ assertNewClients(tracker, servers))
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+ fun testLocalOnlyAndTetheredHotspotClients() {
+ val tracker = ConnectedClientsTracker(clock)
+ doReturn(IpServer.STATE_LOCAL_ONLY).`when`(server1).servingMode()
+ doReturn(IpServer.STATE_TETHERED).`when`(server2).servingMode()
+
+ // Client 1 connected to server1 (LOHS)
+ doReturn(listOf(client1)).`when`(server1).allLeases
+ doReturn(emptyList<TetheredClient>()).`when`(server2).allLeases
+ assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+ localOnlyClients = listOf(wifiClient1)))
+
+ // Client 2 connected to server2 (wifi Tethering)
+ doReturn(listOf(client2)).`when`(server2).allLeases
+ assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers,
+ listOf(wifiClient2), listOf(wifiClient1)))
+
+ // Client 2 L2-disconnected but lease doesn't expired yet
+ assertSameClients(listOf(client1), assertNewClients(tracker, servers,
+ wifiClients = emptyList()))
+
+ // Client 1 lease lost but still L2-connected
+ doReturn(emptyList<TetheredClient>()).`when`(server1).allLeases
+ val client1WithoutAddr = TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)
+ assertSameClients(listOf(client1WithoutAddr), assertNewClients(tracker, servers))
+
+ // Client 1 L2-disconnected
+ assertSameClients(emptyList(), assertNewClients(tracker, servers,
+ localOnlyClients = emptyList()))
}
@Test
fun testUpdateConnectedClients_LeaseExpiration() {
+ doReturn(IpServer.STATE_TETHERED).`when`(server1).servingMode()
+ doReturn(IpServer.STATE_TETHERED).`when`(server2).servingMode()
val tracker = ConnectedClientsTracker(clock)
doReturn(listOf(client1, client2)).`when`(server1).allLeases
doReturn(listOf(client3)).`when`(server2).allLeases
assertSameClients(listOf(client1, client2, client3), assertNewClients(
- tracker, servers, listOf(wifiClient1, wifiClient2)))
+ tracker, servers, wifiClients = listOf(wifiClient1, wifiClient2)))
clock.time += 20
// Client 3 has no remaining lease: removed
@@ -131,15 +204,16 @@
// Only the "t + 30" address is left, the "t + 10" address expired
listOf(client2Exp30AddrInfo),
TETHERING_WIFI))
- assertSameClients(expectedClients, assertNewClients(tracker, servers, null))
+ assertSameClients(expectedClients, assertNewClients(tracker, servers))
}
private fun assertNewClients(
tracker: ConnectedClientsTracker,
ipServers: Iterable<IpServer>,
- wifiClients: List<WifiClient>?
+ wifiClients: List<WifiClient>? = null,
+ localOnlyClients: List<WifiClient>? = null
): List<TetheredClient> {
- assertTrue(tracker.updateConnectedClients(ipServers, wifiClients))
+ assertTrue(tracker.updateConnectedClients(ipServers, wifiClients, localOnlyClients))
return tracker.lastTetheredClients
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index e4263db..c2e1617 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -213,7 +213,8 @@
}
@Override
- PendingIntent createRecheckAlarmIntent() {
+ PendingIntent createRecheckAlarmIntent(final String pkgName) {
+ assertEquals(TEST_PACKAGE_NAME, pkgName);
return mAlarmIntent;
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
index 0d686ed..9e287a0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/FakeTetheringConfiguration.java
@@ -19,22 +19,26 @@
import android.content.Context;
import android.content.res.Resources;
+import androidx.annotation.NonNull;
+
import com.android.net.module.util.SharedLog;
/** FakeTetheringConfiguration is used to override static method for testing. */
public class FakeTetheringConfiguration extends TetheringConfiguration {
FakeTetheringConfiguration(Context ctx, SharedLog log, int id) {
- super(ctx, log, id);
- }
+ super(ctx, log, id, new Dependencies() {
+ @Override
+ boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+ @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
+ return defaultEnabled;
+ }
- @Override
- protected String getDeviceConfigProperty(final String name) {
- return null;
- }
-
- @Override
- protected boolean isFeatureEnabled(Context ctx, String namespace, String featureVersionFlag) {
- return false;
+ @Override
+ boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
+ boolean defaultValue) {
+ return defaultValue;
+ }
+ });
}
@Override
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 55d9852..91b092a 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -19,6 +19,8 @@
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
+import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
@@ -61,6 +63,7 @@
private static final String TEST_IFNAME = "test0";
@Mock private IpServer mHotspotIpServer;
+ @Mock private IpServer mLocalHotspotIpServer;
@Mock private IpServer mUsbIpServer;
@Mock private IpServer mEthernetIpServer;
@Mock private IpServer mWifiP2pIpServer;
@@ -90,6 +93,7 @@
when(mUsbIpServer.interfaceType()).thenReturn(TETHERING_USB);
when(mEthernetIpServer.interfaceType()).thenReturn(TETHERING_ETHERNET);
when(mHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
+ when(mLocalHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI);
when(mWifiP2pIpServer.interfaceType()).thenReturn(TETHERING_WIFI_P2P);
}
@@ -104,9 +108,10 @@
mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig));
}
- private LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) {
+ private LinkAddress requestDownstreamAddress(final IpServer ipServer, int scope,
+ boolean useLastAddress) {
final LinkAddress address = mPrivateAddressCoordinator.requestDownstreamAddress(
- ipServer, useLastAddress);
+ ipServer, scope, useLastAddress);
when(ipServer.getAddress()).thenReturn(address);
return address;
}
@@ -115,19 +120,19 @@
public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception {
final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress);
final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix hotspotPrefix = asIpPrefix(address);
assertNotEquals(hotspotPrefix, bluetoothPrefix);
final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix testDupRequest = asIpPrefix(newAddress);
assertNotEquals(hotspotPrefix, testDupRequest);
assertNotEquals(bluetoothPrefix, testDupRequest);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix usbPrefix = asIpPrefix(usbAddress);
assertNotEquals(usbPrefix, bluetoothPrefix);
assertNotEquals(usbPrefix, hotspotPrefix);
@@ -139,25 +144,28 @@
int fakeSubAddr = 0x2b00; // 43.0.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
fakeSubAddr = 0x2d01; // 45.1.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */);
+ actualAddress = requestDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
fakeSubAddr = 0x2eff; // 46.255.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */);
+ actualAddress = requestDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
fakeSubAddr = 0x2f05; // 47.5.
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr);
- actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */);
+ actualAddress = requestDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
}
@@ -168,7 +176,7 @@
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mBluetoothAddress.getAddress().getAddress()));
final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
@@ -177,7 +185,7 @@
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(hotspotAddress.getAddress().getAddress()));
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix usbPrefix = asIpPrefix(usbAddress);
assertNotEquals(asIpPrefix(mBluetoothAddress), usbPrefix);
assertNotEquals(hotspotPrefix, usbPrefix);
@@ -187,7 +195,7 @@
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer,
- false /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
final IpPrefix etherPrefix = asIpPrefix(etherAddress);
assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix);
assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix);
@@ -200,11 +208,11 @@
final int fakeHotspotSubAddr = 0x2b05; // 43.5
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress);
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
@@ -214,10 +222,10 @@
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals(hotspotAddress, newHotspotAddress);
final LinkAddress newUsbAddress = requestDownstreamAddress(mUsbIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals(usbAddress, newUsbAddress);
final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
@@ -257,7 +265,7 @@
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr);
// - Enable hotspot with prefix 192.168.43.0/24
final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr);
assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix);
// - test mobile network with null NetworkCapabilities. Ideally this should not happen
@@ -311,21 +319,21 @@
// - Restart hotspot again and its prefix is different previous.
mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer);
final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2);
assertNotEquals(hotspotPrefix, hotspotPrefix2);
mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi);
verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT);
// - Usb tethering can be enabled and its prefix is different with conflict one.
final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
final IpPrefix usbPrefix = asIpPrefix(usbAddr);
assertNotEquals(predefinedPrefix, usbPrefix);
assertNotEquals(hotspotPrefix2, usbPrefix);
// - Disable wifi upstream, then wifi's prefix can be selected again.
mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork);
final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
final IpPrefix ethPrefix = asIpPrefix(ethAddr);
assertEquals(predefinedPrefix, ethPrefix);
}
@@ -335,7 +343,7 @@
final int randomAddress = 0x8605; // 134.5
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
// Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5.
assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0);
final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
@@ -345,7 +353,7 @@
// Check whether return address is next prefix of 192.168.134.0/24.
final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1);
final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork,
new LinkAddress("192.168.149.16/19"), null,
@@ -355,7 +363,7 @@
// The conflict range is 128 ~ 159, so the address is 192.168.160.5/24.
final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2);
final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
new LinkAddress("192.168.129.53/18"), null,
@@ -370,7 +378,7 @@
// The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24.
final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3);
final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
new LinkAddress("192.168.188.133/17"), null,
@@ -380,7 +388,7 @@
// Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because
// 192.168.134/24 ~ 192.168.255.255/24 is not available.
final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4);
final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
new LinkAddress("192.168.3.59/21"), null,
@@ -389,7 +397,7 @@
// Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24.
final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5);
final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
new LinkAddress("192.168.68.43/21"), null,
@@ -399,7 +407,7 @@
// Update an upstream that does *not* conflict, check whether return the same address
// 192.168.5/24.
final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6);
final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6,
new LinkAddress("192.168.10.97/21"), null,
@@ -408,7 +416,7 @@
// Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24.
final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7);
final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6,
new LinkAddress("192.168.0.0/17"), null,
@@ -417,7 +425,7 @@
// Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16.
final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8);
}
@@ -426,7 +434,7 @@
final int randomAddress = 0x1f2b2a; // 31.43.42
when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress);
final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
// Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42.
assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1);
final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork,
@@ -437,7 +445,7 @@
// Check whether return address is next address of prefix 192.168.128.0/17.
final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2);
final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork,
new LinkAddress("192.1.2.3/8"), null,
@@ -447,7 +455,7 @@
// Check whether return address is under prefix 172.16.0.0/12.
final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1);
final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2,
new LinkAddress("172.28.123.100/14"), null,
@@ -458,12 +466,12 @@
// 172.28.0.0 ~ 172.31.255.255 is not available.
// Check whether return address is next address of prefix 172.16.0.0/14.
final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2);
// Check whether new downstream is next address of address 172.16.0.42/24.
final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3);
final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3,
new LinkAddress("172.16.0.1/24"), null,
@@ -474,7 +482,7 @@
// Check whether return address is next address of prefix 172.16.1.42/24.
final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4);
final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4,
new LinkAddress("172.16.0.1/13"), null,
@@ -485,11 +493,11 @@
// Check whether return address is next address of prefix 172.16.0.1/13.
final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5);
// Check whether return address is next address of prefix 172.24.0.42/24.
final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6);
final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5,
new LinkAddress("172.24.0.1/12"), null,
@@ -500,11 +508,11 @@
// Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42.
final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1);
// Check whether new downstream is next address of address 10.31.43.42/24.
final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2);
}
@@ -524,7 +532,7 @@
private void assertReseveredWifiP2pPrefix() throws Exception {
LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
final IpPrefix hotspotPrefix = asIpPrefix(address);
final IpPrefix legacyWifiP2pPrefix = asIpPrefix(mLegacyWifiP2pAddress);
assertNotEquals(legacyWifiP2pPrefix, hotspotPrefix);
@@ -544,8 +552,23 @@
// If #shouldEnableWifiP2pDedicatedIp() is enabled, wifi P2P gets the configured address.
LinkAddress address = requestDownstreamAddress(mWifiP2pIpServer,
- true /* useLastAddress */);
+ CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
assertEquals(mLegacyWifiP2pAddress, address);
mPrivateAddressCoordinator.releaseDownstream(mWifiP2pIpServer);
}
+
+ @Test
+ public void testEnableSapAndLohsConcurrently() throws Exception {
+ // 0x2b05 -> 43.5, 0x8605 -> 134.5
+ when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(0x2b05, 0x8605);
+
+ final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
+ CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
+ assertEquals("Wrong hotspot prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress);
+
+ final LinkAddress localHotspotAddress = requestDownstreamAddress(mLocalHotspotIpServer,
+ CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
+ assertEquals("Wrong local hotspot prefix: ", new LinkAddress("192.168.134.5/24"),
+ localHotspotAddress);
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index f662c02..3382af8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -21,14 +21,13 @@
import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
import static android.net.ConnectivityManager.TYPE_WIFI;
-import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.telephony.CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL;
import static android.telephony.CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
+import static com.android.networkstack.tethering.TetheringConfiguration.OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER;
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS;
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION;
import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION;
@@ -39,6 +38,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import android.content.Context;
@@ -49,12 +49,13 @@
import android.content.res.Resources;
import android.os.Build;
import android.os.PersistableBundle;
-import android.provider.DeviceConfig;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
import android.telephony.TelephonyManager;
import android.test.mock.MockContentResolver;
+import android.util.ArrayMap;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -73,8 +74,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
+import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import java.util.Iterator;
@@ -102,13 +102,13 @@
@Mock private ModuleInfo mMi;
private Context mMockContext;
private boolean mHasTelephonyManager;
- private MockitoSession mMockingSession;
private MockContentResolver mContentResolver;
private final PersistableBundle mCarrierConfig = new PersistableBundle();
+ private final MockDependencies mDeps = new MockDependencies();
private class MockTetheringConfiguration extends TetheringConfiguration {
MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
- super(ctx, log, id);
+ super(ctx, log, id, mDeps);
}
@Override
@@ -151,19 +151,43 @@
}
}
+ private static class MockDependencies extends TetheringConfiguration.Dependencies {
+ private ArrayMap<String, Boolean> mMockFlags = new ArrayMap<>();
+
+ @Override
+ boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
+ @NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
+ return isMockFlagEnabled(name, defaultEnabled);
+ }
+
+ @Override
+ boolean getDeviceConfigBoolean(@NonNull String namespace, @NonNull String name,
+ boolean defaultValue) {
+ // Flags should use isFeatureEnabled instead of getBoolean; see comments in
+ // DeviceConfigUtils. getBoolean should only be used for the two legacy flags below.
+ assertTrue(OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD.equals(name)
+ || TETHER_ENABLE_LEGACY_DHCP_SERVER.equals(name));
+
+ // Use the same mocking strategy as isFeatureEnabled for testing
+ return isMockFlagEnabled(name, defaultValue);
+ }
+
+ private boolean isMockFlagEnabled(@NonNull String name, boolean defaultEnabled) {
+ final Boolean flag = mMockFlags.getOrDefault(name, defaultEnabled);
+ // Value in the map can also be null
+ if (flag != null) return flag;
+ return defaultEnabled;
+ }
+
+ void setFeatureEnabled(@NonNull String flag, Boolean enabled) {
+ mMockFlags.put(flag, enabled);
+ }
+ }
+
@Before
public void setUp() throws Exception {
- // TODO: use a dependencies class instead of mock statics.
- mMockingSession = mockitoSession()
- .initMocks(this)
- .mockStatic(DeviceConfig.class)
- .strictness(Strictness.WARN)
- .startMocking();
- DeviceConfigUtils.resetPackageVersionCacheForTest();
- doReturn(null).when(
- () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
- eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
- setTetherForceUpstreamAutomaticFlagVersion(null);
+ MockitoAnnotations.initMocks(this);
+ setTetherForceUpstreamAutomaticFlagEnabled(null);
final PackageInfo pi = new PackageInfo();
pi.setLongVersionCode(TEST_PACKAGE_VERSION);
@@ -202,7 +226,6 @@
@After
public void tearDown() throws Exception {
- mMockingSession.finishMocking();
DeviceConfigUtils.resetPackageVersionCacheForTest();
// Call {@link #clearSettingsProvider()} before and after using FakeSettingsProvider.
FakeSettingsProvider.clearSettingsProvider();
@@ -211,7 +234,7 @@
private TetheringConfiguration getTetheringConfiguration(int... legacyTetherUpstreamTypes) {
when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(
legacyTetherUpstreamTypes);
- return new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ return new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
}
@Test
@@ -297,7 +320,7 @@
when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
final Iterator<Integer> upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator();
assertTrue(upstreamIterator.hasNext());
assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue());
@@ -320,7 +343,7 @@
when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
final Iterator<Integer> upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator();
assertTrue(upstreamIterator.hasNext());
assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue());
@@ -338,7 +361,7 @@
when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false);
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
final Iterator<Integer> upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator();
assertTrue(upstreamIterator.hasNext());
assertEquals(TYPE_WIFI, upstreamIterator.next().intValue());
@@ -350,27 +373,26 @@
}
private void initializeBpfOffloadConfiguration(
- final boolean fromRes, final String fromDevConfig) {
+ final boolean fromRes, final Boolean fromDevConfig) {
when(mResources.getBoolean(R.bool.config_tether_enable_bpf_offload)).thenReturn(fromRes);
- doReturn(fromDevConfig).when(
- () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
- eq(TetheringConfiguration.OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD)));
+ mDeps.setFeatureEnabled(
+ TetheringConfiguration.OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD, fromDevConfig);
}
@Test
public void testBpfOffloadEnabledByResource() {
initializeBpfOffloadConfiguration(true, null /* unset */);
final TetheringConfiguration enableByRes =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(enableByRes.isBpfOffloadEnabled());
}
@Test
public void testBpfOffloadEnabledByDeviceConfigOverride() {
for (boolean res : new boolean[]{true, false}) {
- initializeBpfOffloadConfiguration(res, "true");
+ initializeBpfOffloadConfiguration(res, true);
final TetheringConfiguration enableByDevConOverride =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(enableByDevConOverride.isBpfOffloadEnabled());
}
}
@@ -379,16 +401,16 @@
public void testBpfOffloadDisabledByResource() {
initializeBpfOffloadConfiguration(false, null /* unset */);
final TetheringConfiguration disableByRes =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertFalse(disableByRes.isBpfOffloadEnabled());
}
@Test
public void testBpfOffloadDisabledByDeviceConfigOverride() {
for (boolean res : new boolean[]{true, false}) {
- initializeBpfOffloadConfiguration(res, "false");
+ initializeBpfOffloadConfiguration(res, false);
final TetheringConfiguration disableByDevConOverride =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertFalse(disableByDevConOverride.isBpfOffloadEnabled());
}
}
@@ -397,22 +419,18 @@
public void testNewDhcpServerDisabled() {
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
true);
- doReturn("false").when(
- () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
- eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+ mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER, false);
final TetheringConfiguration enableByRes =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(enableByRes.useLegacyDhcpServer());
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
false);
- doReturn("true").when(
- () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
- eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+ mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER, true);
final TetheringConfiguration enableByDevConfig =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(enableByDevConfig.useLegacyDhcpServer());
}
@@ -420,12 +438,10 @@
public void testNewDhcpServerEnabled() {
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn(
false);
- doReturn("false").when(
- () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
- eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+ mDeps.setFeatureEnabled(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER, false);
final TetheringConfiguration cfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertFalse(cfg.useLegacyDhcpServer());
}
@@ -433,7 +449,7 @@
@Test
public void testOffloadIntervalByResource() {
final TetheringConfiguration intervalByDefault =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS,
intervalByDefault.getOffloadPollInterval());
@@ -442,7 +458,7 @@
when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn(
override);
final TetheringConfiguration overrideByRes =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(override, overrideByRes.getOffloadPollInterval());
}
}
@@ -451,7 +467,7 @@
public void testGetResourcesBySubId() {
setUpResourceForSubId();
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(cfg.provisioningApp.length == 0);
final int anyValidSubId = 1;
final MockTetheringConfiguration mockCfg =
@@ -493,7 +509,7 @@
mockService(Context.CARRIER_CONFIG_SERVICE,
CarrierConfigManager.class, null);
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(cfg.isCarrierSupportTethering);
assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
@@ -506,7 +522,7 @@
CarrierConfigManager.class, mCarrierConfigManager);
when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(null);
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(cfg.isCarrierSupportTethering);
assertTrue(cfg.isCarrierConfigAffirmsEntitlementCheckRequired);
@@ -521,7 +537,7 @@
mCarrierConfig.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig);
final TetheringConfiguration cfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
if (SdkLevel.isAtLeastT()) {
assertFalse(cfg.isCarrierSupportTethering);
@@ -535,13 +551,13 @@
@Test
public void testEnableLegacyWifiP2PAddress() throws Exception {
final TetheringConfiguration defaultCfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertFalse(defaultCfg.shouldEnableWifiP2pDedicatedIp());
when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip))
.thenReturn(true);
final TetheringConfiguration testCfg = new TetheringConfiguration(
- mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertTrue(testCfg.shouldEnableWifiP2pDedicatedIp());
}
@@ -576,16 +592,13 @@
public void testChooseUpstreamAutomatically_FlagOverride() throws Exception {
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
.thenReturn(false);
- setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY,
- TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION, APEX_NAME, false));
-
+ setTetherForceUpstreamAutomaticFlagEnabled(true);
assertChooseUpstreamAutomaticallyIs(true);
- setTetherForceUpstreamAutomaticFlagVersion(0L);
+ setTetherForceUpstreamAutomaticFlagEnabled(null);
assertChooseUpstreamAutomaticallyIs(false);
- setTetherForceUpstreamAutomaticFlagVersion(Long.MAX_VALUE);
+ setTetherForceUpstreamAutomaticFlagEnabled(false);
assertChooseUpstreamAutomaticallyIs(false);
}
@@ -593,7 +606,7 @@
public void testChooseUpstreamAutomatically_FlagOverrideOnSAndT() throws Exception {
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
.thenReturn(false);
- setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+ setTetherForceUpstreamAutomaticFlagEnabled(true);
assertChooseUpstreamAutomaticallyIs(false);
}
@@ -604,28 +617,24 @@
// TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION is.
when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
.thenReturn(false);
- setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY,
- TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION, APEX_NAME, false));
-
+ setTetherForceUpstreamAutomaticFlagEnabled(true);
assertChooseUpstreamAutomaticallyIs(true);
- setTetherForceUpstreamAutomaticFlagVersion(0L);
+ setTetherForceUpstreamAutomaticFlagEnabled(null);
assertChooseUpstreamAutomaticallyIs(true);
- setTetherForceUpstreamAutomaticFlagVersion(Long.MAX_VALUE);
+ setTetherForceUpstreamAutomaticFlagEnabled(false);
assertChooseUpstreamAutomaticallyIs(true);
}
- private void setTetherForceUpstreamAutomaticFlagVersion(Long version) {
- doReturn(version == null ? null : Long.toString(version)).when(
- () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
- eq(TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION)));
+ private void setTetherForceUpstreamAutomaticFlagEnabled(Boolean enabled) {
+ mDeps.setFeatureEnabled(
+ TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION, enabled);
}
private void assertChooseUpstreamAutomaticallyIs(boolean value) {
- assertEquals(value, new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID)
- .chooseUpstreamAutomatically);
+ assertEquals(value, new TetheringConfiguration(
+ mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps).chooseUpstreamAutomatically);
}
@Test
@@ -654,7 +663,7 @@
private void assertIsUsingNcm(boolean expected) {
final TetheringConfiguration cfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(expected, cfg.isUsingNcm());
}
@@ -704,7 +713,7 @@
private void assertUsbAndNcmRegexs(final String[] usbRegexs, final String[] ncmRegexs) {
final TetheringConfiguration cfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertArrayEquals(usbRegexs, cfg.tetherableUsbRegexs);
assertArrayEquals(ncmRegexs, cfg.tetherableNcmRegexs);
}
@@ -716,28 +725,28 @@
final int defaultSubnetPrefixLength = 0;
final TetheringConfiguration defaultCfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(defaultSubnetPrefixLength, defaultCfg.getP2pLeasesSubnetPrefixLength());
final int prefixLengthTooSmall = -1;
when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
prefixLengthTooSmall);
final TetheringConfiguration tooSmallCfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(defaultSubnetPrefixLength, tooSmallCfg.getP2pLeasesSubnetPrefixLength());
final int prefixLengthTooLarge = 31;
when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
prefixLengthTooLarge);
final TetheringConfiguration tooLargeCfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(defaultSubnetPrefixLength, tooLargeCfg.getP2pLeasesSubnetPrefixLength());
final int p2pLeasesSubnetPrefixLength = 27;
when(mResources.getInteger(R.integer.config_p2p_leases_subnet_prefix_length)).thenReturn(
p2pLeasesSubnetPrefixLength);
final TetheringConfiguration p2pCfg =
- new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+ new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID, mDeps);
assertEquals(p2pLeasesSubnetPrefixLength, p2pCfg.getP2pLeasesSubnetPrefixLength());
}
}
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 229dce3..b3f8ed6 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -103,18 +103,6 @@
}
bpf {
- name: "offload@inprocess.o",
- srcs: ["offload@inprocess.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- "-DBTF",
- "-DINPROCESS",
- ],
-}
-
-bpf {
name: "test.o",
srcs: ["test.c"],
cflags: [
@@ -135,18 +123,6 @@
}
bpf {
- name: "test@inprocess.o",
- srcs: ["test@inprocess.c"],
- btf: true,
- cflags: [
- "-Wall",
- "-Werror",
- "-DBTF",
- "-DINPROCESS",
- ],
-}
-
-bpf {
name: "clatd.o",
srcs: ["clatd.c"],
btf: true,
diff --git a/bpf_progs/offload.c b/bpf_progs/offload.c
index f4d4254..80d1a41 100644
--- a/bpf_progs/offload.c
+++ b/bpf_progs/offload.c
@@ -38,13 +38,7 @@
// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
#define TETHERING_UID AID_ROOT
-#ifdef INPROCESS
-#define DEFAULT_BPF_MAP_SELINUX_CONTEXT "fs_bpf_net_shared"
-#define DEFAULT_BPF_PROG_SELINUX_CONTEXT "fs_bpf_net_shared"
-#define TETHERING_GID AID_SYSTEM
-#else
#define TETHERING_GID AID_NETWORK_STACK
-#endif
#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
diff --git a/bpf_progs/offload@inprocess.c b/bpf_progs/offload@inprocess.c
deleted file mode 120000
index 4092e0d..0000000
--- a/bpf_progs/offload@inprocess.c
+++ /dev/null
@@ -1 +0,0 @@
-offload.c
\ No newline at end of file
diff --git a/bpf_progs/test.c b/bpf_progs/test.c
index d1f780f..091743c 100644
--- a/bpf_progs/test.c
+++ b/bpf_progs/test.c
@@ -32,13 +32,7 @@
// Warning: values other than AID_ROOT don't work for map uid on BpfLoader < v0.21
#define TETHERING_UID AID_ROOT
-#ifdef INPROCESS
-#define DEFAULT_BPF_MAP_SELINUX_CONTEXT "fs_bpf_net_shared"
-#define DEFAULT_BPF_PROG_SELINUX_CONTEXT "fs_bpf_net_shared"
-#define TETHERING_GID AID_SYSTEM
-#else
#define TETHERING_GID AID_NETWORK_STACK
-#endif
// This is non production code, only used for testing
// Needed because the bitmap array definition is non-kosher for pre-T OS devices.
diff --git a/bpf_progs/test@inprocess.c b/bpf_progs/test@inprocess.c
deleted file mode 120000
index aeebb26..0000000
--- a/bpf_progs/test@inprocess.c
+++ /dev/null
@@ -1 +0,0 @@
-test.c
\ No newline at end of file
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 87b0a64..6613ee6 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);
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index d119db6..2930cbd 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -429,6 +429,10 @@
private final DiscoveryListener mWrapped;
private final Executor mWrappedExecutor;
private final ArraySet<TrackedNsdInfo> mFoundInfo = new ArraySet<>();
+ // When this flag is set to true, no further service found or lost callbacks should be
+ // handled. This flag indicates that the network for this DelegatingDiscoveryListener is
+ // lost, and any further callbacks would be redundant.
+ private boolean mAllServicesLost = false;
private DelegatingDiscoveryListener(Network network, DiscoveryListener listener,
Executor executor) {
@@ -445,6 +449,7 @@
serviceInfo.setNetwork(mNetwork);
mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo));
}
+ mAllServicesLost = true;
}
@Override
@@ -486,12 +491,22 @@
@Override
public void onServiceFound(NsdServiceInfo serviceInfo) {
+ if (mAllServicesLost) {
+ // This DelegatingDiscoveryListener no longer has a network connection. Ignore
+ // the callback.
+ return;
+ }
mFoundInfo.add(new TrackedNsdInfo(serviceInfo));
mWrappedExecutor.execute(() -> mWrapped.onServiceFound(serviceInfo));
}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
+ if (mAllServicesLost) {
+ // This DelegatingDiscoveryListener no longer has a network connection. Ignore
+ // the callback.
+ return;
+ }
mFoundInfo.remove(new TrackedNsdInfo(serviceInfo));
mWrappedExecutor.execute(() -> mWrapped.onServiceLost(serviceInfo));
}
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/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 381a18a..2315521 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -2535,6 +2535,26 @@
}
/**
+ * Get the supported keepalive count for each transport configured in resource overlays.
+ *
+ * @return An array of supported keepalive count for each transport type.
+ * @hide
+ */
+ @RequiresPermission(anyOf = { android.Manifest.permission.NETWORK_SETTINGS,
+ // CTS 13 used QUERY_ALL_PACKAGES to get the resource value, which was implemented
+ // as below in KeepaliveUtils. Also allow that permission so that KeepaliveUtils can
+ // use this method and avoid breaking released CTS. Apps that have this permission
+ // can query the resource themselves anyway.
+ android.Manifest.permission.QUERY_ALL_PACKAGES })
+ public int[] getSupportedKeepalives() {
+ try {
+ return mService.getSupportedKeepalives();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Ensure that a network route exists to deliver traffic to the specified
* host via the specified network interface. An attempt to add a route that
* already exists is ignored, but treated as successful.
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 1372e9a..ebe8bca 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -195,6 +195,8 @@
void stopKeepalive(in ISocketKeepaliveCallback cb);
+ int[] getSupportedKeepalives();
+
String getCaptivePortalServerUrl();
byte[] getNetworkWatchlistConfigHash();
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index df06b83..3cc9c65 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -675,7 +675,7 @@
/**
* Indicates that this network should be able to prioritize latency for the internet.
*
- * Starting with {@code Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, requesting this capability with
+ * Starting with {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, requesting this capability with
* {@link ConnectivityManager#requestNetwork} requires declaration in the self-certified
* network capabilities. See {@link NetworkRequest} for the self-certification documentation.
*/
@@ -684,7 +684,7 @@
/**
* Indicates that this network should be able to prioritize bandwidth for the internet.
*
- * Starting with {@code Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, requesting this capability with
+ * Starting with {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, requesting this capability with
* {@link ConnectivityManager#requestNetwork} requires declaration in the self-certified
* network capabilities. See {@link NetworkRequest} for the self-certification documentation.
*/
diff --git a/framework/src/android/net/NetworkInfo.java b/framework/src/android/net/NetworkInfo.java
index b7ec519..7aa9847 100644
--- a/framework/src/android/net/NetworkInfo.java
+++ b/framework/src/android/net/NetworkInfo.java
@@ -334,6 +334,24 @@
}
/**
+ * Indicates whether this network is suspended.
+ * @deprecated Apps should instead use the
+ * {@link android.net.ConnectivityManager.NetworkCallback} API to
+ * learn about connectivity changes. See
+ * {@link ConnectivityManager#registerDefaultNetworkCallback} and
+ * {@link ConnectivityManager#registerNetworkCallback}. These will
+ * give a more accurate picture of the connectivity state of
+ * the device and let apps react more easily and quickly to changes.
+ * @hide
+ */
+ @Deprecated
+ public boolean isSuspended() {
+ synchronized (this) {
+ return mState == State.SUSPENDED;
+ }
+ }
+
+ /**
* Indicates whether network connectivity is possible. A network is unavailable
* when a persistent or semi-persistent condition prevents the possibility
* of connecting to that network. Examples include
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 8843fb3..6c351d0 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -123,9 +123,9 @@
* Look up the specific capability and the {@link ConnectivityManager#requestNetwork}
* method for limitations applicable to each capability.
*
- * <p>Also, starting with {@code Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, some capabilities
+ * <p>Also, starting with {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, some capabilities
* require the application to self-certify by explicitly adding the
- * {@code android.content.pm.PackageManager#PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES}
+ * {@link android.content.pm.PackageManager#PROPERTY_SELF_CERTIFIED_NETWORK_CAPABILITIES}
* property in the AndroidManifest.xml, which points to an XML resource file. In the
* XML resource file, the application declares what kind of network capabilities the application
* wants to have.
diff --git a/framework/src/android/net/ProxyInfo.java b/framework/src/android/net/ProxyInfo.java
index 0deda37..adf2376 100644
--- a/framework/src/android/net/ProxyInfo.java
+++ b/framework/src/android/net/ProxyInfo.java
@@ -47,6 +47,8 @@
private final int mPort;
private final String mExclusionList;
private final String[] mParsedExclusionList;
+ // Uri.EMPTY if none.
+ @NonNull
private final Uri mPacFileUrl;
/**
@@ -256,6 +258,14 @@
return proxy;
}
+ /**
+ * @hide
+ * @return whether this proxy uses a Proxy Auto Configuration URL.
+ */
+ public boolean isPacProxy() {
+ return !Uri.EMPTY.equals(mPacFileUrl);
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index 10daf17..f915e72 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -149,9 +149,7 @@
public static final int ERROR_INSUFFICIENT_RESOURCES = -32;
/**
- * There was no such slot. This should only be internally as it indicates
- * a programming error in the system server. It should not propagate to
- * applications.
+ * There was no such slot, or no keepalive running on this slot.
* @hide
*/
@SystemApi
diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java
index 64f14a1..fae2499 100644
--- a/framework/src/android/net/apf/ApfCapabilities.java
+++ b/framework/src/android/net/apf/ApfCapabilities.java
@@ -19,9 +19,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
-import android.content.Context;
-import android.content.res.Resources;
-import android.net.ConnectivityResources;
import android.os.Parcel;
import android.os.Parcelable;
@@ -36,8 +33,6 @@
*/
@SystemApi
public final class ApfCapabilities implements Parcelable {
- private static ConnectivityResources sResources;
-
/**
* Version of APF instruction set supported for packet filtering. 0 indicates no support for
* packet filtering using APF programs.
@@ -67,15 +62,6 @@
apfPacketFormat = in.readInt();
}
- @NonNull
- private static synchronized ConnectivityResources getResources(@NonNull Context ctx) {
- if (sResources == null) {
- sResources = new ConnectivityResources(ctx);
- }
- return sResources;
- }
-
-
@Override
public int describeContents() {
return 0;
diff --git a/framework/src/android/net/util/KeepaliveUtils.java b/framework/src/android/net/util/KeepaliveUtils.java
index 8d7a0b3..07b8c45 100644
--- a/framework/src/android/net/util/KeepaliveUtils.java
+++ b/framework/src/android/net/util/KeepaliveUtils.java
@@ -18,11 +18,8 @@
import android.annotation.NonNull;
import android.content.Context;
-import android.content.res.Resources;
-import android.net.ConnectivityResources;
+import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
-import android.text.TextUtils;
-import android.util.AndroidRuntimeException;
/**
* Collection of utilities for socket keepalive offload.
@@ -33,64 +30,20 @@
public static final String TAG = "KeepaliveUtils";
- public static class KeepaliveDeviceConfigurationException extends AndroidRuntimeException {
- public KeepaliveDeviceConfigurationException(final String msg) {
- super(msg);
- }
- }
-
/**
* Read supported keepalive count for each transport type from overlay resource. This should be
* used to create a local variable store of resource customization, and use it as the input for
- * {@link getSupportedKeepalivesForNetworkCapabilities}.
+ * {@link #getSupportedKeepalivesForNetworkCapabilities}.
*
* @param context The context to read resource from.
* @return An array of supported keepalive count for each transport type.
+ * @deprecated This is used by CTS 13, but can be removed after switching it to
+ * {@link ConnectivityManager#getSupportedKeepalives()}.
*/
@NonNull
+ @Deprecated
public static int[] getSupportedKeepalives(@NonNull Context context) {
- String[] res = null;
- try {
- final ConnectivityResources connRes = new ConnectivityResources(context);
- // TODO: use R.id.config_networkSupportedKeepaliveCount directly
- final int id = connRes.get().getIdentifier("config_networkSupportedKeepaliveCount",
- "array", connRes.getResourcesContext().getPackageName());
- res = new ConnectivityResources(context).get().getStringArray(id);
- } catch (Resources.NotFoundException unused) {
- }
- if (res == null) throw new KeepaliveDeviceConfigurationException("invalid resource");
-
- final int[] ret = new int[NetworkCapabilities.MAX_TRANSPORT + 1];
- for (final String row : res) {
- if (TextUtils.isEmpty(row)) {
- throw new KeepaliveDeviceConfigurationException("Empty string");
- }
- final String[] arr = row.split(",");
- if (arr.length != 2) {
- throw new KeepaliveDeviceConfigurationException("Invalid parameter length");
- }
-
- int transport;
- int supported;
- try {
- transport = Integer.parseInt(arr[0]);
- supported = Integer.parseInt(arr[1]);
- } catch (NumberFormatException e) {
- throw new KeepaliveDeviceConfigurationException("Invalid number format");
- }
-
- if (!NetworkCapabilities.isValidTransport(transport)) {
- throw new KeepaliveDeviceConfigurationException("Invalid transport " + transport);
- }
-
- if (supported < 0) {
- throw new KeepaliveDeviceConfigurationException(
- "Invalid supported count " + supported + " for "
- + NetworkCapabilities.transportNameOf(transport));
- }
- ret[transport] = supported;
- }
- return ret;
+ return context.getSystemService(ConnectivityManager.class).getSupportedKeepalives();
}
/**
diff --git a/nearby/TEST_MAPPING b/nearby/TEST_MAPPING
index d68bcc9..dbaca33 100644
--- a/nearby/TEST_MAPPING
+++ b/nearby/TEST_MAPPING
@@ -8,9 +8,6 @@
},
{
"name": "NearbyIntegrationUntrustedTests"
- },
- {
- "name": "NearbyIntegrationUiTests"
}
],
"postsubmit": [
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 278f823..a8a6eaa 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -52,5 +52,7 @@
static_libs: [
"modules-utils-preconditions",
],
- visibility: ["//packages/modules/Connectivity/nearby/tests:__subpackages__"],
+ visibility: [
+ "//packages/modules/Connectivity/nearby/tests:__subpackages__",
+ "//packages/modules/Connectivity/nearby/halfsheet:__subpackages__"],
}
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/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index feed71b..a70b303 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;
@@ -37,6 +38,7 @@
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
@@ -73,6 +75,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)
@@ -270,29 +273,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 {
@@ -362,10 +378,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);
}
});
}
@@ -404,4 +420,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 7efa5e6..b5d9ad4 100644
--- a/nearby/framework/java/android/nearby/PresenceDevice.java
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -375,7 +375,6 @@
return this;
}
-
/**
* Sets the image url of the discovered Presence device.
*
@@ -387,7 +386,6 @@
return this;
}
-
/**
* Sets discovery timestamp, the clock is based on elapsed time.
*
@@ -399,7 +397,6 @@
return this;
}
-
/**
* Adds an extended property of the discovered presence device.
*
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 9421820..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.
@@ -78,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++) {
@@ -95,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;
}
@@ -168,6 +174,13 @@
}
/**
+ * Returns if CHRE enabled for scanning.
+ */
+ public boolean isOffloadOnly() {
+ return mOffloadOnly;
+ }
+
+ /**
* Returns Scan Filters for this request.
*/
@NonNull
@@ -203,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("]");
@@ -215,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);
@@ -230,6 +250,7 @@
return mScanType == otherRequest.mScanType
&& (mScanMode == otherRequest.mScanMode)
&& (mBleEnabled == otherRequest.mBleEnabled)
+ && (mOffloadOnly == otherRequest.mOffloadOnly)
&& (Objects.equals(mWorkSource, otherRequest.mWorkSource));
}
return false;
@@ -237,7 +258,7 @@
@Override
public int hashCode() {
- return Objects.hash(mScanType, mScanMode, mBleEnabled, mWorkSource);
+ return Objects.hash(mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource);
}
/** @hide **/
@@ -260,6 +281,7 @@
private @ScanMode int mScanMode;
private boolean mBleEnabled;
+ private boolean mOffloadOnly;
private WorkSource mWorkSource;
private List<ScanFilter> mScanFilters;
@@ -267,6 +289,7 @@
public Builder() {
mScanType = INVALID_SCAN_TYPE;
mBleEnabled = true;
+ mOffloadOnly = false;
mWorkSource = new WorkSource();
mScanFilters = new ArrayList<>();
}
@@ -307,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.
@@ -361,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/halfsheet/Android.bp b/nearby/halfsheet/Android.bp
index 8011dc6..6190d45 100644
--- a/nearby/halfsheet/Android.bp
+++ b/nearby/halfsheet/Android.bp
@@ -20,6 +20,7 @@
name: "HalfSheetUX",
defaults: ["platform_app_defaults"],
srcs: ["src/**/*.java"],
+ resource_dirs: ["res"],
sdk_version: "module_current",
// This is included in tethering apex, which uses min SDK 30
min_sdk_version: "30",
@@ -28,7 +29,7 @@
libs: [
"framework-bluetooth",
"framework-connectivity-t.impl",
- "nearby-service-string",
+ "framework-nearby-static",
],
static_libs: [
"androidx.annotation_annotation",
diff --git a/nearby/halfsheet/res/drawable/quantum_ic_devices_other_vd_theme_24.xml b/nearby/halfsheet/res/drawable/quantum_ic_devices_other_vd_theme_24.xml
new file mode 100644
index 0000000..3dcfdee
--- /dev/null
+++ b/nearby/halfsheet/res/drawable/quantum_ic_devices_other_vd_theme_24.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M3,6h18L21,4L3,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h4v-2L3,18L3,6zM13,12L9,12v1.78c-0.61,0.55 -1,1.33 -1,2.22s0.39,1.67 1,2.22L9,20h4v-1.78c0.61,-0.55 1,-1.34 1,-2.22s-0.39,-1.67 -1,-2.22L13,12zM11,17.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM22,8h-6c-0.5,0 -1,0.5 -1,1v10c0,0.5 0.5,1 1,1h6c0.5,0 1,-0.5 1,-1L23,9c0,-0.5 -0.5,-1 -1,-1zM21,18h-4v-8h4v8z"/>
+</vector>
diff --git a/nearby/halfsheet/res/layout-land/fast_pair_device_pairing_fragment.xml b/nearby/halfsheet/res/layout-land/fast_pair_device_pairing_fragment.xml
new file mode 100644
index 0000000..545f7fa
--- /dev/null
+++ b/nearby/halfsheet/res/layout-land/fast_pair_device_pairing_fragment.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:ignore="RtlCompat"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/image_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="12dp"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp">
+
+ <TextView
+ android:id="@+id/header_subtitle"
+ android:textColor="@color/fast_pair_half_sheet_subtitle_color"
+ android:fontFamily="google-sans"
+ android:textSize="14sp"
+ android:gravity="center"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <ImageView
+ android:id="@+id/pairing_pic"
+ android:layout_width="@dimen/fast_pair_half_sheet_land_image_size"
+ android:layout_height="@dimen/fast_pair_half_sheet_land_image_size"
+ android:paddingTop="18dp"
+ android:paddingBottom="18dp"
+ android:importantForAccessibility="no"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
+
+ <TextView
+ android:id="@+id/pin_code"
+ android:textColor="@color/fast_pair_half_sheet_subtitle_color"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/fast_pair_half_sheet_land_image_size"
+ android:paddingTop="18dp"
+ android:paddingBottom="18dp"
+ android:visibility="invisible"
+ android:textSize="50sp"
+ android:letterSpacing="0.2"
+ android:fontFamily="google-sans-medium"
+ android:gravity="center"
+ android:importantForAccessibility="yes"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/header_subtitle" />
+
+ <ProgressBar
+ android:id="@+id/connect_progressbar"
+ android:layout_width="@dimen/fast_pair_half_sheet_land_image_size"
+ android:layout_height="4dp"
+ android:indeterminate="true"
+ android:indeterminateTint="@color/fast_pair_progress_color"
+ android:indeterminateTintMode="src_in"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_marginBottom="6dp"
+ app:layout_constraintTop_toBottomOf="@+id/pairing_pic"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:id="@+id/mid_part"
+ app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent">
+
+ <ImageView
+ android:id="@+id/info_icon"
+ android:layout_alignParentStart="true"
+ android:contentDescription="@null"
+ android:layout_centerInParent="true"
+ android:layout_marginEnd="10dp"
+ android:layout_toStartOf="@id/connect_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:srcCompat="@drawable/fast_pair_ic_info"
+ android:visibility="invisible" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/connect_btn"
+ android:text="@string/paring_action_connect"
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/fast_pair_half_sheet_image_size"
+ android:layout_centerInParent="true"
+ style="@style/HalfSheetButton" />
+
+ </RelativeLayout>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/settings_btn"
+ android:text="@string/paring_action_settings"
+ android:visibility="invisible"
+ android:layout_height="wrap_content"
+ android:layout_width="@dimen/fast_pair_half_sheet_land_image_size"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
+ style="@style/HalfSheetButton" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/cancel_btn"
+ android:text="@string/paring_action_done"
+ android:visibility="invisible"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/mid_part"
+ android:gravity="start|center_vertical"
+ android:paddingTop="6dp"
+ android:paddingBottom="6dp"
+ android:layout_marginBottom="10dp"
+ style="@style/HalfSheetButtonBorderless"/>
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/setup_btn"
+ android:text="@string/paring_action_launch"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/mid_part"
+ android:paddingTop="6dp"
+ android:paddingBottom="6dp"
+ android:layout_marginBottom="10dp"
+ android:visibility="invisible"
+ android:layout_height="@dimen/fast_pair_half_sheet_bottom_button_height"
+ android:layout_width="wrap_content"
+ style="@style/HalfSheetButton" />
+
+ <!--Empty place holder to prevent pairing button from being cut off by screen-->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="30dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/setup_btn"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</ScrollView>
diff --git a/nearby/halfsheet/res/layout-land/fast_pair_half_sheet.xml b/nearby/halfsheet/res/layout-land/fast_pair_half_sheet.xml
new file mode 100644
index 0000000..e993536
--- /dev/null
+++ b/nearby/halfsheet/res/layout-land/fast_pair_half_sheet.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="RtlCompat, UselessParent, MergeRootFrame"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/card"
+ android:orientation="vertical"
+ android:transitionName="card"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity= "center|bottom"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"
+ android:background="@drawable/half_sheet_bg"
+ android:accessibilityLiveRegion="polite"
+ android:gravity="bottom">
+
+ <RelativeLayout
+ android:id="@+id/toolbar_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="20dp"
+ android:paddingRight="20dp">
+
+ <ImageView
+ android:layout_marginTop="9dp"
+ android:layout_marginBottom="9dp"
+ android:id="@+id/toolbar_image"
+ android:layout_width="42dp"
+ android:layout_height="42dp"
+ android:contentDescription="@null"
+ android:layout_toStartOf="@id/toolbar_title"
+ android:layout_centerHorizontal="true"
+ android:visibility="invisible"/>
+
+ <TextView
+ android:layout_marginTop="18dp"
+ android:layout_marginBottom="18dp"
+ android:layout_centerHorizontal="true"
+ android:id="@+id/toolbar_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="google-sans-medium"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textSize="24sp"
+ android:textColor="@color/fast_pair_half_sheet_text_color"
+ style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+
+ </LinearLayout>
+
+</FrameLayout>
+
diff --git a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
index 7fbe229..77cd1ea 100644
--- a/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
+++ b/nearby/halfsheet/res/layout/fast_pair_device_pairing_fragment.xml
@@ -5,7 +5,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:ignore="RtlCompat"
- android:layout_width="match_parent" android:layout_height="match_parent">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/image_view"
@@ -16,6 +17,7 @@
android:paddingEnd="12dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
+
<TextView
android:id="@+id/header_subtitle"
android:textColor="@color/fast_pair_half_sheet_subtitle_color"
@@ -73,14 +75,15 @@
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/connect_progressbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<ImageView
android:id="@+id/info_icon"
- android:layout_width="24dp"
- android:layout_height="24dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
app:srcCompat="@drawable/fast_pair_ic_info"
android:layout_centerInParent="true"
android:contentDescription="@null"
@@ -113,12 +116,11 @@
android:id="@+id/cancel_btn"
android:text="@string/paring_action_done"
android:visibility="invisible"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="start|center_vertical"
android:layout_marginTop="6dp"
+ android:layout_marginBottom="16dp"
style="@style/HalfSheetButtonBorderless"/>
<com.google.android.material.button.MaterialButton
diff --git a/nearby/halfsheet/res/values-af/strings.xml b/nearby/halfsheet/res/values-af/strings.xml
index 7333e63..b0f5631 100644
--- a/nearby/halfsheet/res/values-af/strings.xml
+++ b/nearby/halfsheet/res/values-af/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Begin tans opstelling …"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Stel toestel op"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Toestel is gekoppel"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Gekoppel aan “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Kon nie koppel nie"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kan nie koppel nie"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Probeer die toestel self saambind"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Probeer om die toestel in saambindmodus te sit"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Toestelle binne bereik"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Toestelle met jou rekening"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Jou gestoorde toestel is reg"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Naby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Toestelle"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Koppel tans …"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Klaar"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Stoor"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Koppel"</string>
diff --git a/nearby/halfsheet/res/values-am/strings.xml b/nearby/halfsheet/res/values-am/strings.xml
index da3b144..7c0aed4 100644
--- a/nearby/halfsheet/res/values-am/strings.xml
+++ b/nearby/halfsheet/res/values-am/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ማዋቀርን በመጀመር ላይ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"መሣሪያ አዋቅር"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"መሣሪያ ተገናኝቷል"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"ከ«%s» ጋር ተገናኝቷል"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"መገናኘት አልተቻለም"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"መገናኘት አልተቻለም"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"በእጅ ከመሣሪያው ጋር ለማጣመር ይሞክሩ"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"መሣሪያውን ወደ ማጣመር ሁነታ ለማስገባት ይሞክሩ"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"በቅርብ ያሉ መሣሪያዎች"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ከመለያዎ ጋር መሣሪያዎች"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"የተቀመጠው መሣሪያዎ ይገኛል"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"በአቅራቢያ"</string>
+ <string name="common_devices" msgid="2635603125608104442">"መሣሪያዎች"</string>
+ <string name="common_connecting" msgid="160531481424245303">"በማገናኘት ላይ…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ባትሪ፦ %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ተጠናቅቋል"</string>
<string name="paring_action_save" msgid="6259357442067880136">"አስቀምጥ"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"አገናኝ"</string>
diff --git a/nearby/halfsheet/res/values-ar/strings.xml b/nearby/halfsheet/res/values-ar/strings.xml
index d0bfce4..089faaa 100644
--- a/nearby/halfsheet/res/values-ar/strings.xml
+++ b/nearby/halfsheet/res/values-ar/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"جارٍ الإعداد…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"إعداد جهاز"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"تمّ إقران الجهاز"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"تم الربط بـ \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"تعذّر الربط"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"يتعذَّر الاتصال"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"جرِّب الإقران يدويًا بالجهاز."</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"جرِّب تشغيل الجهاز في وضع الإقران."</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"الأجهزة القريبة"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"الأجهزة المرتبطة بحسابك"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"جهازك المحفوظ متاح"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"مشاركة عن قرب"</string>
+ <string name="common_devices" msgid="2635603125608104442">"الأجهزة"</string>
+ <string name="common_connecting" msgid="160531481424245303">"جارٍ الاتصال…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"البطارية: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"تم"</string>
<string name="paring_action_save" msgid="6259357442067880136">"حفظ"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ربط"</string>
diff --git a/nearby/halfsheet/res/values-as/strings.xml b/nearby/halfsheet/res/values-as/strings.xml
index 8ff4946..bb9dfcc 100644
--- a/nearby/halfsheet/res/values-as/strings.xml
+++ b/nearby/halfsheet/res/values-as/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ছেটআপ আৰম্ভ কৰি থকা হৈছে…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ডিভাইচ ছেট আপ কৰক"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ডিভাইচ সংযোগ কৰা হ’ল"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”ৰ সৈতে সংযোগ কৰা হ’ল"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"সংযোগ কৰিব পৰা নগ’ল"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"সংযোগ কৰিব পৰা নাই"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ডিভাইচটোৰ সৈতে মেনুৱেলী পেয়াৰ কৰক"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ডিভাইচক পেয়াৰ কৰা ম’ডত ৰাখি চাওক"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"সংযোগ সীমাত থকা ডিভাইচসমূহ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"আপোনাৰ একাউণ্টত সংযোগ হোৱা ডিভাইচবোৰ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"আপুনি ছেভ কৰা ডিভাইচ উপলব্ধ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"নিকটৱৰ্তী"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ডিভাইচ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"সংযোগ কৰি থকা হৈছে…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"বেটাৰী: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"হ’ল"</string>
<string name="paring_action_save" msgid="6259357442067880136">"ছেভ কৰক"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"সংযোগ কৰক"</string>
diff --git a/nearby/halfsheet/res/values-az/strings.xml b/nearby/halfsheet/res/values-az/strings.xml
index af499ef..844963b 100644
--- a/nearby/halfsheet/res/values-az/strings.xml
+++ b/nearby/halfsheet/res/values-az/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Ayarlama başladılır…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Cihazı quraşdırın"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Cihaz qoşulub"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” şəbəkəsinə qoşulub"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Qoşulmaq mümkün olmadı"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Qoşulmaq olmur"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Cihazı manual olaraq birləşdirin"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Cihazı qoşalaşdırma rejiminə qoymağa çalışın"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Əl altında olan cihazlar"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Hesabınızdakı cihazlar"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Yadda saxlanmış cihazınız əlçatandır"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Yaxınlıqda"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Cihazlar"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Qoşulur…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batareya: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Oldu"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Saxlayın"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Qoşun"</string>
diff --git a/nearby/halfsheet/res/values-b+sr+Latn/strings.xml b/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
index eea6b64..fcd1dc6 100644
--- a/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
+++ b/nearby/halfsheet/res/values-b+sr+Latn/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Podešavanje se pokreće…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Podesite uređaj"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezani ste sa uređajem %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspelo"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Povezivanje nije uspelo"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Probajte da uparite ručno sa uređajem"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Probajte da prebacite uređaj u režim uparivanja"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Uređaji u dometu"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Uređaji povezani sa nalogom"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Sačuvani uređaj je dostupan"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"U blizini"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Uređaji"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Povezuje se…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Sačuvaj"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
diff --git a/nearby/halfsheet/res/values-be/strings.xml b/nearby/halfsheet/res/values-be/strings.xml
index a5c1ef6..f469922 100644
--- a/nearby/halfsheet/res/values-be/strings.xml
+++ b/nearby/halfsheet/res/values-be/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Пачынаецца наладжванне…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Наладзьце прыладу"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Прылада падключана"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Падключана да прылады \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Не ўдалося падключыцца"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не ўдалося падключыцца"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Паспрабуйце спалучыць прыладу ўручную"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Перавядзіце прыладу ў рэжым спалучэння"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Прылады ў межах дасягальнасці"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Прылады з вашым уліковым запісам"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Ёсць захаваная вамі прылада"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Паблізу"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Прылады"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Падключэнне…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Узровень зараду: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Гатова"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Захаваць"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Падключыць"</string>
diff --git a/nearby/halfsheet/res/values-bg/strings.xml b/nearby/halfsheet/res/values-bg/strings.xml
index 0ee7aef..a0c5103 100644
--- a/nearby/halfsheet/res/values-bg/strings.xml
+++ b/nearby/halfsheet/res/values-bg/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Настройването се стартира…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Настройване на устройството"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Устройството е свързано"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Установена е връзка с(ъс) „%s“"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Свързването не бе успешно"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не може да се установи връзка"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Опитайте да сдвоите устройството ръчно"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Опитайте да зададете режим на сдвояване за устройството"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Устройства в обхват"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Устройства с профила ви"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Запазеното ви у-во е налице"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"В близост"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Устройства"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Свързва се…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батерия: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Запазване"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Свързване"</string>
diff --git a/nearby/halfsheet/res/values-bn/strings.xml b/nearby/halfsheet/res/values-bn/strings.xml
index 484e35b..4d6afc0 100644
--- a/nearby/halfsheet/res/values-bn/strings.xml
+++ b/nearby/halfsheet/res/values-bn/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"সেট-আপ করা শুরু হচ্ছে…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ডিভাইস সেট-আপ করুন"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ডিভাইস কানেক্ট হয়েছে"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”-এ কানেক্ট করা হয়েছে"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"কানেক্ট করা যায়নি"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"কানেক্ট করা যায়নি"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ডিভাইসে ম্যানুয়ালি পেয়ার করার চেষ্টা করুন"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ডিভাইস \'যোগ করার\' মোডে রাখার চেষ্টা করুন"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"কাছে রয়েছে এমন ডিভাইস"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"আপনার অ্যাকাউন্টের সাথে কানেক্ট থাকা ডিভাইস"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"আপনার সেভ করা ডিভাইস উপলভ্য আছে"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"নিয়ারবাই"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ডিভাইস"</string>
+ <string name="common_connecting" msgid="160531481424245303">"কানেক্ট করা হচ্ছে…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ব্যাটারি: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"হয়ে গেছে"</string>
<string name="paring_action_save" msgid="6259357442067880136">"সেভ করুন"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"কানেক্ট করুন"</string>
diff --git a/nearby/halfsheet/res/values-bs/strings.xml b/nearby/halfsheet/res/values-bs/strings.xml
index 2fc8644..47f13c3 100644
--- a/nearby/halfsheet/res/values-bs/strings.xml
+++ b/nearby/halfsheet/res/values-bs/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pokretanje postavljanja…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Postavi uređaj"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezano s uređajem “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspjelo"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nije moguće povezati"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Pokušajte ručno upariti uređaj"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Pokušajte staviti uređaj u način rada za uparivanje"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Uređaji u dometu"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Uređaji s vašim računom"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Sačuvani uređaj je dostupan"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"U blizini"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Uređaji"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Povezivanje…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Sačuvaj"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
diff --git a/nearby/halfsheet/res/values-ca/strings.xml b/nearby/halfsheet/res/values-ca/strings.xml
index 8912792..44ebc3e 100644
--- a/nearby/halfsheet/res/values-ca/strings.xml
+++ b/nearby/halfsheet/res/values-ca/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciant la configuració…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura el dispositiu"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"El dispositiu s\'ha connectat"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connectat a %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"No s\'ha pogut connectar"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"No es pot establir la connexió"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prova de vincular el dispositiu manualment"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prova d\'activar el mode de vinculació al dispositiu"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositius a l\'abast"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositius amb el teu compte"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositiu desat disponible"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"A prop"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositius"</string>
+ <string name="common_connecting" msgid="160531481424245303">"S\'està connectant…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Fet"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Desa"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connecta"</string>
diff --git a/nearby/halfsheet/res/values-cs/strings.xml b/nearby/halfsheet/res/values-cs/strings.xml
index f5c09de..872eef5 100644
--- a/nearby/halfsheet/res/values-cs/strings.xml
+++ b/nearby/halfsheet/res/values-cs/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Zahajování nastavení…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavení zařízení"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Zařízení připojeno"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Připojeno k zařízení %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nelze se připojit"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nepodařilo se připojit"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Zkuste zařízení spárovat ručně"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Přepněte zařízení do režimu párování"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Zařízení v dosahu"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Zařízení s vaším účtem"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Je dostupné uložené zařízení"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"V okolí"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Zařízení"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Připojování…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterie: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Hotovo"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Uložit"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Připojit"</string>
diff --git a/nearby/halfsheet/res/values-da/strings.xml b/nearby/halfsheet/res/values-da/strings.xml
index 1d937e2..89b221f 100644
--- a/nearby/halfsheet/res/values-da/strings.xml
+++ b/nearby/halfsheet/res/values-da/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Begynder konfiguration…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurer enhed"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheden er forbundet"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Der er oprettet forbindelse til \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Forbindelsen kan ikke oprettes"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kunne ikke forbindes"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prøv at parre med enheden manuelt"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prøv at sætte enheden i parringstilstand"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Enheder inden for rækkevidde"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Enheder med din konto"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Din gemte enhed er tilgængelig"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Tæt på"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Enheder"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Forbinder…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batteri: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Luk"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Gem"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Opret forbindelse"</string>
diff --git a/nearby/halfsheet/res/values-de/strings.xml b/nearby/halfsheet/res/values-de/strings.xml
index 9186a44..de54114 100644
--- a/nearby/halfsheet/res/values-de/strings.xml
+++ b/nearby/halfsheet/res/values-de/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Einrichtung wird gestartet..."</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Gerät einrichten"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Gerät verbunden"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Mit „%s“ verbunden"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Verbindung nicht möglich"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kein Verbindungsaufbau möglich"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Versuche, das Gerät manuell zu koppeln"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Versuche, das Gerät in den Kopplungsmodus zu versetzen"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Geräte in Reichweite"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Geräte mit deinem Konto"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Gespeichertes Gerät verfügbar"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Geräte"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Wird verbunden…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Akkustand: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Fertig"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Speichern"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Verbinden"</string>
diff --git a/nearby/halfsheet/res/values-el/strings.xml b/nearby/halfsheet/res/values-el/strings.xml
index 3e18a93..1ea467a 100644
--- a/nearby/halfsheet/res/values-el/strings.xml
+++ b/nearby/halfsheet/res/values-el/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Έναρξη ρύθμισης…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Ρύθμιση συσκευής"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Η συσκευή συνδέθηκε"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Συνδέθηκε με τη συσκευή %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Αδυναμία σύνδεσης"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Δεν είναι δυνατή η σύνδεση"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Δοκιμάστε να κάνετε μη αυτόματη σύζευξη στη συσκευή"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Δοκιμάστε να θέσετε τη συσκευή σε λειτουργία σύζευξης"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Συσκευές εντός εμβέλειας"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Συσκευές με τον λογαριασμό σας"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Η αποθ. συσκ. είναι διαθέσιμη"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Κοντά"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Συσκευές"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Σύνδεση…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Μπαταρία: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Τέλος"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Αποθήκευση"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Σύνδεση"</string>
diff --git a/nearby/halfsheet/res/values-en-rAU/strings.xml b/nearby/halfsheet/res/values-en-rAU/strings.xml
index d4ed675..b7039a1 100644
--- a/nearby/halfsheet/res/values-en-rAU/strings.xml
+++ b/nearby/halfsheet/res/values-en-rAU/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to \'%s\'"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try pairing to the device manually"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
diff --git a/nearby/halfsheet/res/values-en-rCA/strings.xml b/nearby/halfsheet/res/values-en-rCA/strings.xml
index 6094199..06b3a5e 100644
--- a/nearby/halfsheet/res/values-en-rCA/strings.xml
+++ b/nearby/halfsheet/res/values-en-rCA/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting Setup…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try manually pairing to the device"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
diff --git a/nearby/halfsheet/res/values-en-rGB/strings.xml b/nearby/halfsheet/res/values-en-rGB/strings.xml
index d4ed675..b7039a1 100644
--- a/nearby/halfsheet/res/values-en-rGB/strings.xml
+++ b/nearby/halfsheet/res/values-en-rGB/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to \'%s\'"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try pairing to the device manually"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
diff --git a/nearby/halfsheet/res/values-en-rIN/strings.xml b/nearby/halfsheet/res/values-en-rIN/strings.xml
index d4ed675..b7039a1 100644
--- a/nearby/halfsheet/res/values-en-rIN/strings.xml
+++ b/nearby/halfsheet/res/values-en-rIN/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting setup…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to \'%s\'"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try pairing to the device manually"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
diff --git a/nearby/halfsheet/res/values-en-rXC/strings.xml b/nearby/halfsheet/res/values-en-rXC/strings.xml
index 460cc1b..c71272e 100644
--- a/nearby/halfsheet/res/values-en-rXC/strings.xml
+++ b/nearby/halfsheet/res/values-en-rXC/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starting Setup…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Set up device"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Device connected"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connected to “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Couldn\'t connect"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Unable to connect"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Try manually pairing to the device"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Try putting the device into pairing mode"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Devices within reach"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Devices with your account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Your saved device is available"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Devices"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connecting…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Battery: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Done"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Save"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connect"</string>
diff --git a/nearby/halfsheet/res/values-es-rUS/strings.xml b/nearby/halfsheet/res/values-es-rUS/strings.xml
index d8fb283..05eb75d 100644
--- a/nearby/halfsheet/res/values-es-rUS/strings.xml
+++ b/nearby/halfsheet/res/values-es-rUS/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando la configuración…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configuración del dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Se conectó el dispositivo"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Se estableció conexión con \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"No se pudo establecer conexión"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"No se pudo establecer conexión"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Intenta vincular el dispositivo manualmente"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prueba poner el dispositivo en el modo de vinculación"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos al alcance"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos con tu cuenta"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"El dispositivo está disponible"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batería: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Listo"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
diff --git a/nearby/halfsheet/res/values-es/strings.xml b/nearby/halfsheet/res/values-es/strings.xml
index 4b8340a..7142a1a 100644
--- a/nearby/halfsheet/res/values-es/strings.xml
+++ b/nearby/halfsheet/res/values-es/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando configuración…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar el dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectado a \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"No se ha podido conectar"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"No se ha podido conectar"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prueba a emparejar el dispositivo manualmente"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prueba a poner el dispositivo en modo Emparejamiento"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos al alcance"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados con tu cuenta"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo guardado disponible"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batería: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Hecho"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
diff --git a/nearby/halfsheet/res/values-et/strings.xml b/nearby/halfsheet/res/values-et/strings.xml
index e6abc64..20a46a5 100644
--- a/nearby/halfsheet/res/values-et/strings.xml
+++ b/nearby/halfsheet/res/values-et/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Seadistuse käivitamine …"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Seadistage seade"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Seade on ühendatud"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ühendatud seadmega „%s“"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Ühendamine ebaõnnestus"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ühendust ei õnnestu luua"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Proovige seadmega käsitsi siduda"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Lülitage seade sidumisrežiimi"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Ühendusulatuses olevad seadmed"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Teie kontoga ühendatud seadmed"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Salvestatud seade on saadaval"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Läheduses"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Seadmed"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Ühendamine …"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Aku: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Valmis"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Salvesta"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Ühenda"</string>
diff --git a/nearby/halfsheet/res/values-eu/strings.xml b/nearby/halfsheet/res/values-eu/strings.xml
index 4243fd5..cd6eb34 100644
--- a/nearby/halfsheet/res/values-eu/strings.xml
+++ b/nearby/halfsheet/res/values-eu/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Konfigurazio-prozesua abiarazten…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfiguratu gailua"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Konektatu da gailua"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"%s gailura konektatuta"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Ezin izan da konektatu"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ezin da konektatu"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Saiatu gailua eskuz parekatzen"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Jarri gailua parekatzeko moduan"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Estaldura-eremuko gailuak"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Kontura konektatutako gailuak"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Gordetako gailua erabilgarri dago"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Gailuak"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Konektatzen…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %% %d"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Eginda"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Gorde"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Konektatu"</string>
diff --git a/nearby/halfsheet/res/values-fa/strings.xml b/nearby/halfsheet/res/values-fa/strings.xml
index 3585f95..7490e0f 100644
--- a/nearby/halfsheet/res/values-fa/strings.xml
+++ b/nearby/halfsheet/res/values-fa/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"درحال شروع راهاندازی…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"راهاندازی دستگاه"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"دستگاه متصل شد"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"به «%s» متصل شد"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"متصل نشد"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"اتصال برقرار نشد"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"مرتبطسازی با دستگاه را بهصورت دستی امتحان کنید"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"دستگاه را در حالت مرتبطسازی قرار دهید"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"دستگاههای دردسترس"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"دستگاههای متصل به حسابتان"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"دستگاه ذخیرهشدهتان دردسترس است"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"اطراف"</string>
+ <string name="common_devices" msgid="2635603125608104442">"دستگاهها"</string>
+ <string name="common_connecting" msgid="160531481424245303">"درحال اتصال…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"باتری: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"تمام"</string>
<string name="paring_action_save" msgid="6259357442067880136">"ذخیره"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"متصل کردن"</string>
diff --git a/nearby/halfsheet/res/values-fi/strings.xml b/nearby/halfsheet/res/values-fi/strings.xml
index e8d47de..b488b3e 100644
--- a/nearby/halfsheet/res/values-fi/strings.xml
+++ b/nearby/halfsheet/res/values-fi/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Aloitetaan käyttöönottoa…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Määritä laite"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Laite on yhdistetty"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"%s yhdistetty"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Ei yhteyttä"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Yhdistäminen epäonnistui"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Yritä yhdistää laitteet manuaalisesti"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Kokeile asettaa laite parinmuodostustilaan"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Kantoalueella olevat laitteet"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Tiliisi liitetyt laitteet"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Laitteesi on käytettävissä"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Lähellä"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Laitteet"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Yhdistetään…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Akku: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Valmis"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Tallenna"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Yhdistä"</string>
diff --git a/nearby/halfsheet/res/values-fr-rCA/strings.xml b/nearby/halfsheet/res/values-fr-rCA/strings.xml
index 64dd107..9a48890 100644
--- a/nearby/halfsheet/res/values-fr-rCA/strings.xml
+++ b/nearby/halfsheet/res/values-fr-rCA/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Démarrage de la configuration…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurer l\'appareil"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Appareil associé"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connecté à l\'appareil suivant : « %s »"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossible d\'associer"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Échec de la connexion"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Essayez d\'associer manuellement l\'appareil"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Essayez de mettre l\'appareil en mode d\'association"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Appareils à portée"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Appareils connectés à votre compte"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Appareil enregistré accessible"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"À proximité"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Appareils"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connexion en cours…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Pile : %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"OK"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Enregistrer"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Associer"</string>
diff --git a/nearby/halfsheet/res/values-fr/strings.xml b/nearby/halfsheet/res/values-fr/strings.xml
index 484c57b..f1263ab 100644
--- a/nearby/halfsheet/res/values-fr/strings.xml
+++ b/nearby/halfsheet/res/values-fr/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Début de la configuration…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurer un appareil"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Appareil associé"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connecté à \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossible de se connecter"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Impossible de se connecter"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Essayez d\'associer manuellement l\'appareil"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Essayez de mettre l\'appareil en mode association"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Appareils à portée de main"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Appareils connectés à votre compte"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Appareil enregistré disponible"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"À proximité"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Appareils"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connexion…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batterie : %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"OK"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Enregistrer"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connecter"</string>
diff --git a/nearby/halfsheet/res/values-gl/strings.xml b/nearby/halfsheet/res/values-gl/strings.xml
index 30393ff..91eac4f 100644
--- a/nearby/halfsheet/res/values-gl/strings.xml
+++ b/nearby/halfsheet/res/values-gl/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando configuración…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura o dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Conectouse o dispositivo"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"O dispositivo conectouse a %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Non se puido conectar"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Non se puido conectar"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Proba a vincular o dispositivo manualmente"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Proba a poñer o dispositivo no modo de vinculación"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos dentro do alcance"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados á túa conta"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo gardado dispoñible"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batería: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Feito"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Gardar"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
diff --git a/nearby/halfsheet/res/values-gu/strings.xml b/nearby/halfsheet/res/values-gu/strings.xml
index 03b057d..a7a7a2b 100644
--- a/nearby/halfsheet/res/values-gu/strings.xml
+++ b/nearby/halfsheet/res/values-gu/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"સેટઅપ શરૂ કરી રહ્યાં છીએ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ડિવાઇસનું સેટઅપ કરો"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ડિવાઇસ કનેક્ટ કર્યું"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” સાથે કનેક્ટ કરેલું"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"કનેક્ટ કરી શક્યા નથી"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"કનેક્ટ કરી શકાયું નથી"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ડિવાઇસથી મેન્યૂઅલી જોડાણ બનાવવાનો પ્રયાસ કરો"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ડિવાઇસને જોડાણ બનાવવાના મોડમાં રાખવાનો પ્રયાસ કરો"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"પહોંચની અંદરના ડિવાઇસ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"તમારા એકાઉન્ટ સાથેના ડિવાઇસ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"તમારું સાચવેલું ડિવાઇસ ઉપલબ્ધ છે"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"શેરિંગ"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ડિવાઇસ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"કનેક્ટ કરી રહ્યાં છીએ…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"બૅટરી: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"થઈ ગયું"</string>
<string name="paring_action_save" msgid="6259357442067880136">"સાચવો"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"કનેક્ટ કરો"</string>
diff --git a/nearby/halfsheet/res/values-hi/strings.xml b/nearby/halfsheet/res/values-hi/strings.xml
index ecd420e..dff9496 100644
--- a/nearby/halfsheet/res/values-hi/strings.xml
+++ b/nearby/halfsheet/res/values-hi/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेट अप शुरू किया जा रहा है…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"डिवाइस सेट अप करें"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"डिवाइस कनेक्ट हो गया"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” से कनेक्ट हो गया है"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट नहीं किया जा सका"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"कनेक्ट नहीं किया जा सका"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"डिवाइस को मैन्युअल तरीके से दूसरे डिवाइस से जोड़ने की कोशिश करें"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"डिवाइस को दूसरे डिवाइस से जोड़ने वाले मोड में रखें"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ऐसे डिवाइस जो रेंज में हैं"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"आपके खाते से जुड़े डिवाइस"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"सेव किया गया डिवाइस उपलब्ध है"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"आस-पास शेयरिंग"</string>
+ <string name="common_devices" msgid="2635603125608104442">"डिवाइस"</string>
+ <string name="common_connecting" msgid="160531481424245303">"कनेक्ट हो रहा है…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"बैटरी: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"हो गया"</string>
<string name="paring_action_save" msgid="6259357442067880136">"सेव करें"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट करें"</string>
diff --git a/nearby/halfsheet/res/values-hr/strings.xml b/nearby/halfsheet/res/values-hr/strings.xml
index 5a3de8f..13952b8 100644
--- a/nearby/halfsheet/res/values-hr/strings.xml
+++ b/nearby/halfsheet/res/values-hr/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pokretanje postavljanja…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Postavi uređaj"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Uređaj je povezan"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezano s uređajem %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezivanje nije uspjelo"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Povezivanje nije uspjelo"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Pokušajte ručno upariti s uređajem"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Pokušajte staviti uređaj u način uparivanja"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Uređaji u dometu"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Uređaji s vašim računom"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Spremljeni je uređaj dostupan"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"U blizini"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Uređaji"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Povezivanje…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Gotovo"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Spremi"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
diff --git a/nearby/halfsheet/res/values-hu/strings.xml b/nearby/halfsheet/res/values-hu/strings.xml
index ba3d2e0..3d810d4 100644
--- a/nearby/halfsheet/res/values-hu/strings.xml
+++ b/nearby/halfsheet/res/values-hu/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Beállítás megkezdése…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Eszköz beállítása"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Eszköz csatlakoztatva"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Csatlakoztatva a következőhöz: %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nem sikerült csatlakozni"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nem sikerült csatlakozni"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Próbálkozzon az eszköz kézi párosításával"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Próbálja meg bekapcsolni az eszközön a párosítási módot"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Elérhető eszközök"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"A fiókjával társított eszközök"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"A mentett eszköze elérhető"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Közeli"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Eszközök"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Csatlakozás…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Akkumulátor: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Kész"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Mentés"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Csatlakozás"</string>
diff --git a/nearby/halfsheet/res/values-hy/strings.xml b/nearby/halfsheet/res/values-hy/strings.xml
index ecabd16..57b9256 100644
--- a/nearby/halfsheet/res/values-hy/strings.xml
+++ b/nearby/halfsheet/res/values-hy/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Կարգավորում…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Կարգավորեք սարքը"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Սարքը զուգակցվեց"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"«%s» սարքի հետ կապը հաստատվեց"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Չհաջողվեց միանալ"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Չհաջողվեց կապ հաստատել"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Փորձեք ձեռքով զուգակցել սարքը"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Փորձեք սարքում միացնել զուգակցման ռեժիմը"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Սարքեր հասանելիության տիրույթում"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Ձեր հաշվի հետ կապված սարքեր"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Պահված սարքը հասանելի է"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Մոտակայքում"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Սարքեր"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Միացում…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Մարտկոցի լիցքը՝ %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Պատրաստ է"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Պահել"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Միանալ"</string>
diff --git a/nearby/halfsheet/res/values-in/strings.xml b/nearby/halfsheet/res/values-in/strings.xml
index dc777b2..c665572 100644
--- a/nearby/halfsheet/res/values-in/strings.xml
+++ b/nearby/halfsheet/res/values-in/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Memulai Penyiapan …"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Siapkan perangkat"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Perangkat terhubung"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Terhubung ke “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Tidak dapat terhubung"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Tidak dapat terhubung"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Coba sambungkan ke perangkat secara manual"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Coba masukkan perangkat ke dalam mode penyambungan"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Perangkat dalam jangkauan"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Perangkat dengan akun Anda"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Perangkat tersimpan tersedia"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Berbagi Langsung"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Perangkat"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Menghubungkan …"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Daya baterai: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Selesai"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Simpan"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Hubungkan"</string>
diff --git a/nearby/halfsheet/res/values-is/strings.xml b/nearby/halfsheet/res/values-is/strings.xml
index ee094d9..04a4de4 100644
--- a/nearby/halfsheet/res/values-is/strings.xml
+++ b/nearby/halfsheet/res/values-is/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Ræsir uppsetningu…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Uppsetning tækis"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Tækið er tengt"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Tengt við „%s“"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Tenging mistókst"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Tenging tókst ekki"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prófaðu að para tækið handvirkt"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prófaðu að kveikja á pörunarstillingu í tækinu"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Nálæg tæki"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Tæki tengd reikningnum þínum"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Vistaða tækið er tiltækt"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nálægt"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Tæki"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Tengist…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Rafhlaða: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Lokið"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Vista"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Tengja"</string>
diff --git a/nearby/halfsheet/res/values-it/strings.xml b/nearby/halfsheet/res/values-it/strings.xml
index 700dd77..2ffe268 100644
--- a/nearby/halfsheet/res/values-it/strings.xml
+++ b/nearby/halfsheet/res/values-it/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Avvio della configurazione…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configura dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo connesso"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Connesso a \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Impossibile connettere"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Impossibile connettersi"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prova a eseguire l\'accoppiamento manuale con il dispositivo"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prova a impostare il dispositivo sulla modalità di accoppiamento"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivi nelle vicinanze"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivi collegati all\'account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Disposit. salvato disponibile"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nelle vicinanze"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivi"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Connessione in corso"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batteria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Fine"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Salva"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Connetti"</string>
diff --git a/nearby/halfsheet/res/values-iw/strings.xml b/nearby/halfsheet/res/values-iw/strings.xml
index e6ff9b9..61724f0 100644
--- a/nearby/halfsheet/res/values-iw/strings.xml
+++ b/nearby/halfsheet/res/values-iw/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ההגדרה מתבצעת…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"הגדרת המכשיר"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"המכשיר מחובר"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"יש חיבור אל \'%s\'"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"לא ניתן להתחבר"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"לא ניתן להתחבר"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"כדאי לנסות לבצע התאמה ידנית למכשיר"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"כדאי לנסות להעביר את המכשיר למצב התאמה"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"מכשירים בהישג יד"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"מכשירים המחוברים לחשבון שלך"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"המכשיר ששמרת זמין"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"בקרבת מקום"</string>
+ <string name="common_devices" msgid="2635603125608104442">"מכשירים"</string>
+ <string name="common_connecting" msgid="160531481424245303">"מתבצעת התחברות…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"טעינת הסוללה: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"סיום"</string>
<string name="paring_action_save" msgid="6259357442067880136">"שמירה"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"התחברות"</string>
diff --git a/nearby/halfsheet/res/values-ja/strings.xml b/nearby/halfsheet/res/values-ja/strings.xml
index a429b7e..3168bda 100644
--- a/nearby/halfsheet/res/values-ja/strings.xml
+++ b/nearby/halfsheet/res/values-ja/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"セットアップを開始中…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"デバイスのセットアップ"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"デバイス接続完了"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"「%s」に接続しました"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"接続エラー"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"接続できません"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"手動でデバイスとペア設定してみてください"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"デバイスをペア設定モードにしてください"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"近接するデバイス"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"アカウントと接続済みのデバイス"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"保存済みのデバイスがあります"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"周辺ユーザーとの共有"</string>
+ <string name="common_devices" msgid="2635603125608104442">"デバイス"</string>
+ <string name="common_connecting" msgid="160531481424245303">"接続しています…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"バッテリー: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"完了"</string>
<string name="paring_action_save" msgid="6259357442067880136">"保存"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"接続"</string>
diff --git a/nearby/halfsheet/res/values-ka/strings.xml b/nearby/halfsheet/res/values-ka/strings.xml
index 4353ae9..a9ee648 100644
--- a/nearby/halfsheet/res/values-ka/strings.xml
+++ b/nearby/halfsheet/res/values-ka/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"დაყენება იწყება…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"მოწყობილობის დაყენება"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"მოწყობილობა დაკავშირებულია"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"დაკავშირებულია „%s“-თან"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"დაკავშირება ვერ მოხერხდა"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"დაკავშირება ვერ ხერხდება"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ცადეთ მოწყობილობასთან ხელით დაწყვილება"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ცადეთ მოწყობილობის გადაყვანა დაწყვილების რეჟიმზე"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ხელმისაწვდომი მოწყობილობები"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"მოწყობილობები თქვენი ანგარიშით"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"შენახული მოწყობილობა ხელმისაწვდომია"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"ახლომახლო"</string>
+ <string name="common_devices" msgid="2635603125608104442">"მოწყობილობები"</string>
+ <string name="common_connecting" msgid="160531481424245303">"მიმდინარეობს დაკავშირება…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ბატარეა: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"მზადაა"</string>
<string name="paring_action_save" msgid="6259357442067880136">"შენახვა"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"დაკავშირება"</string>
diff --git a/nearby/halfsheet/res/values-kk/strings.xml b/nearby/halfsheet/res/values-kk/strings.xml
index 98d8073..6e1a0bd 100644
--- a/nearby/halfsheet/res/values-kk/strings.xml
+++ b/nearby/halfsheet/res/values-kk/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Реттеу басталуда…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Құрылғыны реттеу"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Құрылғы байланыстырылды"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" құрылғысымен байланыстырылды"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Қосылмады"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Қосылу мүмкін емес"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Құрылғыны қолмен жұптап көріңіз."</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Құрылғыны жұптау режиміне қойып көріңіз."</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Қолжетімді құрылғылар"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Аккаунтпен байланыстырылған құрылғылар"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Сақталған құрылғы қолжетімді"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Маңайдағы"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Құрылғылар"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Жалғанып жатыр…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батарея: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Дайын"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Сақтау"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Қосу"</string>
diff --git a/nearby/halfsheet/res/values-km/strings.xml b/nearby/halfsheet/res/values-km/strings.xml
index 85e39db..deb6504 100644
--- a/nearby/halfsheet/res/values-km/strings.xml
+++ b/nearby/halfsheet/res/values-km/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"កំពុងចាប់ផ្ដើមរៀបចំ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"រៀបចំឧបករណ៍"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"បានភ្ជាប់ឧបករណ៍"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"បានភ្ជាប់ជាមួយ “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"មិនអាចភ្ជាប់បានទេ"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"មិនអាចភ្ជាប់បានទេ"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"សាកល្បងផ្គូផ្គងដោយផ្ទាល់ទៅឧបករណ៍"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"សាកល្បងកំណត់ឧបករណ៍ឱ្យប្រើមុខងារផ្គូផ្គង"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ឧបករណ៍នៅក្បែរដៃ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ឧបករណ៍ភ្ជាប់ជាមួយគណនីរបស់អ្នក"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"ឧបករណ៍ដែលអ្នកបានរក្សាទុកអាចប្រើបានហើយ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"នៅជិត"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ឧបករណ៍"</string>
+ <string name="common_connecting" msgid="160531481424245303">"កំពុងភ្ជាប់…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ថ្ម៖ %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"រួចរាល់"</string>
<string name="paring_action_save" msgid="6259357442067880136">"រក្សាទុក"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ភ្ជាប់"</string>
diff --git a/nearby/halfsheet/res/values-kn/strings.xml b/nearby/halfsheet/res/values-kn/strings.xml
index fb62bb1..87b4fe3 100644
--- a/nearby/halfsheet/res/values-kn/strings.xml
+++ b/nearby/halfsheet/res/values-kn/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ಸೆಟಪ್ ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ಸಾಧನವನ್ನು ಸೆಟಪ್ ಮಾಡಿ"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ಸಾಧನವನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲಾಗಿದೆ"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” ಗೆ ಕನೆಕ್ಟ್ ಮಾಡಲಾಗಿದೆ"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ಸಾಧನಕ್ಕೆ ಹಸ್ತಚಾಲಿತವಾಗಿ ಜೋಡಿಸಲು ಪ್ರಯತ್ನಿಸಿ"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ಜೋಡಿಸುವಿಕೆ ಮೋಡ್ನಲ್ಲಿ ಸಾಧನವನ್ನು ಇರಿಸಲು ಪ್ರಯತ್ನಿಸಿ"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ವ್ಯಾಪ್ತಿಯಲ್ಲಿರುವ ಸಾಧನಗಳು"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ನಿಮ್ಮ ಖಾತೆಗೆ ಸಂಪರ್ಕಿತವಾಗಿರುವ ಸಾಧನಗಳು"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"ಉಳಿಸಲಾದ ನಿಮ್ಮ ಸಾಧನವು ಲಭ್ಯವಿದೆ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"ಸಮೀಪದಲ್ಲಿರುವುದು"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ಸಾಧನಗಳು"</string>
+ <string name="common_connecting" msgid="160531481424245303">"ಕನೆಕ್ಟ್ ಆಗುತ್ತಿದೆ…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ಬ್ಯಾಟರಿ: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ಮುಗಿದಿದೆ"</string>
<string name="paring_action_save" msgid="6259357442067880136">"ಉಳಿಸಿ"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ಕನೆಕ್ಟ್ ಮಾಡಿ"</string>
diff --git a/nearby/halfsheet/res/values-ko/strings.xml b/nearby/halfsheet/res/values-ko/strings.xml
index c94ff76..37d50da 100644
--- a/nearby/halfsheet/res/values-ko/strings.xml
+++ b/nearby/halfsheet/res/values-ko/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"설정을 시작하는 중…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"기기 설정"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"기기 연결됨"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\'%s\'에 연결됨"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"연결할 수 없음"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"연결할 수 없음"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"기기에 수동으로 페어링을 시도해 보세요."</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"기기를 페어링 모드로 전환하세요."</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"연결 가능 기기"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"내 계정에 연결된 기기"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"저장된 기기 사용 가능"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nearby"</string>
+ <string name="common_devices" msgid="2635603125608104442">"기기"</string>
+ <string name="common_connecting" msgid="160531481424245303">"연결 중…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"배터리: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"완료"</string>
<string name="paring_action_save" msgid="6259357442067880136">"저장"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"연결"</string>
diff --git a/nearby/halfsheet/res/values-ky/strings.xml b/nearby/halfsheet/res/values-ky/strings.xml
index b0dfe20..b6aa409 100644
--- a/nearby/halfsheet/res/values-ky/strings.xml
+++ b/nearby/halfsheet/res/values-ky/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Жөндөлүп баштады…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Түзмөктү жөндөө"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Түзмөк туташты"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” түзмөгүнө туташты"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Туташпай койду"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Туташпай жатат"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Түзмөккө кол менен жупташтырып көрүңүз"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Түзмөктү Байланыштыруу режимине коюп көрүңүз"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Жеткиликтүү түзмөктөр"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Аккаунтуңузга кирип турган түзмөктөр"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Сакталган түзмөгүңүз жеткиликтүү"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Жакын жерде"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Түзмөктөр"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Туташууда…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батарея: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Бүттү"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Сактоо"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Туташуу"</string>
diff --git a/nearby/halfsheet/res/values-lo/strings.xml b/nearby/halfsheet/res/values-lo/strings.xml
index 9c945b2..cbc7601 100644
--- a/nearby/halfsheet/res/values-lo/strings.xml
+++ b/nearby/halfsheet/res/values-lo/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ກຳລັງເລີ່ມການຕັ້ງຄ່າ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ຕັ້ງຄ່າອຸປະກອນ"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ເຊື່ອມຕໍ່ອຸປະກອນແລ້ວ"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"ເຊື່ອມຕໍ່ກັບ “%s” ແລ້ວ"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ລອງຈັບຄູ່ອຸປະກອນດ້ວຍຕົນເອງ"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ໃຫ້ລອງຕັ້ງອຸປະກອນເປັນໂໝດຈັບຄູ່"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ອຸປະກອນພ້ອມໃຫ້ເຂົ້າເຖິງ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ອຸປະກອນທີ່ມີບັນຊີຂອງທ່ານ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"ອຸປະກອນທີ່ບັນທຶກໄວ້ຂອງທ່ານສາມາດໃຊ້ໄດ້"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"ໃກ້ຄຽງ"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ອຸປະກອນ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"ກຳລັງເຊື່ອມຕໍ່…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ແບັດເຕີຣີ: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ແລ້ວໆ"</string>
<string name="paring_action_save" msgid="6259357442067880136">"ບັນທຶກ"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ເຊື່ອມຕໍ່"</string>
diff --git a/nearby/halfsheet/res/values-lt/strings.xml b/nearby/halfsheet/res/values-lt/strings.xml
index 5dbad0a..29a9bc5 100644
--- a/nearby/halfsheet/res/values-lt/strings.xml
+++ b/nearby/halfsheet/res/values-lt/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Pradedama sąranka…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Įrenginio nustatymas"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Įrenginys prijungtas"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Prisijungta prie „%s“"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Prisijungti nepavyko"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nepavyko prisijungti"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Pabandykite neautomatiškai susieti įrenginį"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Pabandykite vėl įgalinti įrenginio susiejimo režimą"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Lengvai pasiekiami įrenginiai"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Įrenginiai su jūsų paskyra"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Išsaugotas įrenginys pasiekiamas"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Netoliese"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Įrenginiai"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Prisijungiama…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Akumuliatorius: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Atlikta"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Išsaugoti"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Prisijungti"</string>
diff --git a/nearby/halfsheet/res/values-lv/strings.xml b/nearby/halfsheet/res/values-lv/strings.xml
index a9e1bf9..9573357 100644
--- a/nearby/halfsheet/res/values-lv/strings.xml
+++ b/nearby/halfsheet/res/values-lv/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Tiek sākta iestatīšana…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Iestatiet ierīci"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Ierīce ir pievienota"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Izveidots savienojums ar ierīci “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nevarēja izveidot savienojumu"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nevar izveidot savienojumu."</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Mēģiniet manuāli izveidot savienojumu pārī ar ierīci."</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Ieslēdziet ierīcē režīmu savienošanai pārī"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Sasniedzamas ierīces"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Ierīces ar jūsu kontu"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Jūsu saglabātā ierīce pieejama"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Tuvumā"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Ierīces"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Savienojuma izveide…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Akumulatora uzlādes līmenis: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Gatavs"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Saglabāt"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Izveidot savienojumu"</string>
diff --git a/nearby/halfsheet/res/values-mk/strings.xml b/nearby/halfsheet/res/values-mk/strings.xml
index e29dfa1..693f112 100644
--- a/nearby/halfsheet/res/values-mk/strings.xml
+++ b/nearby/halfsheet/res/values-mk/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Се започнува со поставување…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Поставете го уредот"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Уредот е поврзан"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Поврзан со „%s“"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Не може да се поврзе"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не може да се поврзе"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Обидете се рачно да се спарите со уредот"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Пробајте да го ставите уредот во режим на спарување"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Уреди на дофат"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Уреди поврзани со вашата сметка"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Вашиот зачуван уред е достапен"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Во близина"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Уреди"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Се поврзува…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батерија: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Зачувај"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Поврзи"</string>
diff --git a/nearby/halfsheet/res/values-ml/strings.xml b/nearby/halfsheet/res/values-ml/strings.xml
index cbc171b..56a2db2 100644
--- a/nearby/halfsheet/res/values-ml/strings.xml
+++ b/nearby/halfsheet/res/values-ml/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"സജ്ജീകരിക്കൽ ആരംഭിക്കുന്നു…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ഉപകരണം സജ്ജീകരിക്കുക"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ഉപകരണം കണക്റ്റ് ചെയ്തു"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” എന്നതിലേക്ക് കണക്റ്റ് ചെയ്തു"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"കണക്റ്റ് ചെയ്യാനായില്ല"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"കണക്റ്റ് ചെയ്യാനാകുന്നില്ല"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ഉപകരണം ജോടിയാക്കാൻ നേരിട്ട് ശ്രമിക്കുക"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ഉപകരണം ജോഡിയാക്കൽ മോഡിലേക്ക് മാറ്റിയ ശേഷം ശ്രമിക്കുക"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"പരിധിയിലുള്ള ഉപകരണങ്ങൾ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"അക്കൗണ്ട് ഉപയോഗിക്കുന്ന ഉപകരണങ്ങൾ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"സംരക്ഷിച്ച ഉപകരണം ലഭ്യമാണ്"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"സമീപമുള്ളവ"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ഉപകരണങ്ങൾ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"കണക്റ്റ് ചെയ്യുന്നു…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ബാറ്ററി: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"പൂർത്തിയായി"</string>
<string name="paring_action_save" msgid="6259357442067880136">"സംരക്ഷിക്കുക"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"കണക്റ്റ് ചെയ്യുക"</string>
diff --git a/nearby/halfsheet/res/values-mn/strings.xml b/nearby/halfsheet/res/values-mn/strings.xml
index 6d21eff..5a72ce3 100644
--- a/nearby/halfsheet/res/values-mn/strings.xml
+++ b/nearby/halfsheet/res/values-mn/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Тохируулгыг эхлүүлж байна…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Төхөөрөмж тохируулах"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Төхөөрөмж холбогдсон"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”-д холбогдсон"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Холбогдож чадсангүй"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Холбогдох боломжгүй байна"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Төхөөрөмжийг гар аргаар хослуулна уу"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Төхөөрөмжийг хослуулах горимд оруулахаар оролдоно уу"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Хүрээн дэх төхөөрөмжүүд"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Таны бүртгэлтэй холбогдсон төхөөрөмж"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Таны хадгалсан төхөөрөмж бэлэн байна"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Ойролцоо"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Төхөөрөмж"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Холбогдож байна…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батарей: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Болсон"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Хадгалах"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Холбох"</string>
diff --git a/nearby/halfsheet/res/values-mr/strings.xml b/nearby/halfsheet/res/values-mr/strings.xml
index a3e1d7a..3eeb0ec 100644
--- a/nearby/halfsheet/res/values-mr/strings.xml
+++ b/nearby/halfsheet/res/values-mr/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेटअप सुरू करत आहे…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"डिव्हाइस सेट करा"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"डिव्हाइस कनेक्ट केले आहे"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" शी कनेक्ट केले आहे"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट करता आले नाही"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"कनेक्ट करता आले नाही"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"डिव्हाइसशी मॅन्युअली पेअर करण्याचा प्रयत्न करा"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"डिव्हाइसला पेअरिंग मोडमध्ये ठेवण्याचा प्रयत्न करा"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"पुरेसे जवळ असलेले डिव्हाइस"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"तुमच्या खात्यासह असलेली डिव्हाइस"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"सेव्ह केलेले डिव्हाइस उपलब्ध"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"जवळपास"</string>
+ <string name="common_devices" msgid="2635603125608104442">"डिव्हाइस"</string>
+ <string name="common_connecting" msgid="160531481424245303">"कनेक्ट करत आहे…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"बॅटरी: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"पूर्ण झाले"</string>
<string name="paring_action_save" msgid="6259357442067880136">"सेव्ह करा"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट करा"</string>
diff --git a/nearby/halfsheet/res/values-ms/strings.xml b/nearby/halfsheet/res/values-ms/strings.xml
index 4835c1b..0af903d 100644
--- a/nearby/halfsheet/res/values-ms/strings.xml
+++ b/nearby/halfsheet/res/values-ms/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Memulakan Persediaan…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Sediakan peranti"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Peranti disambungkan"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Disambungkan kepada “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Tidak dapat menyambung"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Tidak dapat menyambung"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Cuba gandingkan dengan peranti secara manual"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Cuba letakkan peranti dalam mod penggandingan"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Peranti dalam capaian"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Peranti dengan akaun anda"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Peranti disimpan anda tersedia"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Berdekatan"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Peranti"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Menyambung…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateri: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Selesai"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Simpan"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Sambung"</string>
diff --git a/nearby/halfsheet/res/values-my/strings.xml b/nearby/halfsheet/res/values-my/strings.xml
index 32c3105..306538f 100644
--- a/nearby/halfsheet/res/values-my/strings.xml
+++ b/nearby/halfsheet/res/values-my/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"စနစ်ထည့်သွင်းခြင်း စတင်နေသည်…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"စက်ကို စနစ်ထည့်သွင်းရန်"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"စက်ကို ချိတ်ဆက်လိုက်ပြီ"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” သို့ ချိတ်ဆက်ထားသည်"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"ချိတ်ဆက်၍မရပါ"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ချိတ်ဆက်၍ မရပါ"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"စက်ကို ကိုယ်တိုင်တွဲချိတ်ကြည့်ပါ"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"စက်ကို တွဲချိတ်ခြင်းမုဒ်သို့ ထားကြည့်ပါ"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"လက်လှမ်းမီသည့် စက်များ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"သင့်အကောင့်ရှိသည့် စက်များ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"သင်သိမ်းထားသောစက် ရပါပြီ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"အနီးတစ်ဝိုက်"</string>
+ <string name="common_devices" msgid="2635603125608104442">"စက်များ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"ချိတ်ဆက်နေသည်…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ဘက်ထရီ− %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ပြီးပြီ"</string>
<string name="paring_action_save" msgid="6259357442067880136">"သိမ်းရန်"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ချိတ်ဆက်ရန်"</string>
diff --git a/nearby/halfsheet/res/values-nb/strings.xml b/nearby/halfsheet/res/values-nb/strings.xml
index 9d72565..72a2ab7 100644
--- a/nearby/halfsheet/res/values-nb/strings.xml
+++ b/nearby/halfsheet/res/values-nb/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Starter konfigureringen …"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurer enheten"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheten er tilkoblet"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Koblet til «%s»"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Kunne ikke koble til"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kan ikke koble til"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Prøv manuell tilkobling til enheten"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prøv å sette enheten i tilkoblingsmodus"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Enheter innen rekkevidde"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Enheter med kontoen din"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Lagret enhet tilgjengelig"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"I nærheten"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Enheter"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Kobler til …"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batteri: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Ferdig"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Lagre"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Koble til"</string>
diff --git a/nearby/halfsheet/res/values-ne/strings.xml b/nearby/halfsheet/res/values-ne/strings.xml
index 1370412..2956768 100644
--- a/nearby/halfsheet/res/values-ne/strings.xml
+++ b/nearby/halfsheet/res/values-ne/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"सेटअप प्रक्रिया सुरु गरिँदै छ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"डिभाइस सेटअप गर्नुहोस्"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"डिभाइस कनेक्ट गरियो"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" मा कनेक्ट गरिएको छ"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"कनेक्ट गर्न सकिएन"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"कनेक्ट गर्न सकिएन"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"म्यानुअल तरिकाले डिभाइस कनेक्ट गरी हेर्नुहोस्"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"डिभाइस कनेक्ट गर्ने मोडमा राखी हेर्नुहोस्"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"कनेक्ट गर्न सकिने नजिकैका डिभाइसहरू"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"तपाईंको खातामा कनेक्ट गरिएका डिभाइस"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"तपाईंले सेभ गरेको डिभाइस उपलब्ध छ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"नजिकै"</string>
+ <string name="common_devices" msgid="2635603125608104442">"डिभाइसहरू"</string>
+ <string name="common_connecting" msgid="160531481424245303">"कनेक्ट गरिँदै छ…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ब्याट्री: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"सम्पन्न भयो"</string>
<string name="paring_action_save" msgid="6259357442067880136">"सेभ गर्नुहोस्"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"कनेक्ट गर्नुहोस्"</string>
diff --git a/nearby/halfsheet/res/values-night/colors.xml b/nearby/halfsheet/res/values-night/colors.xml
new file mode 100644
index 0000000..69b832a
--- /dev/null
+++ b/nearby/halfsheet/res/values-night/colors.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+ <!-- Use original background color -->
+ <color name="fast_pair_notification_background">#00000000</color>
+ <!-- Ignores NewApi as below system colors are available since API 31, and HalfSheet is always
+ running on T+ even though it has min_sdk 30 to match its containing APEX -->
+ <color name="fast_pair_half_sheet_button_color" tools:ignore="NewApi">
+ @android:color/system_accent1_100</color>
+ <color name="fast_pair_half_sheet_button_text" tools:ignore="NewApi">
+ @android:color/system_neutral1_50</color>
+ <color name="fast_pair_half_sheet_button_accent_text" tools:ignore="NewApi">
+ @android:color/system_neutral1_900</color>
+ <color name="fast_pair_progress_color" tools:ignore="NewApi">
+ @android:color/system_accent1_600</color>
+ <color name="fast_pair_half_sheet_subtitle_color" tools:ignore="NewApi">
+ @android:color/system_neutral2_200</color>
+ <color name="fast_pair_half_sheet_text_color" tools:ignore="NewApi">
+ @android:color/system_neutral1_50</color>
+ <color name="fast_pair_half_sheet_background" tools:ignore="NewApi">
+ @android:color/system_neutral1_800</color>
+
+ <!-- Fast Pair -->
+ <color name="fast_pair_primary_text">#FFFFFF</color>
+ <color name="fast_pair_notification_image_outline">#24FFFFFF</color>
+ <color name="fast_pair_battery_level_low">#F6AEA9</color>
+
+</resources>
+
diff --git a/nearby/halfsheet/res/values-nl/strings.xml b/nearby/halfsheet/res/values-nl/strings.xml
index 4eb7624..e956116 100644
--- a/nearby/halfsheet/res/values-nl/strings.xml
+++ b/nearby/halfsheet/res/values-nl/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Instellen starten…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Apparaat instellen"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Apparaat verbonden"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Verbonden met %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Kan geen verbinding maken"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Kan geen verbinding maken"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Probeer handmatig met het apparaat te koppelen"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Activeer de koppelingsstand van het apparaat"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Apparaten binnen bereik"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Apparaten met je account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Opgeslagen apparaat beschikbaar"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"In de buurt"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Apparaten"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Verbinden…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batterij: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Klaar"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Opslaan"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Verbinden"</string>
diff --git a/nearby/halfsheet/res/values-or/strings.xml b/nearby/halfsheet/res/values-or/strings.xml
index c5e8cfc..0ec472c 100644
--- a/nearby/halfsheet/res/values-or/strings.xml
+++ b/nearby/halfsheet/res/values-or/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ସେଟଅପ ଆରମ୍ଭ କରାଯାଉଛି…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ଡିଭାଇସ ସେଟ ଅପ କରନ୍ତୁ"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ଡିଭାଇସ ସଂଯୁକ୍ତ ହୋଇଛି"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” ସହ କନେକ୍ଟ କରାଯାଇଛି"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"ସଂଯୋଗ କରାଯାଇପାରିଲା ନାହିଁ"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"କନେକ୍ଟ କରିବାରେ ଅସମର୍ଥ"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ଡିଭାଇସ ସହ ମାନୁଆଲୀ ପେୟାର କରିବା ପାଇଁ ଚେଷ୍ଟା କରନ୍ତୁ"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ଡିଭାଇସକୁ ପେୟାରିଂ ମୋଡରେ ରଖିବାକୁ ଚେଷ୍ଟା କରନ୍ତୁ"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ପହଞ୍ଚ ଭିତରେ ଥିବା ଡିଭାଇସଗୁଡ଼ିକ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ଆପଣଙ୍କ ଆକାଉଣ୍ଟ ସହ ଥିବା ଡିଭାଇସଗୁଡ଼ିକ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"ସେଭ ଥିବା ଆପଣଙ୍କ ଡିଭାଇସ ଉପଲବ୍ଧ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"ଆଖପାଖର"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ଡିଭାଇସଗୁଡ଼ିକ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"କନେକ୍ଟ କରାଯାଉଛି…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ବେଟେରୀ: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ହୋଇଗଲା"</string>
<string name="paring_action_save" msgid="6259357442067880136">"ସେଭ କରନ୍ତୁ"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ସଂଯୋଗ କରନ୍ତୁ"</string>
diff --git a/nearby/halfsheet/res/values-pa/strings.xml b/nearby/halfsheet/res/values-pa/strings.xml
index f0523a3..4eb0553 100644
--- a/nearby/halfsheet/res/values-pa/strings.xml
+++ b/nearby/halfsheet/res/values-pa/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"ਸੈੱਟਅੱਪ ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ਡੀਵਾਈਸ ਸੈੱਟਅੱਪ ਕਰੋ"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"ਡੀਵਾਈਸ ਕਨੈਕਟ ਕੀਤਾ ਗਿਆ"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” ਨਾਲ ਕਨੈਕਟ ਹੈ"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"ਕਨੈਕਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ਡੀਵਾਈਸ ਨਾਲ ਹੱਥੀਂ ਜੋੜਾਬੱਧ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"ਡੀਵਾਈਸ ਨੂੰ ਜੋੜਾਬੱਧਕਰਨ ਮੋਡ ਵਿੱਚ ਰੱਖਣ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ਡੀਵਾਈਸ ਜੋ ਪਹੁੰਚ ਅੰਦਰ ਹਨ"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ਤੁਹਾਡੇ ਖਾਤੇ ਵਾਲੇ ਡੀਵਾਈਸ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"ਰੱਖਿਅਤ ਕੀਤਾ ਡੀਵਾਈਸ ਉਪਲਬਧ ਹੈ"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"ਨਜ਼ਦੀਕੀ"</string>
+ <string name="common_devices" msgid="2635603125608104442">"ਡੀਵਾਈਸ"</string>
+ <string name="common_connecting" msgid="160531481424245303">"ਕਨੈਕਟ ਹੋ ਰਿਹਾ ਹੈ…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"ਬੈਟਰੀ: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ਹੋ ਗਿਆ"</string>
<string name="paring_action_save" msgid="6259357442067880136">"ਰੱਖਿਅਤ ਕਰੋ"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"ਕਨੈਕਟ ਕਰੋ"</string>
diff --git a/nearby/halfsheet/res/values-pl/strings.xml b/nearby/halfsheet/res/values-pl/strings.xml
index 5abf5fd..5082e18 100644
--- a/nearby/halfsheet/res/values-pl/strings.xml
+++ b/nearby/halfsheet/res/values-pl/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Rozpoczynam konfigurowanie…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Skonfiguruj urządzenie"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Urządzenie połączone"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Połączono z: „%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nie udało się połączyć"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nie udało się połączyć"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Spróbuj ręcznie sparować urządzenie"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Włącz na urządzeniu tryb parowania"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Urządzenia w zasięgu"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Urządzenia z Twoim kontem"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Zapisane urządzenie dostępne"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"W pobliżu"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Urządzenia"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Łączę…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Gotowe"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Zapisz"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Połącz"</string>
diff --git a/nearby/halfsheet/res/values-pt-rBR/strings.xml b/nearby/halfsheet/res/values-pt-rBR/strings.xml
index b021b39..15d29d2 100644
--- a/nearby/halfsheet/res/values-pt-rBR/strings.xml
+++ b/nearby/halfsheet/res/values-pt-rBR/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando a configuração…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectado ao dispositivo \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Erro ao conectar"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Não foi possível conectar"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Tente parear o dispositivo manualmente"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Coloque o dispositivo no modo de pareamento"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos ao alcance"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados à sua conta"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo salvo disponível"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Por perto"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Concluído"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Salvar"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
diff --git a/nearby/halfsheet/res/values-pt-rPT/strings.xml b/nearby/halfsheet/res/values-pt-rPT/strings.xml
index 3285c73..ab8decf 100644
--- a/nearby/halfsheet/res/values-pt-rPT/strings.xml
+++ b/nearby/halfsheet/res/values-pt-rPT/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"A iniciar a configuração…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configure o dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo ligado"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ligado a \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Não foi possível ligar"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Impossível ligar"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Experimente sincronizar manualmente com o dispositivo"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Experimente ativar o modo de sincronização no dispositivo"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos ao alcance"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos com a sua conta"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Disposit. guardado disponível"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Partilha"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
+ <string name="common_connecting" msgid="160531481424245303">"A ligar…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Concluir"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Guardar"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Ligar"</string>
diff --git a/nearby/halfsheet/res/values-pt/strings.xml b/nearby/halfsheet/res/values-pt/strings.xml
index b021b39..15d29d2 100644
--- a/nearby/halfsheet/res/values-pt/strings.xml
+++ b/nearby/halfsheet/res/values-pt/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iniciando a configuração…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurar dispositivo"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispositivo conectado"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectado ao dispositivo \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Erro ao conectar"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Não foi possível conectar"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Tente parear o dispositivo manualmente"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Coloque o dispositivo no modo de pareamento"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispositivos ao alcance"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispositivos conectados à sua conta"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispositivo salvo disponível"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Por perto"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispositivos"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Conectando…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Concluído"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Salvar"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Conectar"</string>
diff --git a/nearby/halfsheet/res/values-ro/strings.xml b/nearby/halfsheet/res/values-ro/strings.xml
index 189f698..0335d01 100644
--- a/nearby/halfsheet/res/values-ro/strings.xml
+++ b/nearby/halfsheet/res/values-ro/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Începe configurarea…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Configurează dispozitivul"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Dispozitivul s-a conectat"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Conectat la %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nu s-a putut conecta"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nu se poate conecta"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Încearcă asocierea manuală cu dispozitivul"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Încearcă să pui dispozitivul în modul de asociere"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Dispozitive în vecinătate"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Dispozitive conectate cu contul"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Dispozitivul este disponibil"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"În apropiere"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Dispozitive"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Se conectează…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterie: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Gata"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Salvează"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Conectează"</string>
diff --git a/nearby/halfsheet/res/values-ru/strings.xml b/nearby/halfsheet/res/values-ru/strings.xml
index ee869df..d90b644 100644
--- a/nearby/halfsheet/res/values-ru/strings.xml
+++ b/nearby/halfsheet/res/values-ru/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Начинаем настройку…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Настройка устройства"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Устройство подключено"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Подключено к устройству \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Ошибка подключения"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не удалось подключиться"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Попробуйте подключиться к устройству вручную."</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Переведите устройство в режим подключения."</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Устройства в зоне охвата"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Устройства с вашим аккаунтом"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Доступно сохранен. устройство"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Мое окружение"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Устройства"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Подключение…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батарея: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Сохранить"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Подключить"</string>
diff --git a/nearby/halfsheet/res/values-si/strings.xml b/nearby/halfsheet/res/values-si/strings.xml
index f4274c2..c9b96bb 100644
--- a/nearby/halfsheet/res/values-si/strings.xml
+++ b/nearby/halfsheet/res/values-si/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"පිහිටුවීම ආරම්භ කරමින්…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"උපාංගය පිහිටුවන්න"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"උපාංගය සම්බන්ධිතයි"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” වෙත සම්බන්ධ විය"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"සම්බන්ධ කළ නොහැකි විය"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"සම්බන්ධ වීමට නොහැකි වේ"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"උපාංගය වෙත හස්තීයව යුගල කිරීමට උත්සාහ කරන්න"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"උපාංගය යුගල කිරීමේ ප්රකාරයට දැමීමට උත්සාහ කරන්න"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"ළඟා විය හැකි උපාංග"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"ඔබේ ගිණුම සමග උපාංග"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"ඔබේ සුරැකි උපාංගය ලබා ගත හැක"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"අවට"</string>
+ <string name="common_devices" msgid="2635603125608104442">"උපාංග"</string>
+ <string name="common_connecting" msgid="160531481424245303">"සම්බන්ධ වෙමින්…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"බැටරිය: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"නිමයි"</string>
<string name="paring_action_save" msgid="6259357442067880136">"සුරකින්න"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"සම්බන්ධ කරන්න"</string>
diff --git a/nearby/halfsheet/res/values-sk/strings.xml b/nearby/halfsheet/res/values-sk/strings.xml
index 3dddc25..7938211 100644
--- a/nearby/halfsheet/res/values-sk/strings.xml
+++ b/nearby/halfsheet/res/values-sk/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Spúšťa sa nastavenie…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavte zariadenie"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Zariadenie je pripojené"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Pripojené k zariadeniu %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nepodarilo sa pripojiť"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nepodarilo sa pripojiť"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Skúste zariadenie spárovať ručne"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Prepnite zariadenie do párovacieho režimu"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Zariadenia v dosahu"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Zariadenia s vaším účtom"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Uložené zariadenie je dostupné"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Nablízku"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Zariadenia"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Pripája sa…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batéria: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Hotovo"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Uložiť"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Pripojiť"</string>
diff --git a/nearby/halfsheet/res/values-sl/strings.xml b/nearby/halfsheet/res/values-sl/strings.xml
index e4f3c91..9e9357c 100644
--- a/nearby/halfsheet/res/values-sl/strings.xml
+++ b/nearby/halfsheet/res/values-sl/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Začetek nastavitve …"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Nastavitev naprave"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Naprava je povezana"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Povezano z napravo »%s«"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Povezava ni mogoča"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Povezave ni mogoče vzpostaviti"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Poskusite ročno seznaniti napravo."</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Poskusite napravo preklopiti v način za seznanjanje."</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Naprave znotraj dosega"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Naprave z vašim računom"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Shranjena naprava je na voljo"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Bližina"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Naprave"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Povezovanje …"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterija: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Končano"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Shrani"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Poveži"</string>
diff --git a/nearby/halfsheet/res/values-sq/strings.xml b/nearby/halfsheet/res/values-sq/strings.xml
index 9265d1f..538e9d6 100644
--- a/nearby/halfsheet/res/values-sq/strings.xml
+++ b/nearby/halfsheet/res/values-sq/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Po nis konfigurimin…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfiguro pajisjen"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Pajisja u lidh"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"U lidh me “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Nuk mund të lidhej"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Nuk mund të lidhet"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Provo të çiftosh me pajisjen manualisht"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Provo ta vendosësh pajisjen në modalitetin e çiftimit"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Pajisjet që mund të arrish"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Pajisjet me llogarinë tënde"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Pajisja jote e ruajtur ofrohet"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Në afërsi"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Pajisjet"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Po lidhet…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Bateria: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"U krye"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Ruaj"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Lidh"</string>
diff --git a/nearby/halfsheet/res/values-sr/strings.xml b/nearby/halfsheet/res/values-sr/strings.xml
index 094be03..c4bcd19 100644
--- a/nearby/halfsheet/res/values-sr/strings.xml
+++ b/nearby/halfsheet/res/values-sr/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Подешавање се покреће…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Подесите уређај"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Уређај је повезан"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Повезани сте са уређајем %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Повезивање није успело"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Повезивање није успело"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Пробајте да упарите ручно са уређајем"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Пробајте да пребаците уређај у режим упаривања"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Уређаји у домету"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Уређаји повезани са налогом"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Сачувани уређај је доступан"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"У близини"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Уређаји"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Повезује се…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Батерија: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Сачувај"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Повежи"</string>
diff --git a/nearby/halfsheet/res/values-sv/strings.xml b/nearby/halfsheet/res/values-sv/strings.xml
index 297b7bc..b00091c 100644
--- a/nearby/halfsheet/res/values-sv/strings.xml
+++ b/nearby/halfsheet/res/values-sv/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Konfigureringen startas …"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Konfigurera enheten"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Enheten är ansluten"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ansluten till %s"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Det gick inte att ansluta"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Det går inte att ansluta"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Testa att parkoppla enheten manuellt"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Testa att aktivera parkopplingsläget på enheten"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Enheter inom räckvidd"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Enheter med ditt konto"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"En sparad enhet är tillgänglig"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Närdelning"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Enheter"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Ansluter …"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batteri: %d %%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Klar"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Spara"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Anslut"</string>
diff --git a/nearby/halfsheet/res/values-sw/strings.xml b/nearby/halfsheet/res/values-sw/strings.xml
index bf0bfeb..238a288 100644
--- a/nearby/halfsheet/res/values-sw/strings.xml
+++ b/nearby/halfsheet/res/values-sw/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Inaanza Kuweka Mipangilio…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Weka mipangilio ya kifaa"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Kifaa kimeunganishwa"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Imeunganishwa kwenye “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Imeshindwa kuunganisha"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Imeshindwa kuunganisha"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Jaribu kuoanisha mwenyewe kwenye kifaa"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Jaribu kuweka kifaa katika hali ya kuoanisha"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Vifaa vilivyo karibu nawe"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Vifaa vilivyounganishwa kwenye akaunti yako"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Kifaa ulichohifadhi kinapatikana"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Uhamishaji wa Karibu"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Vifaa"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Inaunganisha…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Betri: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Imemaliza"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Hifadhi"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Unganisha"</string>
diff --git a/nearby/halfsheet/res/values-ta/strings.xml b/nearby/halfsheet/res/values-ta/strings.xml
index dfd67a6..baadcc2 100644
--- a/nearby/halfsheet/res/values-ta/strings.xml
+++ b/nearby/halfsheet/res/values-ta/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"அமைவைத் தொடங்குகிறது…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"சாதனத்தை அமையுங்கள்"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"சாதனம் இணைக்கப்பட்டது"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s” உடன் இணைக்கப்பட்டது"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"இணைக்க முடியவில்லை"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"இணைக்க முடியவில்லை"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"சாதனத்துடன் நீங்களாகவே இணைக்க முயலவும்"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"சாதனத்தை \'இணைத்தல் பயன்முறையில்\' வைக்கவும்"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"தொடர்பு வரம்பிலுள்ள சாதனங்கள்"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"உங்கள் கணக்குடன் இணைந்துள்ள சாதனங்கள்"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"நீங்கள் சேமித்த சாதனம் உள்ளது"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"அருகில் பகிர்தல்"</string>
+ <string name="common_devices" msgid="2635603125608104442">"சாதனங்கள்"</string>
+ <string name="common_connecting" msgid="160531481424245303">"இணைக்கிறது…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"பேட்டரி: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"முடிந்தது"</string>
<string name="paring_action_save" msgid="6259357442067880136">"சேமி"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"இணை"</string>
diff --git a/nearby/halfsheet/res/values-te/strings.xml b/nearby/halfsheet/res/values-te/strings.xml
index 87be145..cb8f91b 100644
--- a/nearby/halfsheet/res/values-te/strings.xml
+++ b/nearby/halfsheet/res/values-te/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"సెటప్ ప్రారంభమవుతోంది…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"పరికరాన్ని సెటప్ చేయండి"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"పరికరం కనెక్ట్ చేయబడింది"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"“%s”కు కనెక్ట్ చేయబడింది"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"కనెక్ట్ చేయడం సాధ్యపడలేదు"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"కనెక్ట్ చేయలేకపోయింది"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"పరికరానికి మాన్యువల్గా పెయిరింగ్ చేయడానికి ట్రై చేయండి"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"పరికరాన్ని పెయిరింగ్ మోడ్లో ఉంచడానికి ట్రై చేయండి"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"అందుబాటులో ఉన్న పరికరాలు"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"మీ ఖాతా ఉన్న పరికరాలు"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"మీ సేవ్ చేసిన పరికరం అందుబాటులో ఉంది"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"సమీపం"</string>
+ <string name="common_devices" msgid="2635603125608104442">"పరికరాలు"</string>
+ <string name="common_connecting" msgid="160531481424245303">"కనెక్ట్ అవుతోంది…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"బ్యాటరీ: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"పూర్తయింది"</string>
<string name="paring_action_save" msgid="6259357442067880136">"సేవ్ చేయండి"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"కనెక్ట్ చేయండి"</string>
diff --git a/nearby/halfsheet/res/values-th/strings.xml b/nearby/halfsheet/res/values-th/strings.xml
index bc4296b..f5c5c2e 100644
--- a/nearby/halfsheet/res/values-th/strings.xml
+++ b/nearby/halfsheet/res/values-th/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"กำลังเริ่มการตั้งค่า…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"ตั้งค่าอุปกรณ์"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"เชื่อมต่ออุปกรณ์แล้ว"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"เชื่อมต่อกับ \"%s\" แล้ว"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"เชื่อมต่อไม่ได้"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"เชื่อมต่อไม่ได้"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"ลองจับคู่อุปกรณ์ด้วยตนเอง"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"พยายามนำอุปกรณ์เข้าสู่โหมดการจับคู่"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"อุปกรณ์ที่อยู่ติดกัน"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"อุปกรณ์ที่มีบัญชีของคุณ"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"อุปกรณ์ที่บันทึกพร้อมใช้แล้ว"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"ใกล้เคียง"</string>
+ <string name="common_devices" msgid="2635603125608104442">"อุปกรณ์"</string>
+ <string name="common_connecting" msgid="160531481424245303">"กำลังเชื่อมต่อ…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"แบตเตอรี่: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"เสร็จสิ้น"</string>
<string name="paring_action_save" msgid="6259357442067880136">"บันทึก"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"เชื่อมต่อ"</string>
diff --git a/nearby/halfsheet/res/values-tl/strings.xml b/nearby/halfsheet/res/values-tl/strings.xml
index a6de0e8..a546da6 100644
--- a/nearby/halfsheet/res/values-tl/strings.xml
+++ b/nearby/halfsheet/res/values-tl/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Sinisimulan ang Pag-set Up…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"I-set up ang device"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Naikonekta na ang device"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Nakakonekta sa “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Hindi makakonekta"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Hindi makakonekta"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Subukang manual na magpares sa device"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Subukang ilagay sa pairing mode ang device"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Mga naaabot na device"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Mga device sa iyong account"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Available ang iyong na-save na device"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Kalapit"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Mga Device"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Kumokonekta…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Baterya: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Tapos na"</string>
<string name="paring_action_save" msgid="6259357442067880136">"I-save"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Kumonekta"</string>
diff --git a/nearby/halfsheet/res/values-tr/strings.xml b/nearby/halfsheet/res/values-tr/strings.xml
index cd5a6ea..a54c5e7 100644
--- a/nearby/halfsheet/res/values-tr/strings.xml
+++ b/nearby/halfsheet/res/values-tr/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Kurulum Başlatılıyor…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Cihazı kur"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Cihaz bağlandı"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"%s\" cihazına bağlanıldı"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Bağlanamadı"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Bağlantı kurulamadı"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Cihazla manuel olarak eşlemeyi deneyin"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Cihazı eşleme moduna geçirmeyi deneyin"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Erişilebilecek cihazlar"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Hesabınıza bağlı cihazlar"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Kayıtlı cihazınız kullanılabilir"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Yakındaki"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Cihazlar"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Bağlanıyor…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Pil seviyesi: %%%d"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Bitti"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Kaydet"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Bağlan"</string>
diff --git a/nearby/halfsheet/res/values-uk/strings.xml b/nearby/halfsheet/res/values-uk/strings.xml
index 242ca07..ab73c1f 100644
--- a/nearby/halfsheet/res/values-uk/strings.xml
+++ b/nearby/halfsheet/res/values-uk/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Запуск налаштування…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Налаштуйте пристрій"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Пристрій підключено"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Підключено до пристрою \"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Не вдалося підключити"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Не вдалося підключитися"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Спробуйте підключитися до пристрою вручну"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Спробуйте ввімкнути на пристрої режим підключення"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Пристрої в радіусі дії"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Пристрої з вашим обліковим записом"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Збережений пристрій доступний"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Поблизу"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Пристрої"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Підключення…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Акумулятор: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Готово"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Зберегти"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Підключити"</string>
diff --git a/nearby/halfsheet/res/values-ur/strings.xml b/nearby/halfsheet/res/values-ur/strings.xml
index 4a4a59c..a2b2038 100644
--- a/nearby/halfsheet/res/values-ur/strings.xml
+++ b/nearby/halfsheet/res/values-ur/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"سیٹ اپ شروع ہو رہا ہے…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"آلہ سیٹ اپ کریں"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"آلہ منسلک ہے"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"\"\"%s سے منسلک ہے"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"منسلک نہیں ہو سکا"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"منسلک ہونے سے قاصر"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"دستی طور پر آلے کے ساتھ جوڑا بنانا آزمائیں"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"آلے کو جوڑا بنانے والے موڈ میں رکھ کر آزمائیں"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"رسائی کے اندر آلات"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"آپ کے اکاؤنٹ سے منسلک آلات"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"آپ کا محفوظ کردہ آلہ دستیاب ہے"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"قریبی"</string>
+ <string name="common_devices" msgid="2635603125608104442">"آلات"</string>
+ <string name="common_connecting" msgid="160531481424245303">"منسلک ہو رہا ہے…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"بیٹری: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"ہو گیا"</string>
<string name="paring_action_save" msgid="6259357442067880136">"محفوظ کریں"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"منسلک کریں"</string>
diff --git a/nearby/halfsheet/res/values-uz/strings.xml b/nearby/halfsheet/res/values-uz/strings.xml
index 420512d..70c190a 100644
--- a/nearby/halfsheet/res/values-uz/strings.xml
+++ b/nearby/halfsheet/res/values-uz/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Sozlash boshlandi…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Qurilmani sozlash"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Qurilma ulandi"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Bunga ulandi: “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Ulanmadi"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ulanmadi"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Qurilmangizga odatiy usulda ulaning"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Qurilmada ulanish rejimini yoqing"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Atrofdagi qurilmalar"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Hisobingizga ulangan qurilmalar"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Saqlangan qurilmangiz mavjud"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Yaqin-atrofda"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Qurilmalar"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Ulanmoqda…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Batareya: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Tayyor"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Saqlash"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Ulanish"</string>
diff --git a/nearby/halfsheet/res/values-vi/strings.xml b/nearby/halfsheet/res/values-vi/strings.xml
index 9c1e052..e2ea467 100644
--- a/nearby/halfsheet/res/values-vi/strings.xml
+++ b/nearby/halfsheet/res/values-vi/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Đang bắt đầu thiết lập…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Thiết lập thiết bị"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Đã kết nối thiết bị"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Đã kết nối với “%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Không kết nối được"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Không thể kết nối"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Hãy thử ghép nối thủ công với thiết bị"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Hãy thử đưa thiết bị này vào chế độ ghép nối"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Thiết bị trong tầm tay"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Các thiết bị có tài khoản của bạn"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Thiết bị đã lưu của bạn có sẵn"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Lân cận"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Thiết bị"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Đang kết nối…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Pin: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Xong"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Lưu"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Kết nối"</string>
diff --git a/nearby/halfsheet/res/values-zh-rCN/strings.xml b/nearby/halfsheet/res/values-zh-rCN/strings.xml
index 482b5c4..8117bac 100644
--- a/nearby/halfsheet/res/values-zh-rCN/strings.xml
+++ b/nearby/halfsheet/res/values-zh-rCN/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"正在启动设置…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"设置设备"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"设备已连接"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"已连接到“%s”"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"无法连接"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"无法连接"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"请尝试手动与该设备配对"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"请尝试让设备进入配对模式"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"附近的设备"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"与您的帐号相关联的设备"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"您保存的设备已可供使用"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"附近"</string>
+ <string name="common_devices" msgid="2635603125608104442">"设备"</string>
+ <string name="common_connecting" msgid="160531481424245303">"正在连接…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"电量:%d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
<string name="paring_action_save" msgid="6259357442067880136">"保存"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"连接"</string>
diff --git a/nearby/halfsheet/res/values-zh-rHK/strings.xml b/nearby/halfsheet/res/values-zh-rHK/strings.xml
index 3ca73e6..4dac931 100644
--- a/nearby/halfsheet/res/values-zh-rHK/strings.xml
+++ b/nearby/halfsheet/res/values-zh-rHK/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"開始設定…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"設定裝置"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"已連接裝置"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"已連線至「%s」"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"無法連接"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"無法連線"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"嘗試手動配對裝置"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"嘗試讓裝置進入配對模式"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"附近的裝置"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"已連結你帳戶的裝置"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"你儲存的裝置已可使用"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"咫尺共享"</string>
+ <string name="common_devices" msgid="2635603125608104442">"裝置"</string>
+ <string name="common_connecting" msgid="160531481424245303">"正在連接…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"電量:%d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
<string name="paring_action_save" msgid="6259357442067880136">"儲存"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"連接"</string>
diff --git a/nearby/halfsheet/res/values-zh-rTW/strings.xml b/nearby/halfsheet/res/values-zh-rTW/strings.xml
index b4e680d..0c90ebb 100644
--- a/nearby/halfsheet/res/values-zh-rTW/strings.xml
+++ b/nearby/halfsheet/res/values-zh-rTW/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"正在啟動設定程序…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"設定裝置"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"裝置已連線"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"已連線到「%s」"</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"無法連線"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"無法連線"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"嘗試手動配對裝置"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"嘗試讓裝置進入配對模式"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"鄰近裝置"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"與你帳戶連結的裝置"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"你儲存的裝置已可使用"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"鄰近分享"</string>
+ <string name="common_devices" msgid="2635603125608104442">"裝置"</string>
+ <string name="common_connecting" msgid="160531481424245303">"連線中…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"電池電量:%d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"完成"</string>
<string name="paring_action_save" msgid="6259357442067880136">"儲存"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"連線"</string>
diff --git a/nearby/halfsheet/res/values-zu/strings.xml b/nearby/halfsheet/res/values-zu/strings.xml
index 33fb405..3f26469 100644
--- a/nearby/halfsheet/res/values-zu/strings.xml
+++ b/nearby/halfsheet/res/values-zu/strings.xml
@@ -20,7 +20,18 @@
<string name="fast_pair_setup_in_progress" msgid="4158762239172829807">"Iqalisa Ukusetha…"</string>
<string name="fast_pair_title_setup" msgid="2894360355540593246">"Setha idivayisi"</string>
<string name="fast_pair_device_ready" msgid="2903490346082833101">"Idivayisi ixhunyiwe"</string>
+ <string name="fast_pair_device_ready_with_device_name" msgid="2151967995692339422">"Ixhunywe ku-\"%s\""</string>
<string name="fast_pair_title_fail" msgid="5677174346601290232">"Ayikwazanga ukuxhuma"</string>
+ <string name="fast_pair_unable_to_connect" msgid="3661854812014294569">"Ayikwazanga ukuxhuma"</string>
+ <string name="fast_pair_unable_to_connect_description" msgid="3926830740860653891">"Zama ukubhangqa kule divayisi"</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" msgid="3197372738187738030">"Zama ukubeka le divayisi kumodi yokubhangqa"</string>
+ <string name="devices_within_reach_channel_name" msgid="876280551450910440">"Amadivayisi afinyelelekayo"</string>
+ <string name="devices_with_your_account_channel_name" msgid="8120067812798598102">"Amadivayisi ane-akhawunti yakho"</string>
+ <string name="fast_pair_your_device" msgid="3662423897069320840">"Idivayisi yakho elondoloziwe ikhona"</string>
+ <string name="common_nearby_title" msgid="5480324514713607015">"Eduze"</string>
+ <string name="common_devices" msgid="2635603125608104442">"Amadivayisi"</string>
+ <string name="common_connecting" msgid="160531481424245303">"Iyaxhuma…"</string>
+ <string name="common_battery_level" msgid="8748495823047456645">"Ibhethri: %d%%"</string>
<string name="paring_action_done" msgid="6888875159174470731">"Kwenziwe"</string>
<string name="paring_action_save" msgid="6259357442067880136">"Londoloza"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Xhuma"</string>
diff --git a/nearby/halfsheet/res/values/strings.xml b/nearby/halfsheet/res/values/strings.xml
index 01a82e4..c1f53d4 100644
--- a/nearby/halfsheet/res/values/strings.xml
+++ b/nearby/halfsheet/res/values/strings.xml
@@ -35,8 +35,22 @@
[CHAR LIMIT=30]
-->
<string name="fast_pair_device_ready">Device connected</string>
+ <string name="fast_pair_device_ready_with_device_name" description="Notification title combined with device name after we successfully pair with the audio device. For example: Connected to "Tommy's Bose QC35." [BACKUP_MESSAGE_ID: 6018442069058338390]">Connected to \u201c%s\u201d</string>
<!-- Title text shown when peripheral device fail to connect to phone. [CHAR_LIMIT=30] -->
<string name="fast_pair_title_fail">Couldn\'t connect</string>
+ <string name="fast_pair_unable_to_connect" description="Notification title after a pairing has failed. [CHAR LIMIT=30]">Unable to connect</string>
+ <string name="fast_pair_unable_to_connect_description" description="Notification body after a pairing has failed. [CHAR LIMIT=120]">Try manually pairing to the device</string>
+ <string name="fast_pair_turn_on_bt_device_pairing_mode" description="Notification body after a pairing has failed. [CHAR LIMIT=120]">Try putting the device into pairing mode</string>
+
+ <!--
+ ============================================================
+ PAIRING NOTIFICATION
+ ============================================================
+ -->
+
+ <string name="devices_within_reach_channel_name" description="Notification channel for devices within reach. [CHAR LIMIT=37]">Devices within reach</string>
+ <string name="devices_with_your_account_channel_name" description="Notification channel for devices that are connected to the user's account. [CHAR LIMIT=37]">Devices with your account</string>
+ <string name="fast_pair_your_device" description="Notification title for devices that are recognized as being owned by you. [CHAR LIMIT=30]">Your saved device is available</string>
<!--
============================================================
@@ -44,6 +58,14 @@
============================================================
-->
+ <!-- Title for Nearby component [CHAR LIMIT=40] -->
+ <string name="common_nearby_title">Nearby</string>
+ <!-- The product name for devices notification and list view. [CHAR LIMIT=37]-->
+ <string name="common_devices">Devices</string>
+ <!-- Text used to indicate that a connection attempt is ongoing [CHAR LIMIT=20] -->
+ <string name="common_connecting">Connecting…</string>
+ <!-- Label describing the battery level, for example "Battery: 72%". [CHAR LIMIT=60] -->
+ <string name="common_battery_level">Battery: %d%%</string>
<!--
A button shown after paring process to dismiss the current activity.
[CHAR LIMIT=30]
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
index 07e5776..94f4ef4 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
@@ -18,13 +18,19 @@
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
+import static com.android.nearby.halfsheet.constants.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_FOREGROUND;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_INFO;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_IS_RETROACTIVE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_TYPE;
+import static com.android.nearby.halfsheet.constants.Constant.TAG;
+import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
+import static com.android.nearby.halfsheet.constants.UserActionHandlerBase.EXTRA_MAC_ADDRESS;
import static com.android.nearby.halfsheet.fragment.DevicePairingFragment.APP_LAUNCH_FRAGMENT_TYPE;
-import static com.android.server.nearby.common.bluetooth.fastpair.FastPairConstants.EXTRA_MODEL_ID;
-import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_MAC_ADDRESS;
-import static com.android.server.nearby.fastpair.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
-import static com.android.server.nearby.fastpair.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_TYPE;
import android.content.Intent;
import android.os.Bundle;
@@ -49,31 +55,6 @@
* A class show Fast Pair related information in Half sheet format.
*/
public class HalfSheetActivity extends FragmentActivity {
-
- public static final String TAG = "FastPairHalfSheet";
-
- public static final String EXTRA_HALF_SHEET_CONTENT =
- "com.android.nearby.halfsheet.HALF_SHEET_CONTENT";
- public static final String EXTRA_TITLE =
- "com.android.nearby.halfsheet.HALF_SHEET_TITLE";
- public static final String EXTRA_DESCRIPTION =
- "com.android.nearby.halfsheet.HALF_SHEET_DESCRIPTION";
- public static final String EXTRA_HALF_SHEET_ID =
- "com.android.nearby.halfsheet.HALF_SHEET_ID";
- public static final String EXTRA_HALF_SHEET_IS_RETROACTIVE =
- "com.android.nearby.halfsheet.HALF_SHEET_IS_RETROACTIVE";
- public static final String EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR =
- "com.android.nearby.halfsheet.HALF_SHEET_IS_SUBSEQUENT_PAIR";
- public static final String EXTRA_HALF_SHEET_PAIRING_RESURFACE =
- "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_PAIRING_RESURFACE";
- public static final String ACTION_HALF_SHEET_FOREGROUND_STATE =
- "com.android.nearby.halfsheet.ACTION_HALF_SHEET_FOREGROUND_STATE";
- // Intent extra contains the user gmail name eg. testaccount@gmail.com.
- public static final String EXTRA_HALF_SHEET_ACCOUNT_NAME =
- "com.android.nearby.halfsheet.HALF_SHEET_ACCOUNT_NAME";
- public static final String EXTRA_HALF_SHEET_FOREGROUND =
- "com.android.nearby.halfsheet.EXTRA_HALF_SHEET_FOREGROUND";
- public static final String ARG_FRAGMENT_STATE = "ARG_FRAGMENT_STATE";
@Nullable
private HalfSheetModuleFragment mHalfSheetModuleFragment;
@Nullable
@@ -141,6 +122,10 @@
@Override
protected void onStart() {
super.onStart();
+ BroadcastUtils.sendBroadcast(
+ this,
+ new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
+ .putExtra(EXTRA_HALF_SHEET_FOREGROUND, true));
}
@Override
@@ -207,30 +192,48 @@
finish();
}
+
+ /**
+ * Changes the half sheet ban state to active.
+ * Sometimes users leave half sheet to go to fast pair info page,
+ * we do not want the behavior to be counted as dismiss.
+ */
+ public void sendBanStateResetBroadcast() {
+ if (mScanFastPairStoreItem == null) {
+ return;
+ }
+ BroadcastUtils.sendBroadcast(
+ this,
+ new Intent(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET)
+ .putExtra(EXTRA_MODEL_ID, mScanFastPairStoreItem.getModelId()
+ .toLowerCase(Locale.ROOT)));
+ }
+
private void sendHalfSheetCancelBroadcast() {
BroadcastUtils.sendBroadcast(
this,
new Intent(ACTION_HALF_SHEET_FOREGROUND_STATE)
.putExtra(EXTRA_HALF_SHEET_FOREGROUND, false));
- if (mScanFastPairStoreItem != null) {
- BroadcastUtils.sendBroadcast(
- this,
- new Intent(ACTION_FAST_PAIR_HALF_SHEET_CANCEL)
- .putExtra(EXTRA_MODEL_ID,
- mScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT))
- .putExtra(EXTRA_HALF_SHEET_TYPE,
- getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE))
- .putExtra(
- EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
- getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
- false))
- .putExtra(
- EXTRA_HALF_SHEET_IS_RETROACTIVE,
- getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_RETROACTIVE,
- false))
- .putExtra(EXTRA_MAC_ADDRESS, mScanFastPairStoreItem.getAddress()),
- ACCESS_FINE_LOCATION);
+ if (mScanFastPairStoreItem == null) {
+ return;
}
+ BroadcastUtils.sendBroadcast(
+ this,
+ new Intent(ACTION_FAST_PAIR_HALF_SHEET_CANCEL)
+ .putExtra(EXTRA_MODEL_ID,
+ mScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT))
+ .putExtra(EXTRA_HALF_SHEET_TYPE,
+ getIntent().getStringExtra(EXTRA_HALF_SHEET_TYPE))
+ .putExtra(
+ EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
+ getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR,
+ false))
+ .putExtra(
+ EXTRA_HALF_SHEET_IS_RETROACTIVE,
+ getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_RETROACTIVE,
+ false))
+ .putExtra(EXTRA_MAC_ADDRESS, mScanFastPairStoreItem.getAddress()),
+ ACCESS_FINE_LOCATION);
}
@Override
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/Constant.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/Constant.java
new file mode 100644
index 0000000..65c76d1
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/Constant.java
@@ -0,0 +1,65 @@
+/*
+ * 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.nearby.halfsheet.constants;
+
+/**
+ * String constant for half sheet.
+ */
+public class Constant {
+ public static final String TAG = "FastPairHalfSheet";
+ private static final String PREFIX = "com.android.nearby.halfsheet.";
+
+ // Intent extra
+ public static final String EXTRA_BINDER = "com.android.server.nearby.fastpair.BINDER";
+ public static final String EXTRA_BUNDLE = "com.android.server.nearby.fastpair.BUNDLE_EXTRA";
+
+ public static final String EXTRA_TITLE = PREFIX + "HALF_SHEET_TITLE";
+ public static final String EXTRA_DESCRIPTION = PREFIX + "HALF_SHEET_DESCRIPTION";
+ public static final String EXTRA_HALF_SHEET_ID = PREFIX + "HALF_SHEET_ID";
+ public static final String EXTRA_HALF_SHEET_INFO = PREFIX + "HALF_SHEET";
+ public static final String EXTRA_HALF_SHEET_TYPE = PREFIX + "HALF_SHEET_TYPE";
+ public static final String EXTRA_HALF_SHEET_ACCOUNT_NAME = PREFIX + "HALF_SHEET_ACCOUNT_NAME";
+ public static final String EXTRA_HALF_SHEET_CONTENT = PREFIX + "HALF_SHEET_CONTENT";
+ public static final String EXTRA_HALF_SHEET_FOREGROUND =
+ PREFIX + "EXTRA_HALF_SHEET_FOREGROUND";
+ public static final String EXTRA_HALF_SHEET_IS_RETROACTIVE =
+ PREFIX + "HALF_SHEET_IS_RETROACTIVE";
+ public static final String EXTRA_HALF_SHEET_IS_SUBSEQUENT_PAIR =
+ PREFIX + "HALF_SHEET_IS_SUBSEQUENT_PAIR";
+ public static final String EXTRA_HALF_SHEET_PAIRING_RESURFACE =
+ PREFIX + "EXTRA_HALF_SHEET_PAIRING_RESURFACE";
+
+ // Intent Actions
+ public static final String ACTION_HALF_SHEET_FOREGROUND_STATE =
+ PREFIX + "ACTION_HALF_SHEET_FOREGROUND_STATE";
+ public static final String ACTION_FAST_PAIR_HALF_SHEET_CANCEL =
+ "com.android.nearby.ACTION_FAST_PAIR_HALF_SHEET_CANCEL";
+ public static final String ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET =
+ "com.android.nearby.ACTION_FAST_PAIR_BAN_STATE_RESET";
+ public static final String ACTION_RESOURCES_APK =
+ "android.nearby.SHOW_HALFSHEET";
+ public static final String ACTION_FAST_PAIR = PREFIX + "ACTION_MAGIC_PAIR";
+
+ public static final String RESULT_FAIL = "RESULT_FAIL";
+ public static final String ARG_FRAGMENT_STATE = "ARG_FRAGMENT_STATE";
+ public static final String DEVICE_PAIRING_FRAGMENT_TYPE = "DEVICE_PAIRING";
+
+ // Content url for help page about Fast Pair in half sheet.
+ // Todo(b/246007000): Add a flag to set up content url of the help page.
+ public static final String FAST_PAIR_HALF_SHEET_HELP_URL =
+ "https://support.google.com/android/answer/9075925";
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/FastPairConstants.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/FastPairConstants.java
new file mode 100644
index 0000000..7cfd33a
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/FastPairConstants.java
@@ -0,0 +1,26 @@
+/*
+ * 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.nearby.halfsheet.constants;
+
+/** Constants to share with other team. */
+public class FastPairConstants {
+ private static final String PACKAGE_NAME = "com.android.server.nearby";
+ public static final String PREFIX = PACKAGE_NAME + ".common.bluetooth.fastpair.";
+
+ /** MODEL_ID item name for extended intent field. */
+ public static final String EXTRA_MODEL_ID = PREFIX + "MODEL_ID";
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/fastpair/service/UserActionHandlerBase.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/UserActionHandlerBase.java
similarity index 85%
rename from nearby/service/java/com/android/server/nearby/common/fastpair/service/UserActionHandlerBase.java
rename to nearby/halfsheet/src/com/android/nearby/halfsheet/constants/UserActionHandlerBase.java
index 67d87e3..767c6d6 100644
--- a/nearby/service/java/com/android/server/nearby/common/fastpair/service/UserActionHandlerBase.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/constants/UserActionHandlerBase.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.nearby.common.fastpair.service;
+package com.android.nearby.halfsheet.constants;
/** Handles intents to {@link com.android.server.nearby.fastpair.FastPairManager}. */
public class UserActionHandlerBase {
@@ -25,5 +25,5 @@
public static final String EXTRA_COMPANION_APP = ACTION_PREFIX + "EXTRA_COMPANION_APP";
public static final String EXTRA_MAC_ADDRESS = PREFIX + "EXTRA_MAC_ADDRESS";
+ public static final String ACTION_FAST_PAIR = ACTION_PREFIX + "ACTION_FAST_PAIR";
}
-
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
index 320965b..9f5c915 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/DevicePairingFragment.java
@@ -17,22 +17,24 @@
import static android.text.TextUtils.isEmpty;
-import static com.android.nearby.halfsheet.HalfSheetActivity.ARG_FRAGMENT_STATE;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_DESCRIPTION;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_ACCOUNT_NAME;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_CONTENT;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_HALF_SHEET_ID;
-import static com.android.nearby.halfsheet.HalfSheetActivity.EXTRA_TITLE;
-import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.constants.Constant.ARG_FRAGMENT_STATE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BINDER;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BUNDLE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_DESCRIPTION;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_ACCOUNT_NAME;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_CONTENT;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_ID;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_INFO;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_TITLE;
+import static com.android.nearby.halfsheet.constants.Constant.FAST_PAIR_HALF_SHEET_HELP_URL;
+import static com.android.nearby.halfsheet.constants.Constant.RESULT_FAIL;
+import static com.android.nearby.halfsheet.constants.Constant.TAG;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FAILED;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.FOUND_DEVICE;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_LAUNCHABLE;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRED_UNLAUNCHABLE;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.PAIRING;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_BINDER;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_BUNDLE;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
@@ -61,6 +63,7 @@
import com.android.nearby.halfsheet.HalfSheetActivity;
import com.android.nearby.halfsheet.R;
import com.android.nearby.halfsheet.utils.FastPairUtils;
+import com.android.nearby.halfsheet.utils.HelpUtils;
import com.android.nearby.halfsheet.utils.IconUtils;
import com.google.protobuf.InvalidProtocolBufferException;
@@ -183,6 +186,11 @@
new FastPairUiServiceClient(getContext(), mBundle.getBinder(EXTRA_BINDER));
mFastPairUiServiceClient.registerHalfSheetStateCallBack(this);
}
+ if (args.containsKey(EXTRA_HALF_SHEET_CONTENT)) {
+ if (RESULT_FAIL.equals(args.getString(EXTRA_HALF_SHEET_CONTENT))) {
+ mPairStatus = PairStatusMetadata.Status.FAIL;
+ }
+ }
if (args.containsKey(ARG_FRAGMENT_STATE)) {
mFragmentState = (HalfSheetFragmentState) args.getSerializable(ARG_FRAGMENT_STATE);
}
@@ -230,12 +238,12 @@
if (icon != null) {
mImage.setImageBitmap(icon);
}
- mConnectButton.setOnClickListener(v -> onConnectClick());
+ mConnectButton.setOnClickListener(v -> onConnectClicked());
mCancelButton.setOnClickListener(v ->
((HalfSheetActivity) getActivity()).onCancelClicked());
mSettingsButton.setOnClickListener(v -> onSettingsClicked());
- mSetupButton.setOnClickListener(v -> onSetupClick());
-
+ mSetupButton.setOnClickListener(v -> onSetupClicked());
+ mInfoIconButton.setOnClickListener(v -> onHelpClicked());
return rootView;
}
@@ -250,7 +258,14 @@
public void onStart() {
super.onStart();
Log.v(TAG, "onStart: invalidate states");
- invalidateState();
+ // If the fragmentState is not NOT_STARTED, it is because the fragment was just resumed from
+ // configuration change (e.g. rotating the screen or half-sheet resurface). Let's recover
+ // the UI directly.
+ if (mFragmentState != NOT_STARTED) {
+ setState(mFragmentState);
+ } else {
+ invalidateState();
+ }
}
@Override
@@ -266,7 +281,7 @@
startActivity(new Intent(Settings.ACTION_BLUETOOTH_SETTINGS));
}
- private void onSetupClick() {
+ private void onSetupClicked() {
String companionApp =
FastPairUtils.getCompanionAppFromActionUrl(mScanFastPairStoreItem.getActionUrl());
Intent intent =
@@ -284,7 +299,7 @@
}
}
- private void onConnectClick() {
+ private void onConnectClicked() {
if (mScanFastPairStoreItem == null) {
Log.w(TAG, "No pairing related information in half sheet");
return;
@@ -303,6 +318,12 @@
.build());
}
+ private void onHelpClicked() {
+ HelpUtils.showHelpPage(getContext(), FAST_PAIR_HALF_SHEET_HELP_URL);
+ ((HalfSheetActivity) getActivity()).sendBanStateResetBroadcast();
+ getActivity().finish();
+ }
+
// Receives callback from service.
@Override
public void onPairUpdate(FastPairDevice fastPairDevice, PairStatusMetadata pairStatusMetadata) {
@@ -475,8 +496,7 @@
case FAILED:
return mScanFastPairStoreItem.getFastPairStrings().getPairingFailDescription();
case PAIRED_UNLAUNCHABLE:
- getString(R.string.fast_pair_device_ready);
- // fall through
+ return getString(R.string.fast_pair_device_ready);
case FOUND_DEVICE:
case NOT_STARTED:
return mScanFastPairStoreItem.getFastPairStrings().getInitialPairingDescription();
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
index f1db4d0..d87c015 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/fragment/HalfSheetModuleFragment.java
@@ -15,7 +15,7 @@
*/
package com.android.nearby.halfsheet.fragment;
-import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.constants.Constant.TAG;
import static com.android.nearby.halfsheet.fragment.HalfSheetModuleFragment.HalfSheetFragmentState.NOT_STARTED;
import android.os.Bundle;
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
index 00a365c..a1588a9 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/FastPairUtils.java
@@ -15,8 +15,8 @@
*/
package com.android.nearby.halfsheet.utils;
-import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_COMPANION_APP;
-import static com.android.server.nearby.fastpair.UserActionHandler.ACTION_FAST_PAIR;
+import static com.android.nearby.halfsheet.constants.UserActionHandlerBase.ACTION_FAST_PAIR;
+import static com.android.nearby.halfsheet.constants.UserActionHandlerBase.EXTRA_COMPANION_APP;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/HelpUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/HelpUtils.java
new file mode 100644
index 0000000..98f2242
--- /dev/null
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/HelpUtils.java
@@ -0,0 +1,54 @@
+/*
+ * 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.nearby.halfsheet.utils;
+
+import static com.android.nearby.halfsheet.constants.Constant.TAG;
+
+import static java.util.Objects.requireNonNull;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * Util class for launching help page in Fast Pair.
+ */
+public class HelpUtils {
+ /**
+ * Sets up the info button to launch a help page
+ */
+ public static void showHelpPage(Context context, String url) {
+ requireNonNull(context, "context cannot be null");
+ requireNonNull(url, "url cannot be null");
+
+ try {
+ context.startActivity(createHelpPageIntent(url));
+ } catch (ActivityNotFoundException e) {
+ Log.w(TAG, "Failed to find activity for url " + url, e);
+ }
+ }
+
+ /**
+ * Creates the intent for help page
+ */
+ private static Intent createHelpPageIntent(String url) {
+ return new Intent(Intent.ACTION_VIEW, Uri.parse(url)).setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
index 218c756..e547369 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/IconUtils.java
@@ -16,17 +16,13 @@
package com.android.nearby.halfsheet.utils;
-import static com.android.nearby.halfsheet.HalfSheetActivity.TAG;
+import static com.android.nearby.halfsheet.constants.Constant.TAG;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
import android.util.Log;
import androidx.annotation.Nullable;
-import androidx.core.graphics.ColorUtils;
/**
* Utility class for icon size verification.
@@ -85,49 +81,12 @@
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, size);
if (IconUtils.isIconSizeCorrect(icon)) {
// Do not add background for Half Sheet.
- return IconUtils.addWhiteCircleBackground(icon);
+ return icon;
}
} catch (OutOfMemoryError e) {
Log.w(TAG, "getIcon: Failed to decode icon, returning null.", e);
}
return null;
}
-
- /** Adds a circular, white background to the bitmap. */
- @Nullable
- public static Bitmap addWhiteCircleBackground(Bitmap bitmap) {
- if (bitmap == null) {
- Log.w(TAG, "addWhiteCircleBackground: Bitmap is null, not adding background.");
- return null;
- }
-
- if (bitmap.getWidth() != bitmap.getHeight()) {
- Log.w(TAG, "addWhiteCircleBackground: Bitmap dimensions not square. Skipping"
- + "adding background.");
- return bitmap;
- }
-
- int padding = (int) (bitmap.getWidth() * NOTIFICATION_BACKGROUND_PADDING_PERCENT);
- Bitmap bitmapWithBackground =
- Bitmap.createBitmap(
- bitmap.getWidth() + (2 * padding),
- bitmap.getHeight() + (2 * padding),
- bitmap.getConfig());
- Canvas canvas = new Canvas(bitmapWithBackground);
- Paint paint = new Paint();
- paint.setColor(
- ColorUtils.setAlphaComponent(
- Color.WHITE, (int) (255 * NOTIFICATION_BACKGROUND_ALPHA)));
- paint.setStyle(Paint.Style.FILL);
- paint.setAntiAlias(true);
- canvas.drawCircle(
- bitmapWithBackground.getWidth() / 2,
- bitmapWithBackground.getHeight() / 2,
- bitmapWithBackground.getWidth() / 2,
- paint);
- canvas.drawBitmap(bitmap, padding, padding, null);
-
- return bitmapWithBackground;
- }
}
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index ef07bb9..d860048 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -24,23 +24,6 @@
],
}
-filegroup {
- name: "nearby-service-string-res",
- srcs: [
- "java/**/Constant.java",
- "java/**/UserActionHandlerBase.java",
- "java/**/UserActionHandler.java",
- "java/**/FastPairConstants.java",
- ],
-}
-
-java_library {
- name: "nearby-service-string",
- srcs: [":nearby-service-string-res"],
- libs: ["framework-bluetooth"],
- sdk_version: "module_current",
-}
-
// Common lib for nearby end-to-end testing.
java_library {
name: "nearby-common-lib",
@@ -90,6 +73,7 @@
"framework-configinfrastructure",
"framework-connectivity-t.impl",
"framework-statsd",
+ "HalfSheetUX",
],
static_libs: [
"androidx.core_core",
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 4c8067f..9b32d69 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -73,13 +73,12 @@
}
/**
- * Returns the DeviceConfig namespace for Nearby. The DeviceConfig#NAMESPACE_NEARBY was
+ * 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()) {
- // DeviceConfig.NAMESPACE_NEARBY
- return "nearby";
+ return DeviceConfig.NAMESPACE_NEARBY;
}
return DeviceConfig.NAMESPACE_TETHERING;
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 9a4834c..2ac2b00 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -37,6 +37,7 @@
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;
@@ -44,7 +45,9 @@
import com.android.server.nearby.fastpair.FastPairManager;
import com.android.server.nearby.injector.Injector;
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.provider.FastPairDataProvider;
import com.android.server.nearby.util.identity.CallerIdentity;
@@ -79,18 +82,21 @@
}
}
};
- private final DiscoveryProviderManager mProviderManager;
+ 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);
final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
mFastPairManager = new FastPairManager(lcw);
mPresenceManager = new PresenceManager(lcw);
mNearbyConfiguration = new NearbyConfiguration();
+ mDiscoveryProviderManager =
+ mNearbyConfiguration.refactorDiscoveryManager()
+ ? new DiscoveryProviderManager(context, mInjector)
+ : new DiscoveryProviderManagerLegacy(context, mInjector);
}
@VisibleForTesting
@@ -107,7 +113,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- return mProviderManager.registerScanListener(scanRequest, listener, identity);
+ return mDiscoveryProviderManager.registerScanListener(scanRequest, listener, identity);
}
@Override
@@ -118,7 +124,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- mProviderManager.unregisterScanListener(listener);
+ mDiscoveryProviderManager.unregisterScanListener(listener);
}
@Override
@@ -144,6 +150,11 @@
mBroadcastProviderManager.stopBroadcast(listener);
}
+ @Override
+ public void queryOffloadCapability(IOffloadCallback callback) {
+ mDiscoveryProviderManager.queryOffloadCapability(callback);
+ }
+
/**
* Called by the service initializer.
*
@@ -168,7 +179,7 @@
// Initialize ContextManager for CHRE scan.
((SystemInjector) mInjector).initializeContextHubManager();
}
- mProviderManager.init();
+ mDiscoveryProviderManager.init();
mContext.registerReceiver(
mBluetoothReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
index 0ff1bf2..008891f 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairConstants.java
@@ -16,15 +16,12 @@
package com.android.server.nearby.common.bluetooth.fastpair;
+import static com.android.nearby.halfsheet.constants.FastPairConstants.PREFIX;
+
import android.bluetooth.BluetoothDevice;
/** Constants to share with other team. */
public class FastPairConstants {
- private static final String PACKAGE_NAME = "com.android.server.nearby";
- private static final String PREFIX = PACKAGE_NAME + ".common.bluetooth.fastpair.";
-
- /** MODEL_ID item name for extended intent field. */
- public static final String EXTRA_MODEL_ID = PREFIX + "MODEL_ID";
/** CONNECTION_ID item name for extended intent field. */
public static final String EXTRA_CONNECTION_ID = PREFIX + "CONNECTION_ID";
/** BLUETOOTH_MAC_ADDRESS item name for extended intent field. */
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
index 0695b5f..f35703f 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/Constant.java
@@ -30,14 +30,4 @@
* Tag for Fast Pair service related logs.
*/
public static final String TAG = "FastPairService";
-
- public static final String EXTRA_BINDER = "com.android.server.nearby.fastpair.BINDER";
- public static final String EXTRA_BUNDLE = "com.android.server.nearby.fastpair.BUNDLE_EXTRA";
- public static final String ACTION_FAST_PAIR_HALF_SHEET_CANCEL =
- "com.android.nearby.ACTION_FAST_PAIR_HALF_SHEET_CANCEL";
- public static final String EXTRA_HALF_SHEET_INFO =
- "com.android.nearby.halfsheet.HALF_SHEET";
- public static final String EXTRA_HALF_SHEET_TYPE =
- "com.android.nearby.halfsheet.HALF_SHEET_TYPE";
- public static final String DEVICE_PAIRING_FRAGMENT_TYPE = "DEVICE_PAIRING";
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
index d459329..412b738 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -31,9 +31,14 @@
import com.android.server.nearby.common.ble.decode.FastPairDecoder;
import com.android.server.nearby.common.ble.util.RangingUtils;
import com.android.server.nearby.common.bloomfilter.BloomFilter;
+import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
import com.android.server.nearby.provider.FastPairDataProvider;
+import com.android.server.nearby.util.ArrayUtils;
import com.android.server.nearby.util.DataUtils;
import com.android.server.nearby.util.Hex;
@@ -49,10 +54,18 @@
public class FastPairAdvHandler {
Context mContext;
String mBleAddress;
- // Need to be deleted after notification manager in use.
- private boolean mIsFirst = false;
+ // TODO(b/247152236): Need to confirm the usage
+ // and deleted this after notification manager in use.
+ private boolean mIsFirst = true;
private FastPairDataProvider mPairDataProvider;
private static final double NEARBY_DISTANCE_THRESHOLD = 0.6;
+ // The byte, 0bLLLLTTTT, for battery length and type.
+ // Bit 0 - 3: type, 0b0011 (show UI indication) or 0b0100 (hide UI indication).
+ // Bit 4 - 7: length.
+ // https://developers.google.com/nearby/fast-pair/specifications/extensions/batterynotification
+ private static final byte SHOW_UI_INDICATION = 0b0011;
+ private static final byte HIDE_UI_INDICATION = 0b0100;
+ private static final int LENGTH_ADVERTISEMENT_TYPE_BIT = 4;
/** The types about how the bloomfilter is processed. */
public enum ProcessBloomFilterType {
@@ -91,7 +104,7 @@
if (FastPairDecoder.checkModelId(fastPairDevice.getData())) {
byte[] model = FastPairDecoder.getModelId(fastPairDevice.getData());
- Log.d(TAG, "On discovery model id " + Hex.bytesToStringLowercase(model));
+ Log.v(TAG, "On discovery model id " + Hex.bytesToStringLowercase(model));
// Use api to get anti spoofing key from model id.
try {
List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
@@ -111,7 +124,7 @@
}
Locator.get(mContext, FastPairHalfSheetManager.class).showHalfSheet(
DataUtils.toScanFastPairStoreItem(
- response, mBleAddress,
+ response, mBleAddress, Hex.bytesToStringLowercase(model),
accountList.isEmpty() ? null : accountList.get(0).name));
} catch (IllegalStateException e) {
Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
@@ -119,43 +132,176 @@
} else {
// Start to process bloom filter. Yet to finish.
try {
- List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
- byte[] bloomFilterByteArray = FastPairDecoder
- .getBloomFilter(fastPairDevice.getData());
- byte[] bloomFilterSalt = FastPairDecoder
- .getBloomFilterSalt(fastPairDevice.getData());
- if (bloomFilterByteArray == null || bloomFilterByteArray.length == 0) {
- return;
- }
+ subsequentPair(fastPairDevice);
} catch (IllegalStateException e) {
- Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
+ Log.e(TAG, "handleBroadcast: subsequent pair failed", e);
}
}
}
+ @Nullable
+ @VisibleForTesting
+ static byte[] getBloomFilterBytes(byte[] data) {
+ byte[] bloomFilterBytes = FastPairDecoder.getBloomFilter(data);
+ if (bloomFilterBytes == null) {
+ bloomFilterBytes = FastPairDecoder.getBloomFilterNoNotification(data);
+ }
+ if (ArrayUtils.isEmpty(bloomFilterBytes)) {
+ Log.d(TAG, "subsequentPair: bloomFilterByteArray empty");
+ return null;
+ }
+ return bloomFilterBytes;
+ }
+
+ private int getTxPower(FastPairDevice scannedDevice,
+ Data.FastPairDeviceWithAccountKey recognizedDevice) {
+ return recognizedDevice.getDiscoveryItem().getTxPower() == 0
+ ? scannedDevice.getTxPower()
+ : recognizedDevice.getDiscoveryItem().getTxPower();
+ }
+
+ private void subsequentPair(FastPairDevice scannedDevice) {
+ byte[] data = scannedDevice.getData();
+
+ if (ArrayUtils.isEmpty(data)) {
+ Log.d(TAG, "subsequentPair: no valid data");
+ return;
+ }
+
+ byte[] bloomFilterBytes = getBloomFilterBytes(data);
+ if (ArrayUtils.isEmpty(bloomFilterBytes)) {
+ Log.d(TAG, "subsequentPair: no valid bloom filter");
+ return;
+ }
+
+ byte[] salt = FastPairDecoder.getBloomFilterSalt(data);
+ if (ArrayUtils.isEmpty(salt)) {
+ Log.d(TAG, "subsequentPair: no valid salt");
+ return;
+ }
+ byte[] saltWithData = concat(salt, generateBatteryData(data));
+
+ List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
+ for (Account account : accountList) {
+ List<Data.FastPairDeviceWithAccountKey> devices =
+ mPairDataProvider.loadFastPairDeviceWithAccountKey(account);
+ Data.FastPairDeviceWithAccountKey recognizedDevice =
+ findRecognizedDevice(devices,
+ new BloomFilter(bloomFilterBytes,
+ new FastPairBloomFilterHasher()), saltWithData);
+ if (recognizedDevice == null) {
+ Log.v(TAG, "subsequentPair: recognizedDevice is null");
+ continue;
+ }
+
+ // Check the distance of the device if the distance is larger than the
+ // threshold
+ if (!isNearby(scannedDevice.getRssi(), getTxPower(scannedDevice, recognizedDevice))) {
+ Log.v(TAG,
+ "subsequentPair: the distance of the device is larger than the threshold");
+ return;
+ }
+
+ // Check if the device is already paired
+ List<Cache.StoredFastPairItem> storedFastPairItemList =
+ Locator.get(mContext, FastPairCacheManager.class)
+ .getAllSavedStoredFastPairItem();
+ Cache.StoredFastPairItem recognizedStoredFastPairItem =
+ findRecognizedDeviceFromCachedItem(storedFastPairItemList,
+ new BloomFilter(bloomFilterBytes,
+ new FastPairBloomFilterHasher()), saltWithData);
+ if (recognizedStoredFastPairItem != null) {
+ // The bloomfilter is recognized in the cache so the device is paired
+ // before
+ Log.d(TAG, "bloom filter is recognized in the cache");
+ continue;
+ }
+ showSubsequentNotification(account, scannedDevice, recognizedDevice);
+ }
+ }
+
+ private void showSubsequentNotification(Account account, FastPairDevice scannedDevice,
+ Data.FastPairDeviceWithAccountKey recognizedDevice) {
+ // Get full info from api the initial request will only return
+ // part of the info due to size limit.
+ List<Data.FastPairDeviceWithAccountKey> devicesWithAccountKeys =
+ mPairDataProvider.loadFastPairDeviceWithAccountKey(account,
+ List.of(recognizedDevice.getAccountKey().toByteArray()));
+ if (devicesWithAccountKeys == null || devicesWithAccountKeys.isEmpty()) {
+ Log.d(TAG, "No fast pair device with account key is found.");
+ return;
+ }
+
+ // Saved device from footprint does not have ble address.
+ // We need to fill ble address with current scan result.
+ Cache.StoredDiscoveryItem storedDiscoveryItem =
+ devicesWithAccountKeys.get(0).getDiscoveryItem().toBuilder()
+ .setMacAddress(
+ scannedDevice.getBluetoothAddress())
+ .build();
+ // Show notification
+ FastPairNotificationManager fastPairNotificationManager =
+ Locator.get(mContext, FastPairNotificationManager.class);
+ DiscoveryItem item = new DiscoveryItem(mContext, storedDiscoveryItem);
+ Locator.get(mContext, FastPairCacheManager.class).saveDiscoveryItem(item);
+ fastPairNotificationManager.showDiscoveryNotification(item,
+ devicesWithAccountKeys.get(0).getAccountKey().toByteArray());
+ }
+
+ // Battery advertisement format:
+ // Byte 0: Battery length and type, Bit 0 - 3: type, Bit 4 - 7: length.
+ // Byte 1 - 3: Battery values.
+ // Reference:
+ // https://developers.google.com/nearby/fast-pair/specifications/extensions/batterynotification
+ @VisibleForTesting
+ static byte[] generateBatteryData(byte[] data) {
+ byte[] batteryLevelNoNotification = FastPairDecoder.getBatteryLevelNoNotification(data);
+ boolean suppressBatteryNotification =
+ (batteryLevelNoNotification != null && batteryLevelNoNotification.length > 0);
+ byte[] batteryValues =
+ suppressBatteryNotification
+ ? batteryLevelNoNotification
+ : FastPairDecoder.getBatteryLevel(data);
+ if (ArrayUtils.isEmpty(batteryValues)) {
+ return new byte[0];
+ }
+ return generateBatteryData(suppressBatteryNotification, batteryValues);
+ }
+
+ @VisibleForTesting
+ static byte[] generateBatteryData(boolean suppressBatteryNotification, byte[] batteryValues) {
+ return concat(
+ new byte[] {
+ (byte)
+ (batteryValues.length << LENGTH_ADVERTISEMENT_TYPE_BIT
+ | (suppressBatteryNotification
+ ? HIDE_UI_INDICATION : SHOW_UI_INDICATION))
+ },
+ batteryValues);
+ }
+
/**
* Checks the bloom filter to see if any of the devices are recognized and should have a
* notification displayed for them. A device is recognized if the account key + salt combination
* is inside the bloom filter.
*/
@Nullable
+ @VisibleForTesting
static Data.FastPairDeviceWithAccountKey findRecognizedDevice(
List<Data.FastPairDeviceWithAccountKey> devices, BloomFilter bloomFilter, byte[] salt) {
- Log.d(TAG, "saved devices size in the account is " + devices.size());
for (Data.FastPairDeviceWithAccountKey device : devices) {
if (device.getAccountKey().toByteArray() == null || salt == null) {
return null;
}
byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt);
+
StringBuilder sb = new StringBuilder();
for (byte b : rotatedKey) {
sb.append(b);
}
+
if (bloomFilter.possiblyContains(rotatedKey)) {
- Log.d(TAG, "match " + sb.toString());
return device;
- } else {
- Log.d(TAG, "not match " + sb.toString());
}
}
return null;
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
index e1db7e5..447d199 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
@@ -129,11 +129,8 @@
Log.d(TAG, "Incorrect state, ignore pairing");
return;
}
- boolean useLargeNotifications =
- item.getAuthenticationPublicKeySecp256R1() != null;
FastPairNotificationManager fastPairNotificationManager =
- new FastPairNotificationManager(mContext, item,
- useLargeNotifications);
+ Locator.get(mContext, FastPairNotificationManager.class);
FastPairHalfSheetManager fastPairHalfSheetManager =
Locator.get(mContext, FastPairHalfSheetManager.class);
mFastPairCacheManager.saveDiscoveryItem(item);
@@ -169,7 +166,7 @@
@Nullable byte[] accountKey,
@Nullable String companionApp) {
FastPairNotificationManager fastPairNotificationManager =
- new FastPairNotificationManager(mContext, item, false);
+ Locator.get(mContext, FastPairNotificationManager.class);
FastPairHalfSheetManager fastPairHalfSheetManager =
Locator.get(mContext, FastPairHalfSheetManager.class);
PairingProgressHandlerBase pairingProgressHandlerBase =
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
index e3de4e2..8a9614e 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -16,8 +16,16 @@
package com.android.server.nearby.fastpair;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_FOREGROUND;
+import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
import static com.android.server.nearby.fastpair.Constant.TAG;
+import static com.google.common.io.BaseEncoding.base16;
+
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.app.KeyguardManager;
@@ -25,7 +33,6 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -35,8 +42,6 @@
import android.nearby.NearbyManager;
import android.nearby.ScanCallback;
import android.nearby.ScanRequest;
-import android.net.Uri;
-import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -89,7 +94,6 @@
/** A notification ID which should be dismissed */
public static final String EXTRA_NOTIFICATION_ID = ACTION_PREFIX + "EXTRA_NOTIFICATION_ID";
- public static final String ACTION_RESOURCES_APK = "android.nearby.SHOW_HALFSHEET";
private static Executor sFastPairExecutor;
@@ -98,17 +102,74 @@
final LocatorContextWrapper mLocatorContextWrapper;
final IntentFilter mIntentFilter;
final Locator mLocator;
- private boolean mScanEnabled;
+ private boolean mScanEnabled = false;
+ private final FastPairCacheManager mFastPairCacheManager;
private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)
- || intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
- Log.d(TAG, "onReceive: ACTION_SCREEN_ON or boot complete.");
- invalidateScan();
- } else if (intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
- processBluetoothConnectionEvent(intent);
+ String action = intent.getAction();
+ switch (action) {
+ case Intent.ACTION_SCREEN_ON:
+ Log.d(TAG, "onReceive: ACTION_SCREEN_ON");
+ invalidateScan();
+ break;
+ case Intent.ACTION_BOOT_COMPLETED:
+ Log.d(TAG, "onReceive: ACTION_BOOT_COMPLETED.");
+ invalidateScan();
+ break;
+ case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
+ Log.d(TAG, "onReceive: ACTION_BOND_STATE_CHANGED");
+ processBluetoothConnectionEvent(intent);
+ break;
+ case ACTION_HALF_SHEET_FOREGROUND_STATE:
+ boolean state = intent.getBooleanExtra(EXTRA_HALF_SHEET_FOREGROUND, false);
+ Log.d(TAG, "halfsheet report foreground state: " + state);
+ Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
+ .setHalfSheetForeground(state);
+ break;
+ case ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET:
+ Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET");
+ String deviceModelId = intent.getStringExtra(EXTRA_MODEL_ID);
+ if (deviceModelId == null) {
+ Log.d(TAG, "HalfSheetManager reset device ban state skipped, "
+ + "deviceModelId not found");
+ break;
+ }
+ Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
+ .resetBanState(deviceModelId);
+ break;
+ case ACTION_FAST_PAIR_HALF_SHEET_CANCEL:
+ Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_CANCEL");
+ String modelId = intent.getStringExtra(EXTRA_MODEL_ID);
+ if (modelId == null) {
+ Log.d(TAG, "skip half sheet cancel action, model id not found");
+ break;
+ }
+ Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
+ .dismiss(modelId);
+ break;
+ case ACTION_FAST_PAIR:
+ Log.d(TAG, "onReceive: ACTION_FAST_PAIR");
+ String itemId = intent.getStringExtra(UserActionHandler.EXTRA_ITEM_ID);
+ String accountKeyString = intent
+ .getStringExtra(UserActionHandler.EXTRA_FAST_PAIR_SECRET);
+ if (itemId == null || accountKeyString == null) {
+ Log.d(TAG, "skip pair action, item id "
+ + "or fast pair account key not found");
+ break;
+ }
+ try {
+ FastPairController controller =
+ Locator.getFromContextWrapper(mLocatorContextWrapper,
+ FastPairController.class);
+ if (mFastPairCacheManager != null) {
+ controller.pair(mFastPairCacheManager.getDiscoveryItem(itemId),
+ base16().decode(accountKeyString), /* companionApp= */ null);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Cannot find FastPairController class", e);
+ }
}
}
};
@@ -120,6 +181,8 @@
mLocator.bind(new FastPairModule());
Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
Rpcs.GetObservedDeviceResponse.newBuilder().build();
+ mFastPairCacheManager =
+ Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
}
final ScanCallback mScanCallback = new ScanCallback() {
@@ -141,6 +204,11 @@
byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
Log.d(TAG, "lost model id" + Hex.bytesToStringLowercase(modelArray));
}
+
+ @Override
+ public void onError(int errorCode) {
+ Log.w(TAG, "[FastPairManager] Scan error is " + errorCode);
+ }
};
/**
@@ -151,14 +219,13 @@
mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
mIntentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
+ mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_CANCEL);
+ mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET);
+ mIntentFilter.addAction(ACTION_HALF_SHEET_FOREGROUND_STATE);
+ mIntentFilter.addAction(ACTION_FAST_PAIR);
- mLocatorContextWrapper.getContext()
- .registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
-
- Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
- // Default false for now.
- mScanEnabled = NearbyManager.getFastPairScanEnabled(mLocatorContextWrapper.getContext());
- registerFastPairScanChangeContentObserver(mLocatorContextWrapper.getContentResolver());
+ mLocatorContextWrapper.getContext().registerReceiver(mScreenBroadcastReceiver,
+ mIntentFilter, Context.RECEIVER_EXPORTED);
}
/**
@@ -259,6 +326,13 @@
context, item.getMacAddress(),
prefsBuilder.build(),
null);
+ connection.setOnPairedCallback(
+ address -> {
+ Log.v(TAG, "connection on paired callback;");
+ // TODO(b/259150992) add fill Bluetooth metadata values logic
+ pairingProgressHandlerBase.onPairedCallbackCalled(
+ connection, accountKey, footprints, address);
+ });
pairingProgressHandlerBase.onPairingSetupCompleted();
FastPairConnection.SharedSecret sharedSecret;
@@ -353,25 +427,6 @@
}
}
- private void registerFastPairScanChangeContentObserver(ContentResolver resolver) {
- mFastPairScanChangeContentObserver = new ContentObserver(ForegroundThread.getHandler()) {
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- super.onChange(selfChange, uri);
- setScanEnabled(
- NearbyManager.getFastPairScanEnabled(mLocatorContextWrapper.getContext()));
- }
- };
- try {
- resolver.registerContentObserver(
- Settings.Secure.getUriFor(NearbyManager.FAST_PAIR_SCAN_ENABLED),
- /* notifyForDescendants= */ false,
- mFastPairScanChangeContentObserver);
- } catch (SecurityException e) {
- Log.e(TAG, "Failed to register content observer for fast pair scan.", e);
- }
- }
-
/**
* Processed task in a background thread
*/
@@ -399,13 +454,6 @@
return (NearbyManager) mLocatorContextWrapper
.getApplicationContext().getSystemService(Context.NEARBY_SERVICE);
}
- private void setScanEnabled(boolean scanEnabled) {
- if (mScanEnabled == scanEnabled) {
- return;
- }
- mScanEnabled = scanEnabled;
- invalidateScan();
- }
/**
* Starts or stops scanning according to mAllowScan value.
@@ -414,7 +462,7 @@
NearbyManager nearbyManager = getNearbyManager();
if (nearbyManager == null) {
Log.w(TAG, "invalidateScan: "
- + "failed to start or stop scannning because NearbyManager is null.");
+ + "failed to start or stop scanning because NearbyManager is null.");
return;
}
if (mScanEnabled) {
@@ -444,8 +492,7 @@
processBackgroundTask(new Runnable() {
@Override
public void run() {
- mLocatorContextWrapper.getLocator().get(FastPairCacheManager.class)
- .removeStoredFastPairItem(device.getAddress());
+ mFastPairCacheManager.removeStoredFastPairItem(device.getAddress());
}
});
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
index d7946d1..1df4723 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
@@ -24,6 +24,7 @@
import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
import java.time.Clock;
import java.time.Instant;
@@ -52,6 +53,9 @@
locator.bind(FastPairHalfSheetManager.class, new FastPairHalfSheetManager(context));
} else if (type.equals(FastPairAdvHandler.class)) {
locator.bind(FastPairAdvHandler.class, new FastPairAdvHandler(context));
+ } else if (type.equals(FastPairNotificationManager.class)) {
+ locator.bind(FastPairNotificationManager.class,
+ new FastPairNotificationManager(context));
} else if (type.equals(Clock.class)) {
locator.bind(Clock.class, new Clock() {
@Override
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/HalfSheetResources.java b/nearby/service/java/com/android/server/nearby/fastpair/HalfSheetResources.java
new file mode 100644
index 0000000..86dd44d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/HalfSheetResources.java
@@ -0,0 +1,129 @@
+/*
+ * 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.fastpair;
+
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorRes;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Utility to obtain the {@link com.android.nearby.halfsheet} {@link Resources}, in the
+ * HalfSheetUX APK.
+ * @hide
+ */
+public class HalfSheetResources {
+ @NonNull
+ private final Context mContext;
+
+ @Nullable
+ private Context mResourcesContext = null;
+
+ @Nullable
+ private static Context sTestResourcesContext = null;
+
+ public HalfSheetResources(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Convenience method to mock all resources for the duration of a test.
+ *
+ * Call with a null context to reset after the test.
+ */
+ @VisibleForTesting
+ public static void setResourcesContextForTest(@Nullable Context testContext) {
+ sTestResourcesContext = testContext;
+ }
+
+ /**
+ * Get the {@link Context} of the resources package.
+ */
+ @Nullable
+ public synchronized Context getResourcesContext() {
+ if (sTestResourcesContext != null) {
+ return sTestResourcesContext;
+ }
+
+ if (mResourcesContext != null) {
+ return mResourcesContext;
+ }
+
+ String packageName = PackageUtils.getHalfSheetApkPkgName(mContext);
+ if (packageName == null) {
+ Log.e(TAG, "Resolved package not found");
+ return null;
+ }
+ final Context pkgContext;
+ try {
+ pkgContext = mContext.createPackageContext(packageName, 0 /* flags */);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Resolved package not found");
+ return null;
+ }
+
+ mResourcesContext = pkgContext;
+ return pkgContext;
+ }
+
+ /**
+ * Get the {@link Resources} of the ServiceConnectivityResources APK.
+ */
+ public Resources get() {
+ return getResourcesContext().getResources();
+ }
+
+ /**
+ * Gets the {@code String} with given resource Id.
+ */
+ public String getString(@StringRes int id) {
+ return get().getString(id);
+ }
+
+ /**
+ * Gets the {@code String} with given resource Id and formatted arguments.
+ */
+ public String getString(@StringRes int id, Object... formatArgs) {
+ return get().getString(id, formatArgs);
+ }
+
+ /**
+ * Gets the {@link Drawable} with given resource Id.
+ */
+ public Drawable getDrawable(@DrawableRes int id) {
+ return get().getDrawable(id, getResourcesContext().getTheme());
+ }
+
+ /**
+ * Gets a themed color integer associated with a particular resource ID.
+ */
+ @ColorInt
+ public int getColor(@ColorRes int id) {
+ return get().getColor(id, getResourcesContext().getTheme());
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/PackageUtils.java b/nearby/service/java/com/android/server/nearby/fastpair/PackageUtils.java
new file mode 100644
index 0000000..0ff8caf
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/PackageUtils.java
@@ -0,0 +1,72 @@
+/*
+ * 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.fastpair;
+
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_RESOURCES_APK;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.util.Log;
+
+import com.android.server.nearby.util.Environment;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class for package related methods.
+ */
+public class PackageUtils {
+
+ /**
+ * Gets the package name of HalfSheet.apk
+ */
+ @Nullable
+ public static String getHalfSheetApkPkgName(Context context) {
+ List<ResolveInfo> resolveInfos = context
+ .getPackageManager().queryIntentActivities(
+ new Intent(ACTION_RESOURCES_APK),
+ PackageManager.MATCH_SYSTEM_ONLY);
+
+ // remove apps that don't live in the nearby apex
+ resolveInfos.removeIf(info ->
+ !Environment.isAppInNearbyApex(info.activityInfo.applicationInfo));
+
+ if (resolveInfos.isEmpty()) {
+ // Resource APK not loaded yet, print a stack trace to see where this is called from
+ Log.e("FastPairManager", "Attempted to fetch resources before halfsheet "
+ + " APK is installed or package manager can't resolve correctly!",
+ new IllegalStateException());
+ return null;
+ }
+
+ if (resolveInfos.size() > 1) {
+ // multiple apps found, log a warning, but continue
+ Log.w("FastPairManager", "Found > 1 APK that can resolve halfsheet APK intent: "
+ + resolveInfos.stream()
+ .map(info -> info.activityInfo.applicationInfo.packageName)
+ .collect(Collectors.joining(", ")));
+ }
+
+ // Assume the first ResolveInfo is the one we're looking for
+ ResolveInfo info = resolveInfos.get(0);
+ return info.activityInfo.applicationInfo.packageName;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
index 674633d..2b00ca5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/UserActionHandler.java
@@ -16,16 +16,16 @@
package com.android.server.nearby.fastpair;
-import com.android.server.nearby.common.fastpair.service.UserActionHandlerBase;
+import com.android.nearby.halfsheet.constants.UserActionHandlerBase;
/**
* User action handler class.
*/
public class UserActionHandler extends UserActionHandlerBase {
- public static final String EXTRA_DISCOVERY_ITEM = PREFIX + "EXTRA_DISCOVERY_ITEM";
+ public static final String EXTRA_ITEM_ID = PREFIX + "EXTRA_DISCOVERY_ITEM";
public static final String EXTRA_FAST_PAIR_SECRET = PREFIX + "EXTRA_FAST_PAIR_SECRET";
- public static final String ACTION_FAST_PAIR = ACTION_PREFIX + "ACTION_FAST_PAIR";
+
public static final String EXTRA_PRIVATE_BLE_ADDRESS =
ACTION_PREFIX + "EXTRA_PRIVATE_BLE_ADDRESS";
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/blocklist/Blocklist.java b/nearby/service/java/com/android/server/nearby/fastpair/blocklist/Blocklist.java
new file mode 100644
index 0000000..d8091ba
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/blocklist/Blocklist.java
@@ -0,0 +1,66 @@
+/*
+ * 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.fastpair.blocklist;
+
+
+/**
+ * Skeletal implementation of Blocklist
+ *
+ * <p>Controls the frequency to show the available device to users.
+ */
+public interface Blocklist {
+
+ /** Checks certain item is blocked within durationSeconds. */
+ boolean isBlocklisted(int id, int durationSeconds);
+
+ /** Updates the HalfSheet blocklist state for a given id. */
+ boolean updateState(int id, BlocklistState state);
+
+ /** Removes the HalfSheet blocklist. */
+ boolean removeBlocklist(int id);
+
+ /** Resets certain device ban state to active. */
+ void resetBlockState(int id);
+
+ /**
+ * Used for indicate what state is the blocklist item.
+ *
+ * <p>The different states have differing priorities and higher priority states will override
+ * lower one.
+ * More details and state transition diagram,
+ * see: https://docs.google.com/document/d/1wzE5CHXTkzKJY-2AltSrxOVteom2Nebc1sbjw1Tt7BQ/edit?usp=sharing&resourcekey=0-L-wUz3Hw5gZPThm5VPwHOQ
+ */
+ enum BlocklistState {
+ UNKNOWN(0),
+ ACTIVE(1),
+ DISMISSED(2),
+ PAIRING(3),
+ PAIRED(4),
+ DO_NOT_SHOW_AGAIN(5),
+ DO_NOT_SHOW_AGAIN_LONG(6);
+
+ private final int mValue;
+
+ BlocklistState(final int value) {
+ this.mValue = value;
+ }
+
+ public boolean hasHigherPriorityThan(BlocklistState otherState) {
+ return this.mValue > otherState.mValue;
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/blocklist/BlocklistElement.java b/nearby/service/java/com/android/server/nearby/fastpair/blocklist/BlocklistElement.java
new file mode 100644
index 0000000..d058d58
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/blocklist/BlocklistElement.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.fastpair.blocklist;
+
+import com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetBlocklist;
+
+/** Element in the {@link FastPairHalfSheetBlocklist} */
+public class BlocklistElement {
+ private final long mTimeStamp;
+ private final BlocklistState mState;
+
+ public BlocklistElement(BlocklistState state, long timeStamp) {
+ this.mState = state;
+ this.mTimeStamp = timeStamp;
+ }
+
+ public Long getTimeStamp() {
+ return mTimeStamp;
+ }
+
+ public BlocklistState getState() {
+ return mState;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklist.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklist.java
new file mode 100644
index 0000000..146b97a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklist.java
@@ -0,0 +1,193 @@
+/*
+ * 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.fastpair.halfsheet;
+
+
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.ACTIVE;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DISMISSED;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG;
+
+import android.util.Log;
+import android.util.LruCache;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.server.nearby.fastpair.blocklist.Blocklist;
+import com.android.server.nearby.fastpair.blocklist.BlocklistElement;
+import com.android.server.nearby.util.Clock;
+import com.android.server.nearby.util.DefaultClock;
+
+
+/**
+ * Maintains a list of half sheet id to tell whether the half sheet should be suppressed or not.
+ *
+ * <p>When user cancel half sheet, the ble address related half sheet should be in block list and
+ * after certain duration of time half sheet can show again.
+ */
+public class FastPairHalfSheetBlocklist extends LruCache<Integer, BlocklistElement>
+ implements Blocklist {
+ private static final String TAG = "HalfSheetBlocklist";
+ // Number of entries in the FastPair blocklist
+ private static final int FAST_PAIR_BLOCKLIST_CACHE_SIZE = 16;
+ // Duration between first half sheet dismiss and second half sheet shows: 2 seconds
+ private static final int FAST_PAIR_HALF_SHEET_DISMISS_COOL_DOWN_MILLIS = 2000;
+ // The timeout to ban half sheet after user trigger the ban logic even number of time : 1 day
+ private static final int DURATION_RESURFACE_HALFSHEET_EVEN_NUMBER_BAN_MILLI_SECONDS = 86400000;
+ // Timeout for DISMISSED entries in the blocklist to expire : 1 min
+ private static final int FAST_PAIR_BLOCKLIST_DISMISSED_HALF_SHEET_TIMEOUT_MILLIS = 60000;
+ // The timeout for entries in the blocklist to expire : 1 day
+ private static final int STATE_EXPIRATION_MILLI_SECONDS = 86400000;
+ private long mEndTimeBanAllItems;
+ private final Clock mClock;
+
+
+ public FastPairHalfSheetBlocklist() {
+ // Reuses the size limit from notification cache.
+ // Number of entries in the FastPair blocklist
+ super(FAST_PAIR_BLOCKLIST_CACHE_SIZE);
+ mClock = new DefaultClock();
+ }
+
+ @VisibleForTesting
+ FastPairHalfSheetBlocklist(int size, Clock clock) {
+ super(size);
+ mClock = clock;
+ }
+
+ /**
+ * Checks whether need to show HalfSheet or not.
+ *
+ * <p> When the HalfSheet {@link BlocklistState} is DISMISS, there is a little cool down period
+ * to allow half sheet to reshow.
+ * If the HalfSheet {@link BlocklistState} is DO_NOT_SHOW_AGAIN, within durationMilliSeconds
+ * from banned start time, the function will return true
+ * otherwise it will return false if the status is expired
+ * If the HalfSheet {@link BlocklistState} is DO_NOT_SHOW_AGAIN_LONG, the half sheet will be
+ * baned for a longer duration.
+ *
+ * @param id {@link com.android.nearby.halfsheet.HalfSheetActivity} id
+ * @param durationMilliSeconds the time duration from item is banned to now
+ * @return whether the HalfSheet is blocked to show
+ */
+ @Override
+ public boolean isBlocklisted(int id, int durationMilliSeconds) {
+ if (shouldBanAllItem()) {
+ return true;
+ }
+ BlocklistElement entry = get(id);
+ if (entry == null) {
+ return false;
+ }
+ if (entry.getState().equals(DO_NOT_SHOW_AGAIN)) {
+ Log.d(TAG, "BlocklistState: DO_NOT_SHOW_AGAIN");
+ return mClock.elapsedRealtime() < entry.getTimeStamp() + durationMilliSeconds;
+ }
+ if (entry.getState().equals(DO_NOT_SHOW_AGAIN_LONG)) {
+ Log.d(TAG, "BlocklistState: DO_NOT_SHOW_AGAIN_LONG ");
+ return mClock.elapsedRealtime()
+ < entry.getTimeStamp()
+ + DURATION_RESURFACE_HALFSHEET_EVEN_NUMBER_BAN_MILLI_SECONDS;
+ }
+
+ if (entry.getState().equals(ACTIVE)) {
+ Log.d(TAG, "BlocklistState: ACTIVE");
+ return false;
+ }
+ // Get some cool down period for dismiss state
+ if (entry.getState().equals(DISMISSED)) {
+ Log.d(TAG, "BlocklistState: DISMISSED");
+ return mClock.elapsedRealtime()
+ < entry.getTimeStamp() + FAST_PAIR_HALF_SHEET_DISMISS_COOL_DOWN_MILLIS;
+ }
+ if (dismissStateHasExpired(entry)) {
+ Log.d(TAG, "stateHasExpired: True");
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean removeBlocklist(int id) {
+ BlocklistElement oldValue = remove(id);
+ return oldValue != null;
+ }
+
+ /**
+ * Updates the HalfSheet blocklist state
+ *
+ * <p>When the new {@link BlocklistState} has higher priority then old {@link BlocklistState} or
+ * the old {@link BlocklistState} status is expired,the function will update the status.
+ *
+ * @param id HalfSheet id
+ * @param state Blocklist state
+ * @return update status successful or not
+ */
+ @Override
+ public boolean updateState(int id, BlocklistState state) {
+ BlocklistElement entry = get(id);
+ if (entry == null || state.hasHigherPriorityThan(entry.getState())
+ || dismissStateHasExpired(entry)) {
+ Log.d(TAG, "updateState: " + state);
+ put(id, new BlocklistElement(state, mClock.elapsedRealtime()));
+ return true;
+ }
+ return false;
+ }
+
+ /** Enables lower state to override the higher value state. */
+ public void forceUpdateState(int id, BlocklistState state) {
+ put(id, new BlocklistElement(state, mClock.elapsedRealtime()));
+ }
+
+ /** Resets certain device ban state to active. */
+ @Override
+ public void resetBlockState(int id) {
+ BlocklistElement entry = get(id);
+ if (entry != null) {
+ put(id, new BlocklistElement(ACTIVE, mClock.elapsedRealtime()));
+ }
+ }
+
+ /** Checks whether certain device state has expired. */
+ public boolean isStateExpired(int id) {
+ BlocklistElement entry = get(id);
+ if (entry != null) {
+ return mClock.elapsedRealtime() > entry.getTimeStamp() + STATE_EXPIRATION_MILLI_SECONDS;
+ }
+ return false;
+ }
+
+ private boolean dismissStateHasExpired(BlocklistElement entry) {
+ return mClock.elapsedRealtime()
+ > entry.getTimeStamp() + FAST_PAIR_BLOCKLIST_DISMISSED_HALF_SHEET_TIMEOUT_MILLIS;
+ }
+
+ /**
+ * Updates the end time that all half sheet will be banned.
+ */
+ void banAllItem(long banDurationTimeMillis) {
+ long endTime = mClock.elapsedRealtime() + banDurationTimeMillis;
+ if (endTime > mEndTimeBanAllItems) {
+ mEndTimeBanAllItems = endTime;
+ }
+ }
+
+ private boolean shouldBanAllItem() {
+ return mClock.elapsedRealtime() < mEndTimeBanAllItems;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
index 553d5ce..7b266a7 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManager.java
@@ -16,34 +16,51 @@
package com.android.server.nearby.fastpair.halfsheet;
-import static com.android.server.nearby.fastpair.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_BINDER;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_BUNDLE;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_INFO;
-import static com.android.server.nearby.fastpair.Constant.EXTRA_HALF_SHEET_TYPE;
-import static com.android.server.nearby.fastpair.FastPairManager.ACTION_RESOURCES_APK;
+import static com.android.nearby.halfsheet.constants.Constant.DEVICE_PAIRING_FRAGMENT_TYPE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BINDER;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_BUNDLE;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_CONTENT;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_INFO;
+import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_TYPE;
+import static com.android.nearby.halfsheet.constants.Constant.FAST_PAIR_HALF_SHEET_HELP_URL;
+import static com.android.nearby.halfsheet.constants.Constant.RESULT_FAIL;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.annotation.UiThread;
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.nearby.FastPairDevice;
import android.nearby.FastPairStatusCallback;
import android.nearby.PairStatusMetadata;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.Log;
+import android.util.LruCache;
+import android.widget.Toast;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.nearby.halfsheet.R;
+import com.android.server.nearby.common.eventloop.Annotations;
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.common.eventloop.NamedRunnable;
+import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.FastPairController;
+import com.android.server.nearby.fastpair.PackageUtils;
+import com.android.server.nearby.fastpair.blocklist.Blocklist;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.util.Environment;
+import java.util.HashMap;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
import service.proto.Cache;
@@ -55,20 +72,56 @@
private static final String HALF_SHEET_CLASS_NAME =
"com.android.nearby.halfsheet.HalfSheetActivity";
private static final String TAG = "FPHalfSheetManager";
+ public static final String FINISHED_STATE = "FINISHED_STATE";
+ @VisibleForTesting static final String DISMISS_HALFSHEET_RUNNABLE_NAME = "DismissHalfSheet";
+ @VisibleForTesting static final String SHOW_TOAST_RUNNABLE_NAME = "SuccessPairingToast";
+
+ // The timeout to ban half sheet after user trigger the ban logic odd number of time: 5 mins
+ private static final int DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS = 300000;
+ // Number of seconds half sheet will show after the advertisement is no longer seen.
+ private static final int HALF_SHEET_TIME_OUT_SECONDS = 12;
+
+ static final int HALFSHEET_ID_SEED = "new_fast_pair_half_sheet".hashCode();
private String mHalfSheetApkPkgName;
+ private boolean mIsHalfSheetForeground = false;
+ private boolean mIsActivePairing = false;
+ private Cache.ScanFastPairStoreItem mCurrentScanFastPairStoreItem = null;
private final LocatorContextWrapper mLocatorContextWrapper;
+ private final AtomicInteger mNotificationIds = new AtomicInteger(HALFSHEET_ID_SEED);
+ private FastPairHalfSheetBlocklist mHalfSheetBlocklist;
+ // Todo: Make "16" a flag, which can be updated from the server side.
+ final LruCache<String, Integer> mModelIdMap = new LruCache<>(16);
+ HalfSheetDismissState mHalfSheetDismissState = HalfSheetDismissState.ACTIVE;
+ // Ban count map track the number of ban happens to certain model id
+ // If the model id is baned by the odd number of time it is banned for 5 mins
+ // if the model id is banned even number of time ban 24 hours.
+ private final Map<Integer, Integer> mBanCountMap = new HashMap<>();
FastPairUiServiceImpl mFastPairUiService;
+ private NamedRunnable mDismissRunnable;
+
+ /**
+ * Half sheet state default is active. If user dismiss half sheet once controller will mark half
+ * sheet as dismiss state. If user dismiss half sheet twice controller will mark half sheet as
+ * ban state for certain period of time.
+ */
+ enum HalfSheetDismissState {
+ ACTIVE,
+ DISMISS,
+ BAN
+ }
public FastPairHalfSheetManager(Context context) {
this(new LocatorContextWrapper(context));
+ mHalfSheetBlocklist = new FastPairHalfSheetBlocklist();
}
@VisibleForTesting
- FastPairHalfSheetManager(LocatorContextWrapper locatorContextWrapper) {
+ public FastPairHalfSheetManager(LocatorContextWrapper locatorContextWrapper) {
mLocatorContextWrapper = locatorContextWrapper;
mFastPairUiService = new FastPairUiServiceImpl();
+ mHalfSheetBlocklist = new FastPairHalfSheetBlocklist();
}
/**
@@ -76,6 +129,40 @@
* app can't get the correct component name.
*/
public void showHalfSheet(Cache.ScanFastPairStoreItem scanFastPairStoreItem) {
+ String modelId = scanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT);
+ if (modelId == null) {
+ Log.d(TAG, "model id not found");
+ return;
+ }
+
+ synchronized (mModelIdMap) {
+ if (mModelIdMap.get(modelId) == null) {
+ mModelIdMap.put(modelId, createNewHalfSheetId());
+ }
+ }
+ int halfSheetId = mModelIdMap.get(modelId);
+
+ if (!allowedToShowHalfSheet(halfSheetId)) {
+ Log.d(TAG, "Not allow to show initial Half sheet");
+ return;
+ }
+
+ // If currently half sheet UI is in the foreground,
+ // DO NOT request start-activity to avoid unnecessary memory usage
+ if (mIsHalfSheetForeground) {
+ updateForegroundHalfSheet(scanFastPairStoreItem);
+ return;
+ } else {
+ // If the half sheet is not in foreground but the system is still pairing
+ // with the same device, mark as duplicate request and skip.
+ if (mCurrentScanFastPairStoreItem != null && mIsActivePairing
+ && mCurrentScanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT)
+ .equals(scanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT))) {
+ Log.d(TAG, "Same device is pairing.");
+ return;
+ }
+ }
+
try {
if (mLocatorContextWrapper != null) {
String packageName = getHalfSheetApkPkgName();
@@ -97,32 +184,78 @@
.setComponent(new ComponentName(packageName,
HALF_SHEET_CLASS_NAME)),
UserHandle.CURRENT);
+ mHalfSheetBlocklist.updateState(halfSheetId, Blocklist.BlocklistState.ACTIVE);
}
} catch (IllegalStateException e) {
Log.e(TAG, "Can't resolve package that contains half sheet");
}
+ Log.d(TAG, "show initial half sheet.");
+ mCurrentScanFastPairStoreItem = scanFastPairStoreItem;
+ mIsHalfSheetForeground = true;
+ enableAutoDismiss(scanFastPairStoreItem.getAddress(), HALF_SHEET_TIME_OUT_SECONDS);
}
/**
- * Shows pairing fail half sheet.
+ * Auto dismiss half sheet after timeout
*/
- public void showPairingFailed() {
- FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
- if (pairStatusCallback != null) {
- Log.v(TAG, "showPairingFailed: pairStatusCallback not NULL");
- pairStatusCallback.onPairUpdate(new FastPairDevice.Builder().build(),
- new PairStatusMetadata(PairStatusMetadata.Status.FAIL));
- } else {
- Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
- + "the pairStatusCallback is null");
+ @VisibleForTesting
+ void enableAutoDismiss(String address, long timeoutDuration) {
+ if (mDismissRunnable == null
+ || !mDismissRunnable.name.equals(DISMISS_HALFSHEET_RUNNABLE_NAME)) {
+ mDismissRunnable =
+ new NamedRunnable(DISMISS_HALFSHEET_RUNNABLE_NAME) {
+ @Override
+ public void run() {
+ Log.d(TAG, "Dismiss the half sheet after "
+ + timeoutDuration + " seconds");
+ // BMW car kit will advertise even after pairing start,
+ // to avoid the half sheet be dismissed during active pairing,
+ // If the half sheet is in the pairing state, disable the auto dismiss.
+ // See b/182396106
+ if (mIsActivePairing) {
+ return;
+ }
+ mIsHalfSheetForeground = false;
+ FastPairStatusCallback pairStatusCallback =
+ mFastPairUiService.getPairStatusCallback();
+ if (pairStatusCallback != null) {
+ pairStatusCallback.onPairUpdate(new FastPairDevice.Builder()
+ .setBluetoothAddress(address).build(),
+ new PairStatusMetadata(PairStatusMetadata.Status.DISMISS));
+ } else {
+ Log.w(TAG, "pairStatusCallback is null,"
+ + " failed to enable auto dismiss ");
+ }
+ }
+ };
}
+ if (Locator.get(mLocatorContextWrapper, EventLoop.class).isPosted(mDismissRunnable)) {
+ disableDismissRunnable();
+ }
+ Locator.get(mLocatorContextWrapper, EventLoop.class)
+ .postRunnableDelayed(mDismissRunnable, SECONDS.toMillis(timeoutDuration));
}
- /**
- * Get the half sheet status whether it is foreground or dismissed
- */
- public boolean getHalfSheetForegroundState() {
- return true;
+ private void updateForegroundHalfSheet(Cache.ScanFastPairStoreItem scanFastPairStoreItem) {
+ if (mCurrentScanFastPairStoreItem == null) {
+ return;
+ }
+ if (mCurrentScanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT)
+ .equals(scanFastPairStoreItem.getAddress().toLowerCase(Locale.ROOT))) {
+ // If current address is the same, reset the timeout.
+ Log.d(TAG, "same Address device, reset the auto dismiss timeout");
+ enableAutoDismiss(scanFastPairStoreItem.getAddress(), HALF_SHEET_TIME_OUT_SECONDS);
+ } else {
+ // If current address is different, not reset timeout
+ // wait for half sheet auto dismiss or manually dismiss to start new pair.
+ if (mCurrentScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT)
+ .equals(scanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT))) {
+ Log.d(TAG, "same model id device is also nearby");
+ }
+ Log.d(TAG, "showInitialHalfsheet: address changed, from "
+ + mCurrentScanFastPairStoreItem.getAddress()
+ + " to " + scanFastPairStoreItem.getAddress());
+ }
}
/**
@@ -140,23 +273,187 @@
/**
* Shows pairing success info.
+ * If the half sheet is not shown, show toast to remind user.
*/
public void showPairingSuccessHalfSheet(String address) {
- FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
- if (pairStatusCallback != null) {
+ resetPairingStateDisableAutoDismiss();
+ if (mIsHalfSheetForeground) {
+ FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
+ if (pairStatusCallback == null) {
+ Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
+ + "the pairStatusCallback is null");
+ return;
+ }
+ Log.d(TAG, "showPairingSuccess: pairStatusCallback not NULL");
pairStatusCallback.onPairUpdate(
new FastPairDevice.Builder().setBluetoothAddress(address).build(),
new PairStatusMetadata(PairStatusMetadata.Status.SUCCESS));
} else {
- Log.w(TAG, "FastPairHalfSheetManager failed to show success half sheet because "
- + "the pairStatusCallback is null");
+ Locator.get(mLocatorContextWrapper, EventLoop.class)
+ .postRunnable(
+ new NamedRunnable(SHOW_TOAST_RUNNABLE_NAME) {
+ @Override
+ public void run() {
+ try {
+ Toast.makeText(mLocatorContextWrapper,
+ mLocatorContextWrapper
+ .getPackageManager()
+ .getResourcesForApplication(
+ getHalfSheetApkPkgName())
+ .getString(R.string.fast_pair_device_ready),
+ Toast.LENGTH_LONG).show();
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "showPairingSuccess fail:"
+ + " package name cannot be found ");
+ e.printStackTrace();
+ }
+ }
+ });
}
}
/**
- * Removes dismiss runnable.
+ * Shows pairing fail half sheet.
+ * If the half sheet is not shown, create a new half sheet to help user go to Setting
+ * to manually pair with the device.
+ */
+ public void showPairingFailed() {
+ resetPairingStateDisableAutoDismiss();
+ if (mCurrentScanFastPairStoreItem == null) {
+ return;
+ }
+ if (mIsHalfSheetForeground) {
+ FastPairStatusCallback pairStatusCallback = mFastPairUiService.getPairStatusCallback();
+ if (pairStatusCallback != null) {
+ Log.v(TAG, "showPairingFailed: pairStatusCallback not NULL");
+ pairStatusCallback.onPairUpdate(
+ new FastPairDevice.Builder()
+ .setBluetoothAddress(mCurrentScanFastPairStoreItem.getAddress())
+ .build(),
+ new PairStatusMetadata(PairStatusMetadata.Status.FAIL));
+ } else {
+ Log.w(TAG, "FastPairHalfSheetManager failed to show fail half sheet because "
+ + "the pairStatusCallback is null");
+ }
+ } else {
+ String packageName = getHalfSheetApkPkgName();
+ if (packageName == null) {
+ Log.e(TAG, "package name is null");
+ return;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putBinder(EXTRA_BINDER, mFastPairUiService);
+ mLocatorContextWrapper
+ .startActivityAsUser(new Intent(ACTIVITY_INTENT_ACTION)
+ .putExtra(EXTRA_HALF_SHEET_INFO,
+ mCurrentScanFastPairStoreItem.toByteArray())
+ .putExtra(EXTRA_HALF_SHEET_TYPE,
+ DEVICE_PAIRING_FRAGMENT_TYPE)
+ .putExtra(EXTRA_HALF_SHEET_CONTENT, RESULT_FAIL)
+ .putExtra(EXTRA_BUNDLE, bundle)
+ .setComponent(new ComponentName(packageName,
+ HALF_SHEET_CLASS_NAME)),
+ UserHandle.CURRENT);
+ Log.d(TAG, "Starts a new half sheet to showPairingFailed");
+ String modelId = mCurrentScanFastPairStoreItem.getModelId().toLowerCase(Locale.ROOT);
+ if (modelId == null || mModelIdMap.get(modelId) == null) {
+ Log.d(TAG, "info not enough");
+ return;
+ }
+ int halfSheetId = mModelIdMap.get(modelId);
+ mHalfSheetBlocklist.updateState(halfSheetId, Blocklist.BlocklistState.ACTIVE);
+ }
+ }
+
+ /**
+ * Removes dismiss half sheet runnable. When half sheet shows, there is timer for half sheet to
+ * dismiss. But when user is pairing, half sheet should not dismiss.
+ * So this function disable the runnable.
*/
public void disableDismissRunnable() {
+ if (mDismissRunnable == null) {
+ return;
+ }
+ Log.d(TAG, "remove dismiss runnable");
+ Locator.get(mLocatorContextWrapper, EventLoop.class).removeRunnable(mDismissRunnable);
+ }
+
+ /**
+ * When user first click back button or click the empty space in half sheet the half sheet will
+ * be banned for certain short period of time for that device model id. When user click cancel
+ * or dismiss half sheet for the second time the half sheet related item should be added to
+ * blocklist so the half sheet will not show again to interrupt user.
+ *
+ * @param modelId half sheet display item modelId.
+ */
+ @Annotations.EventThread
+ public void dismiss(String modelId) {
+ Log.d(TAG, "HalfSheetManager report dismiss device modelId: " + modelId);
+ mIsHalfSheetForeground = false;
+ Integer halfSheetId = mModelIdMap.get(modelId);
+ if (mDismissRunnable != null
+ && Locator.get(mLocatorContextWrapper, EventLoop.class)
+ .isPosted(mDismissRunnable)) {
+ disableDismissRunnable();
+ }
+ if (halfSheetId != null) {
+ Log.d(TAG, "id: " + halfSheetId + " half sheet is dismissed");
+ boolean isDontShowAgain =
+ !mHalfSheetBlocklist.updateState(halfSheetId,
+ Blocklist.BlocklistState.DISMISSED);
+ if (isDontShowAgain) {
+ if (!mBanCountMap.containsKey(halfSheetId)) {
+ mBanCountMap.put(halfSheetId, 0);
+ }
+ int dismissCountTrack = mBanCountMap.get(halfSheetId) + 1;
+ mBanCountMap.put(halfSheetId, dismissCountTrack);
+ if (dismissCountTrack % 2 == 1) {
+ Log.d(TAG, "id: " + halfSheetId + " half sheet is short time banned");
+ mHalfSheetBlocklist.forceUpdateState(halfSheetId,
+ Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN);
+ } else {
+ Log.d(TAG, "id: " + halfSheetId + " half sheet is long time banned");
+ mHalfSheetBlocklist.updateState(halfSheetId,
+ Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG);
+ }
+ }
+ }
+ }
+
+ /**
+ * Changes the half sheet ban state to active.
+ */
+ @UiThread
+ public void resetBanState(String modelId) {
+ Log.d(TAG, "HalfSheetManager reset device ban state modelId: " + modelId);
+ Integer halfSheetId = mModelIdMap.get(modelId);
+ if (halfSheetId == null) {
+ Log.d(TAG, "halfSheetId not found.");
+ return;
+ }
+ mHalfSheetBlocklist.resetBlockState(halfSheetId);
+ }
+
+ // Invokes this method to reset some states when showing the pairing result.
+ private void resetPairingStateDisableAutoDismiss() {
+ mIsActivePairing = false;
+ if (mDismissRunnable != null && Locator.get(mLocatorContextWrapper, EventLoop.class)
+ .isPosted(mDismissRunnable)) {
+ disableDismissRunnable();
+ }
+ }
+
+ /**
+ * When the device pairing finished should remove the suppression for the model id
+ * so the user canntry twice if the user want to.
+ */
+ public void reportDonePairing(int halfSheetId) {
+ mHalfSheetBlocklist.removeBlocklist(halfSheetId);
+ }
+
+ @VisibleForTesting
+ public FastPairHalfSheetBlocklist getHalfSheetBlocklist() {
+ return mHalfSheetBlocklist;
}
/**
@@ -166,9 +463,96 @@
}
/**
- * Notify manager the pairing has finished.
+ * Notifies manager the pairing has finished.
*/
public void notifyPairingProcessDone(boolean success, String address, DiscoveryItem item) {
+ mCurrentScanFastPairStoreItem = null;
+ mIsHalfSheetForeground = false;
+ }
+
+ private boolean allowedToShowHalfSheet(int halfSheetId) {
+ // Half Sheet will not show when the screen is locked so disable half sheet
+ KeyguardManager keyguardManager =
+ mLocatorContextWrapper.getSystemService(KeyguardManager.class);
+ if (keyguardManager != null && keyguardManager.isKeyguardLocked()) {
+ Log.d(TAG, "device is locked");
+ return false;
+ }
+
+ // Check whether the blocklist state has expired
+ if (mHalfSheetBlocklist.isStateExpired(halfSheetId)) {
+ mHalfSheetBlocklist.removeBlocklist(halfSheetId);
+ mBanCountMap.remove(halfSheetId);
+ }
+
+ // Half Sheet will not show when the model id is banned
+ if (mHalfSheetBlocklist.isBlocklisted(
+ halfSheetId, DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)) {
+ Log.d(TAG, "id: " + halfSheetId + " is blocked");
+ return false;
+ }
+ return !isHelpPageForeground();
+ }
+
+ /**
+ * Checks if the user already open the info page, return true to suppress half sheet.
+ * ActivityManager#getRunningTasks to get the most recent task and check the baseIntent's
+ * url to see if we should suppress half sheet.
+ */
+ private boolean isHelpPageForeground() {
+ ActivityManager activityManager =
+ mLocatorContextWrapper.getSystemService(ActivityManager.class);
+ if (activityManager == null) {
+ Log.d(TAG, "ActivityManager is null");
+ return false;
+ }
+ try {
+ List<ActivityManager.RunningTaskInfo> taskInfos = activityManager.getRunningTasks(1);
+ if (taskInfos.isEmpty()) {
+ Log.d(TAG, "Empty running tasks");
+ return false;
+ }
+ String url = taskInfos.get(0).baseIntent.getDataString();
+ Log.d(TAG, "Info page url:" + url);
+ if (FAST_PAIR_HALF_SHEET_HELP_URL.equals(url)) {
+ return true;
+ }
+ } catch (SecurityException e) {
+ Log.d(TAG, "Unable to get running tasks");
+ }
+ return false;
+ }
+
+ /** Report actively pairing when the Fast Pair starts. */
+ public void reportActivelyPairing() {
+ mIsActivePairing = true;
+ }
+
+
+ private Integer createNewHalfSheetId() {
+ return mNotificationIds.getAndIncrement();
+ }
+
+ /** Gets the half sheet status whether it is foreground or dismissed */
+ public boolean getHalfSheetForeground() {
+ return mIsHalfSheetForeground;
+ }
+
+ /** Sets whether the half sheet is at the foreground or not. */
+ public void setHalfSheetForeground(boolean state) {
+ mIsHalfSheetForeground = state;
+ }
+
+ /** Returns whether the fast pair is actively pairing . */
+ @VisibleForTesting
+ public boolean isActivePairing() {
+ return mIsActivePairing;
+ }
+
+ /** Sets fast pair to be active pairing or not, used for testing. */
+ @VisibleForTesting
+ public void setIsActivePairing(boolean isActivePairing) {
+ mIsActivePairing = isActivePairing;
}
/**
@@ -176,39 +560,12 @@
* getHalfSheetApkPkgName may invoke PackageManager multiple times and it does not have
* race condition check. Since there is no lock for mHalfSheetApkPkgName.
*/
- String getHalfSheetApkPkgName() {
+ private String getHalfSheetApkPkgName() {
if (mHalfSheetApkPkgName != null) {
return mHalfSheetApkPkgName;
}
- List<ResolveInfo> resolveInfos = mLocatorContextWrapper
- .getPackageManager().queryIntentActivities(
- new Intent(ACTION_RESOURCES_APK),
- PackageManager.MATCH_SYSTEM_ONLY);
-
- // remove apps that don't live in the nearby apex
- resolveInfos.removeIf(info ->
- !Environment.isAppInNearbyApex(info.activityInfo.applicationInfo));
-
- if (resolveInfos.isEmpty()) {
- // Resource APK not loaded yet, print a stack trace to see where this is called from
- Log.e("FastPairManager", "Attempted to fetch resources before halfsheet "
- + " APK is installed or package manager can't resolve correctly!",
- new IllegalStateException());
- return null;
- }
-
- if (resolveInfos.size() > 1) {
- // multiple apps found, log a warning, but continue
- Log.w("FastPairManager", "Found > 1 APK that can resolve halfsheet APK intent: "
- + resolveInfos.stream()
- .map(info -> info.activityInfo.applicationInfo.packageName)
- .collect(Collectors.joining(", ")));
- }
-
- // Assume the first ResolveInfo is the one we're looking for
- ResolveInfo info = resolveInfos.get(0);
- mHalfSheetApkPkgName = info.activityInfo.applicationInfo.packageName;
- Log.i("FastPairManager", "Found halfsheet APK at: " + mHalfSheetApkPkgName);
+ mHalfSheetApkPkgName = PackageUtils.getHalfSheetApkPkgName(mLocatorContextWrapper);
+ Log.v(TAG, "Found halfsheet APK at: " + mHalfSheetApkPkgName);
return mHalfSheetApkPkgName;
}
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
index 3bd273e..eb1fb85 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/halfsheet/FastPairUiServiceImpl.java
@@ -27,6 +27,7 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.fastpair.FastPairController;
/**
@@ -93,6 +94,14 @@
}
/**
+ * Sets fastPairStatusCallback.
+ */
+ @VisibleForTesting
+ public void setFastPairStatusCallback(FastPairStatusCallback fastPairStatusCallback) {
+ mFastPairStatusCallback = fastPairStatusCallback;
+ }
+
+ /**
* Sets function for Fast Pair controller.
*/
public void setFastPairController(FastPairController fastPairController) {
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilder.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilder.java
new file mode 100644
index 0000000..4260235
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilder.java
@@ -0,0 +1,67 @@
+/*
+ * 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.fastpair.notification;
+
+import android.app.Notification;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.nearby.halfsheet.R;
+import com.android.server.nearby.fastpair.HalfSheetResources;
+import com.android.server.nearby.fastpair.PackageUtils;
+
+/** Wrapper class for Fast Pair specific logic for notification builder. */
+public class FastPairNotificationBuilder extends Notification.Builder {
+
+ @VisibleForTesting
+ static final String NOTIFICATION_OVERRIDE_NAME_EXTRA = "android.substName";
+ final String mPackageName;
+ final Context mContext;
+ final HalfSheetResources mResources;
+
+ public FastPairNotificationBuilder(Context context, String channelId) {
+ super(context, channelId);
+ this.mContext = context;
+ this.mPackageName = PackageUtils.getHalfSheetApkPkgName(context);
+ this.mResources = new HalfSheetResources(context);
+ }
+
+ /**
+ * If the flag is enabled, all the devices notification should use "Devices" as the source name,
+ * and links/Apps uses "Nearby". If the flag is not enabled, all notifications use "Nearby" as
+ * source name.
+ */
+ public FastPairNotificationBuilder setIsDevice(boolean isDevice) {
+ Bundle extras = new Bundle();
+ String notificationOverrideName =
+ isDevice
+ ? mResources.get().getString(R.string.common_devices)
+ : mResources.get().getString(R.string.common_nearby_title);
+ extras.putString(NOTIFICATION_OVERRIDE_NAME_EXTRA, notificationOverrideName);
+ addExtras(extras);
+ return this;
+ }
+
+ /** Set the "ticker" text which is sent to accessibility services. */
+ public FastPairNotificationBuilder setTickerForAccessibility(String tickerText) {
+ // On Lollipop and above, setTicker() tells Accessibility what to say about the notification
+ // (e.g. this is what gets announced when a HUN appears).
+ setTicker(tickerText);
+ return this;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
index b1ae573..c74249c 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
@@ -16,51 +16,166 @@
package com.android.server.nearby.fastpair.notification;
+import static com.android.server.nearby.fastpair.Constant.TAG;
import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
import android.content.Context;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.nearby.halfsheet.R;
+import com.android.server.nearby.fastpair.HalfSheetResources;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.google.common.base.Objects;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+
+import java.util.concurrent.TimeUnit;
+
/**
* Responsible for show notification logic.
*/
public class FastPairNotificationManager {
- /**
- * FastPair notification manager that handle notification ui for fast pair.
- */
- public FastPairNotificationManager(Context context, DiscoveryItem item, boolean useLargeIcon,
- int notificationId) {
- }
- /**
- * FastPair notification manager that handle notification ui for fast pair.
- */
- public FastPairNotificationManager(Context context, DiscoveryItem item, boolean useLargeIcon) {
+ private static int sInstanceId = 0;
+ // Notification channel group ID for Devices notification channels.
+ private static final String DEVICES_CHANNEL_GROUP_ID = "DEVICES_CHANNEL_GROUP_ID";
+ // These channels are rebranded string because they are migrated from different channel ID they
+ // should not be changed.
+ // Channel ID for channel "Devices within reach".
+ static final String DEVICES_WITHIN_REACH_CHANNEL_ID = "DEVICES_WITHIN_REACH_REBRANDED";
+ // Channel ID for channel "Devices".
+ static final String DEVICES_CHANNEL_ID = "DEVICES_REBRANDED";
+ // Channel ID for channel "Devices with your account".
+ public static final String DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID = "DEVICES_WITH_YOUR_ACCOUNT";
+ // Default channel importance for channel "Devices within reach".
+ private static final int DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE =
+ NotificationManager.IMPORTANCE_HIGH;
+ // Default channel importance for channel "Devices".
+ private static final int DEFAULT_DEVICES_CHANNEL_IMPORTANCE =
+ NotificationManager.IMPORTANCE_LOW;
+ // Default channel importance for channel "Devices with your account".
+ private static final int DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE =
+ NotificationManager.IMPORTANCE_MIN;
+
+ /** Fixed notification ID that won't duplicated with {@code notificationId}. */
+ private static final int MAGIC_PAIR_NOTIFICATION_ID = "magic_pair_notification_id".hashCode();
+ /** Fixed notification ID that won't duplicated with {@code mNotificationId}. */
+ @VisibleForTesting
+ static final int PAIR_SUCCESS_NOTIFICATION_ID = MAGIC_PAIR_NOTIFICATION_ID - 1;
+ /** Fixed notification ID for showing the pairing failure notification. */
+ @VisibleForTesting static final int PAIR_FAILURE_NOTIFICATION_ID =
+ MAGIC_PAIR_NOTIFICATION_ID - 3;
+
+ /**
+ * The amount of delay enforced between notifications. The system only allows 10 notifications /
+ * second, but delays in the binder IPC can cause overlap.
+ */
+ private static final long MIN_NOTIFICATION_DELAY_MILLIS = 300;
+
+ // To avoid a (really unlikely) race where the user pairs and succeeds quickly more than once,
+ // use a unique ID per session, so we can delay cancellation without worrying.
+ // This is for connecting related notifications only. Discovery notification will use item id
+ // as notification id.
+ @VisibleForTesting
+ final int mNotificationId;
+ private HalfSheetResources mResources;
+ private final FastPairNotifications mNotifications;
+ private boolean mDiscoveryNotificationEnable = true;
+ // A static cache that remembers all recently shown notifications. We use this to throttle
+ // ourselves from showing notifications too rapidly. If we attempt to show a notification faster
+ // than once every 100ms, the later notifications will be dropped and we'll show stale state.
+ // Maps from Key -> Uptime Millis
+ private final Cache<Key, Long> mNotificationCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(100)
+ .expireAfterWrite(MIN_NOTIFICATION_DELAY_MILLIS, TimeUnit.MILLISECONDS)
+ .build();
+ private NotificationManager mNotificationManager;
+
+ /**
+ * FastPair notification manager that handle notification ui for fast pair.
+ */
+ @VisibleForTesting
+ public FastPairNotificationManager(Context context, int notificationId,
+ NotificationManager notificationManager, HalfSheetResources resources) {
+ mNotificationId = notificationId;
+ mNotificationManager = notificationManager;
+ mResources = resources;
+ mNotifications = new FastPairNotifications(context, mResources);
+
+ configureDevicesNotificationChannels();
+ }
+
+ /**
+ * FastPair notification manager that handle notification ui for fast pair.
+ */
+ public FastPairNotificationManager(Context context, int notificationId) {
+ this(context, notificationId, context.getSystemService(NotificationManager.class),
+ new HalfSheetResources(context));
+ }
+
+ /**
+ * FastPair notification manager that handle notification ui for fast pair.
+ */
+ public FastPairNotificationManager(Context context) {
+ this(context, /* notificationId= */ MAGIC_PAIR_NOTIFICATION_ID + sInstanceId);
+
+ sInstanceId++;
+ }
+
+ /**
+ * Shows the notification when found saved device. A notification will be like
+ * "Your saved device is available."
+ * This uses item id as notification Id. This should be disabled when connecting starts.
+ */
+ public void showDiscoveryNotification(DiscoveryItem item, byte[] accountKey) {
+ if (mDiscoveryNotificationEnable) {
+ Log.v(TAG, "the discovery notification is disabled");
+ return;
+ }
+
+ show(item.getId().hashCode(), mNotifications.discoveryNotification(item, accountKey));
}
/**
* Shows pairing in progress notification.
*/
- public void showConnectingNotification() {}
+ public void showConnectingNotification(DiscoveryItem item) {
+ disableShowDiscoveryNotification();
+ cancel(PAIR_FAILURE_NOTIFICATION_ID);
+ show(mNotificationId, mNotifications.progressNotification(item));
+ }
/**
- * Shows success notification
+ * Shows when Fast Pair successfully pairs the headset.
*/
public void showPairingSucceededNotification(
- @Nullable String companionApp,
+ DiscoveryItem item,
int batteryLevel,
- @Nullable String deviceName,
- String address) {
-
+ @Nullable String deviceName) {
+ enableShowDiscoveryNotification();
+ cancel(mNotificationId);
+ show(PAIR_SUCCESS_NOTIFICATION_ID,
+ mNotifications
+ .pairingSucceededNotification(
+ batteryLevel, deviceName, item.getTitle(), item));
}
/**
* Shows failed notification.
*/
- public void showPairingFailedNotification(byte[] accountKey) {
-
+ public synchronized void showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
+ enableShowDiscoveryNotification();
+ cancel(mNotificationId);
+ show(PAIR_FAILURE_NOTIFICATION_ID,
+ mNotifications.showPairingFailedNotification(item, accountKey));
}
/**
@@ -68,4 +183,98 @@
*/
public void notifyPairingProcessDone(boolean success, boolean forceNotify,
String privateAddress, String publicAddress) {}
+
+ /** Enables the discovery notification when pairing is in progress */
+ public void enableShowDiscoveryNotification() {
+ Log.v(TAG, "enabling discovery notification");
+ mDiscoveryNotificationEnable = true;
+ }
+
+ /** Disables the discovery notification when pairing is in progress */
+ public synchronized void disableShowDiscoveryNotification() {
+ Log.v(TAG, "disabling discovery notification");
+ mDiscoveryNotificationEnable = false;
+ }
+
+ private void show(int id, Notification notification) {
+ mNotificationManager.notify(id, notification);
+ }
+
+ /**
+ * Configures devices related notification channels, including "Devices" and "Devices within
+ * reach" channels.
+ */
+ private void configureDevicesNotificationChannels() {
+ mNotificationManager.createNotificationChannelGroup(
+ new NotificationChannelGroup(
+ DEVICES_CHANNEL_GROUP_ID,
+ mResources.get().getString(R.string.common_devices)));
+ mNotificationManager.createNotificationChannel(
+ createNotificationChannel(
+ DEVICES_WITHIN_REACH_CHANNEL_ID,
+ mResources.get().getString(R.string.devices_within_reach_channel_name),
+ DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE,
+ DEVICES_CHANNEL_GROUP_ID));
+ mNotificationManager.createNotificationChannel(
+ createNotificationChannel(
+ DEVICES_CHANNEL_ID,
+ mResources.get().getString(R.string.common_devices),
+ DEFAULT_DEVICES_CHANNEL_IMPORTANCE,
+ DEVICES_CHANNEL_GROUP_ID));
+ mNotificationManager.createNotificationChannel(
+ createNotificationChannel(
+ DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID,
+ mResources.get().getString(R.string.devices_with_your_account_channel_name),
+ DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE,
+ DEVICES_CHANNEL_GROUP_ID));
+ }
+
+ private NotificationChannel createNotificationChannel(
+ String channelId, String channelName, int channelImportance, String channelGroupId) {
+ NotificationChannel channel =
+ new NotificationChannel(channelId, channelName, channelImportance);
+ channel.setGroup(channelGroupId);
+ if (channelImportance >= NotificationManager.IMPORTANCE_HIGH) {
+ channel.setSound(/* sound= */ null, /* audioAttributes= */ null);
+ // Disable vibration. Otherwise, the silent sound triggers a vibration if your
+ // ring volume is set to vibrate (aka turned down all the way).
+ channel.enableVibration(false);
+ }
+
+ return channel;
+ }
+
+ /** Cancel a previously shown notification. */
+ public void cancel(int id) {
+ try {
+ mNotificationManager.cancel(id);
+ } catch (SecurityException e) {
+ Log.e(TAG, "Failed to cancel notification " + id, e);
+ }
+ mNotificationCache.invalidate(new Key(id));
+ }
+
+ private static final class Key {
+ @Nullable final String mTag;
+ final int mId;
+
+ Key(int id) {
+ this.mTag = null;
+ this.mId = id;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof Key) {
+ Key that = (Key) o;
+ return Objects.equal(mTag, that.mTag) && (mId == that.mId);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mTag == null ? 0 : mTag, mId);
+ }
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotifications.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotifications.java
new file mode 100644
index 0000000..3b4eef8
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotifications.java
@@ -0,0 +1,207 @@
+/*
+ * 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.fastpair.notification;
+
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
+import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
+import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_ITEM_ID;
+import static com.android.server.nearby.fastpair.notification.FastPairNotificationManager.DEVICES_WITHIN_REACH_CHANNEL_ID;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.os.SystemClock;
+import android.provider.Settings;
+
+import com.android.nearby.halfsheet.R;
+import com.android.server.nearby.common.fastpair.IconUtils;
+import com.android.server.nearby.fastpair.HalfSheetResources;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+
+import service.proto.Cache;
+
+/**
+ * Collection of utilities to create {@link Notification} objects that are displayed through {@link
+ * FastPairNotificationManager}.
+ */
+public class FastPairNotifications {
+
+ private final Context mContext;
+ private final HalfSheetResources mResources;
+ /**
+ * Note: Idea copied from Google.
+ *
+ * <p>Request code used for notification pending intents (executed on tap, dismiss).
+ *
+ * <p>Android only keeps one PendingIntent instance if it thinks multiple pending intents match.
+ * As comparing PendingIntents/Intents does not inspect the data in the extras, multiple pending
+ * intents can conflict. This can have surprising consequences (see b/68702692#comment8).
+ *
+ * <p>We also need to avoid conflicts with notifications started by an earlier launch of the app
+ * so use the truncated uptime of when the class was instantiated. The uptime will only overflow
+ * every ~50 days, and even then chances of conflict will be rare.
+ */
+ private static int sRequestCode = (int) SystemClock.elapsedRealtime();
+
+ public FastPairNotifications(Context context, HalfSheetResources resources) {
+ this.mContext = context;
+ this.mResources = resources;
+ }
+
+ /**
+ * Creates the initial "Your saved device is available" notification when subsequent pairing
+ * is available.
+ * @param item discovered item which contains title and item id
+ * @param accountKey used for generating intent for pairing
+ */
+ public Notification discoveryNotification(DiscoveryItem item, byte[] accountKey) {
+ Notification.Builder builder =
+ newBaseBuilder(item)
+ .setContentTitle(mResources.getString(R.string.fast_pair_your_device))
+ .setContentText(item.getTitle())
+ .setContentIntent(getPairIntent(item.getCopyOfStoredItem(), accountKey))
+ .setCategory(Notification.CATEGORY_RECOMMENDATION)
+ .setAutoCancel(false);
+ return builder.build();
+ }
+
+ /**
+ * Creates the in progress "Connecting" notification when the device and headset are paring.
+ */
+ public Notification progressNotification(DiscoveryItem item) {
+ String summary = mResources.getString(R.string.common_connecting);
+ Notification.Builder builder =
+ newBaseBuilder(item)
+ .setTickerForAccessibility(summary)
+ .setCategory(Notification.CATEGORY_PROGRESS)
+ .setContentTitle(mResources.getString(R.string.fast_pair_your_device))
+ .setContentText(summary)
+ // Intermediate progress bar.
+ .setProgress(0, 0, true)
+ // Tapping does not dismiss this.
+ .setAutoCancel(false);
+
+ return builder.build();
+ }
+
+ /**
+ * Creates paring failed notification.
+ */
+ public Notification showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
+ String couldNotPair = mResources.getString(R.string.fast_pair_unable_to_connect);
+ String notificationContent;
+ if (accountKey != null) {
+ notificationContent = mResources.getString(
+ R.string.fast_pair_turn_on_bt_device_pairing_mode);
+ } else {
+ notificationContent =
+ mResources.getString(R.string.fast_pair_unable_to_connect_description);
+ }
+ Notification.Builder builder =
+ newBaseBuilder(item)
+ .setTickerForAccessibility(couldNotPair)
+ .setCategory(Notification.CATEGORY_ERROR)
+ .setContentTitle(couldNotPair)
+ .setContentText(notificationContent)
+ .setContentIntent(getBluetoothSettingsIntent())
+ // Dismissing completes the attempt.
+ .setDeleteIntent(getBluetoothSettingsIntent());
+ return builder.build();
+
+ }
+
+ /**
+ * Creates paring successfully notification.
+ */
+ public Notification pairingSucceededNotification(
+ int batteryLevel,
+ @Nullable String deviceName,
+ String modelName,
+ DiscoveryItem item) {
+ final String contentText;
+ StringBuilder contentTextBuilder = new StringBuilder();
+ contentTextBuilder.append(modelName);
+ if (batteryLevel >= 0 && batteryLevel <= 100) {
+ contentTextBuilder
+ .append("\n")
+ .append(mResources.getString(R.string.common_battery_level, batteryLevel));
+ }
+ String pairingComplete =
+ deviceName == null
+ ? mResources.getString(R.string.fast_pair_device_ready)
+ : mResources.getString(
+ R.string.fast_pair_device_ready_with_device_name, deviceName);
+ contentText = contentTextBuilder.toString();
+ Notification.Builder builder =
+ newBaseBuilder(item)
+ .setTickerForAccessibility(pairingComplete)
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setContentTitle(pairingComplete)
+ .setContentText(contentText);
+
+ return builder.build();
+ }
+
+ private PendingIntent getPairIntent(Cache.StoredDiscoveryItem item, byte[] accountKey) {
+ Intent intent =
+ new Intent(ACTION_FAST_PAIR)
+ .putExtra(EXTRA_ITEM_ID, item.getId())
+ // Encode account key as a string instead of bytes so that it can be passed
+ // to the string representation of the intent.
+ .putExtra(EXTRA_FAST_PAIR_SECRET, base16().encode(accountKey))
+ .setPackage(mContext.getPackageName());
+ return PendingIntent.getBroadcast(mContext, sRequestCode++, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
+ }
+
+ private PendingIntent getBluetoothSettingsIntent() {
+ Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
+ return PendingIntent.getActivity(mContext, sRequestCode++, intent,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ private LargeHeadsUpNotificationBuilder newBaseBuilder(DiscoveryItem item) {
+ LargeHeadsUpNotificationBuilder builder =
+ (LargeHeadsUpNotificationBuilder)
+ (new LargeHeadsUpNotificationBuilder(
+ mContext,
+ DEVICES_WITHIN_REACH_CHANNEL_ID,
+ /* largeIcon= */ true)
+ .setIsDevice(true)
+ // Tapping does not dismiss this.
+ .setSmallIcon(Icon.createWithResource(
+ mResources.getResourcesContext(),
+ R.drawable.quantum_ic_devices_other_vd_theme_24)))
+ .setLargeIcon(IconUtils.addWhiteCircleBackground(
+ mResources.getResourcesContext(), item.getIcon()))
+ // Dismissible.
+ .setOngoing(false)
+ // Timestamp is not relevant, hide it.
+ .setShowWhen(false)
+ .setColor(mResources.getColor(R.color.discovery_activity_accent))
+ .setLocalOnly(true)
+ // don't show these notifications on wear devices
+ .setAutoCancel(true);
+
+ return builder;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/LargeHeadsUpNotificationBuilder.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/LargeHeadsUpNotificationBuilder.java
new file mode 100644
index 0000000..ec41d76
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/fastpair/notification/LargeHeadsUpNotificationBuilder.java
@@ -0,0 +1,188 @@
+/*
+ * 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.fastpair.notification;
+
+import static com.android.server.nearby.fastpair.Constant.TAG;
+
+import android.annotation.LayoutRes;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+
+import com.android.nearby.halfsheet.R;
+
+/** Wrapper class for creating larger heads up notifications. */
+public class LargeHeadsUpNotificationBuilder extends FastPairNotificationBuilder {
+ private final boolean mLargeIcon;
+ private final RemoteViews mNotification;
+ private final RemoteViews mNotificationCollapsed;
+
+ @Nullable private Runnable mLargeIconAction;
+ @Nullable private Runnable mProgressAction;
+
+ public LargeHeadsUpNotificationBuilder(Context context, String channelId, boolean largeIcon) {
+ super(context, channelId);
+
+ this.mLargeIcon = largeIcon;
+ this.mNotification = getRemoteViews(R.layout.fast_pair_heads_up_notification);
+ this.mNotificationCollapsed = getRemoteViews(R.layout.fast_pair_heads_up_notification);
+
+ if (mNotification != null) {
+ mNotificationCollapsed.setViewVisibility(android.R.id.secondaryProgress, View.GONE);
+ }
+ }
+
+ /**
+ * Create a new RemoteViews object that will display the views contained
+ * fast_pair_heads_up_notification layout.
+ */
+ @Nullable
+ public RemoteViews getRemoteViews(@LayoutRes int layoutId) {
+ return new RemoteViews(mPackageName, layoutId);
+ }
+
+ @Override
+ public Notification.Builder setContentTitle(@Nullable CharSequence title) {
+ if (mNotification != null) {
+ mNotification.setTextViewText(android.R.id.title, title);
+ }
+ if (mNotificationCollapsed != null) {
+ mNotificationCollapsed.setTextViewText(android.R.id.title, title);
+ // Collapsed mode does not need additional lines.
+ mNotificationCollapsed.setInt(android.R.id.title, "setMaxLines", 1);
+ }
+ return super.setContentTitle(title);
+ }
+
+ @Override
+ public Notification.Builder setContentText(@Nullable CharSequence text) {
+ if (mNotification != null) {
+ mNotification.setTextViewText(android.R.id.text1, text);
+ }
+ if (mNotificationCollapsed != null) {
+ mNotificationCollapsed.setTextViewText(android.R.id.text1, text);
+ // Collapsed mode does not need additional lines.
+ mNotificationCollapsed.setInt(android.R.id.text1, "setMaxLines", 1);
+ }
+
+ return super.setContentText(text);
+ }
+
+ @Override
+ public Notification.Builder setSubText(@Nullable CharSequence subText) {
+ if (mNotification != null) {
+ mNotification.setTextViewText(android.R.id.text2, subText);
+ }
+ if (mNotificationCollapsed != null) {
+ mNotificationCollapsed.setTextViewText(android.R.id.text2, subText);
+ }
+ return super.setSubText(subText);
+ }
+
+ @Override
+ public Notification.Builder setLargeIcon(@Nullable Bitmap bitmap) {
+ RemoteViews image =
+ getRemoteViews(
+ useLargeIcon()
+ ? R.layout.fast_pair_heads_up_notification_large_image
+ : R.layout.fast_pair_heads_up_notification_small_image);
+ if (image == null) {
+ return super.setLargeIcon(bitmap);
+ }
+ image.setImageViewBitmap(android.R.id.icon, bitmap);
+
+ if (mNotification != null) {
+ mNotification.removeAllViews(android.R.id.icon1);
+ mNotification.addView(android.R.id.icon1, image);
+ }
+ if (mNotificationCollapsed != null) {
+ mNotificationCollapsed.removeAllViews(android.R.id.icon1);
+ mNotificationCollapsed.addView(android.R.id.icon1, image);
+ // In Android S, if super.setLargeIcon() is called, there will be an extra icon on
+ // top-right.
+ // But we still need to store this setting for the default UI when something wrong.
+ mLargeIconAction = () -> super.setLargeIcon(bitmap);
+ return this;
+ }
+ return super.setLargeIcon(bitmap);
+ }
+
+ @Override
+ public Notification.Builder setProgress(int max, int progress, boolean indeterminate) {
+ if (mNotification != null) {
+ mNotification.setViewVisibility(android.R.id.secondaryProgress, View.VISIBLE);
+ mNotification.setProgressBar(android.R.id.progress, max, progress, indeterminate);
+ }
+ if (mNotificationCollapsed != null) {
+ mNotificationCollapsed.setViewVisibility(android.R.id.secondaryProgress, View.VISIBLE);
+ mNotificationCollapsed.setProgressBar(android.R.id.progress, max, progress,
+ indeterminate);
+ // In Android S, if super.setProgress() is called, there will be an extra progress bar.
+ // But we still need to store this setting for the default UI when something wrong.
+ mProgressAction = () -> super.setProgress(max, progress, indeterminate);
+ return this;
+ }
+ return super.setProgress(max, progress, indeterminate);
+ }
+
+ @Override
+ public Notification build() {
+ if (mNotification != null) {
+ boolean buildSuccess = false;
+ try {
+ // Attempt to apply the remote views. This verifies that all of the resources are
+ // correctly available.
+ // If it fails, fall back to a non-custom notification.
+ mNotification.apply(mContext, new LinearLayout(mContext));
+ if (mNotificationCollapsed != null) {
+ mNotificationCollapsed.apply(mContext, new LinearLayout(mContext));
+ }
+ buildSuccess = true;
+ } catch (Resources.NotFoundException e) {
+ Log.w(TAG, "Failed to build notification, not setting custom view.", e);
+ }
+
+ if (buildSuccess) {
+ if (mNotificationCollapsed != null) {
+ setStyle(new Notification.DecoratedCustomViewStyle());
+ setCustomContentView(mNotificationCollapsed);
+ setCustomBigContentView(mNotification);
+ } else {
+ setCustomHeadsUpContentView(mNotification);
+ }
+ } else {
+ if (mLargeIconAction != null) {
+ mLargeIconAction.run();
+ }
+ if (mProgressAction != null) {
+ mProgressAction.run();
+ }
+ }
+ }
+ return super.build();
+ }
+
+ private boolean useLargeIcon() {
+ return mLargeIcon;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
index c95f74f..e56f1ea 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandler.java
@@ -63,9 +63,10 @@
public void onPairingStarted() {
super.onPairingStarted();
// Half sheet is not in the foreground reshow half sheet, also avoid showing HalfSheet on TV
- if (!mFastPairHalfSheetManager.getHalfSheetForegroundState()) {
+ if (!mFastPairHalfSheetManager.getHalfSheetForeground()) {
mFastPairHalfSheetManager.showPairingHalfSheet(mItemResurface);
}
+ mFastPairHalfSheetManager.reportActivelyPairing();
mFastPairHalfSheetManager.disableDismissRunnable();
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
index d469c45..5317673 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
@@ -20,7 +20,6 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
-import android.text.TextUtils;
import android.util.Log;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
@@ -68,7 +67,7 @@
@Override
public void onReadyToPair() {
super.onReadyToPair();
- mFastPairNotificationManager.showConnectingNotification();
+ mFastPairNotificationManager.showConnectingNotification(mItem);
}
@Override
@@ -79,32 +78,24 @@
String address) {
String deviceName = super.onPairedCallbackCalled(connection, accountKey, footprints,
address);
-
int batteryLevel = -1;
BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
- if (bluetoothAdapter != null) {
- // Need to check battery level here set that to -1 for now
- batteryLevel = -1;
+ if (address != null && bluetoothAdapter != null) {
+ batteryLevel = bluetoothAdapter.getRemoteDevice(address).getBatteryLevel();
} else {
- Log.v(
- "NotificationPairingProgressHandler",
- "onPairedCallbackCalled getBatteryLevel failed,"
- + " adapter is null");
+ Log.v(TAG, "onPairedCallbackCalled getBatteryLevel failed");
}
- mFastPairNotificationManager.showPairingSucceededNotification(
- !TextUtils.isEmpty(mCompanionApp) ? mCompanionApp : null,
- batteryLevel,
- deviceName,
- address);
+ mFastPairNotificationManager
+ .showPairingSucceededNotification(mItem, batteryLevel, deviceName);
return deviceName;
}
@Override
public void onPairingFailed(Throwable throwable) {
super.onPairingFailed(throwable);
- mFastPairNotificationManager.showPairingFailedNotification(mAccountKey);
+ mFastPairNotificationManager.showPairingFailedNotification(mItem, mAccountKey);
mFastPairNotificationManager.notifyPairingProcessDone(
/* success= */ false,
/* forceNotify= */ false,
@@ -115,6 +106,19 @@
@Override
public void onPairingSuccess(String address) {
super.onPairingSuccess(address);
+ int batteryLevel = -1;
+
+ BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
+ BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
+ String deviceName = null;
+ if (address != null && bluetoothAdapter != null) {
+ deviceName = bluetoothAdapter.getRemoteDevice(address).getName();
+ batteryLevel = bluetoothAdapter.getRemoteDevice(address).getBatteryLevel();
+ } else {
+ Log.v(TAG, "onPairedCallbackCalled getBatteryLevel failed");
+ }
+ mFastPairNotificationManager
+ .showPairingSucceededNotification(mItem, batteryLevel, deviceName);
mFastPairNotificationManager.notifyPairingProcessDone(
/* success= */ true,
/* forceNotify= */ false,
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
index 5fb05d5..6f2dc40 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
@@ -35,6 +35,7 @@
/** Base class for pairing progress handler. */
public abstract class PairingProgressHandlerBase {
+ protected static final String TAG = "FPPairingHandler";
protected final Context mContext;
protected final DiscoveryItem mItem;
@Nullable
@@ -49,7 +50,6 @@
this.mItem = item;
}
-
/**
* Pairing progress init function.
*/
@@ -75,59 +75,55 @@
new HalfSheetPairingProgressHandler(context, item, companionApp, accountKey);
}
-
- Log.v("PairingHandler",
- "PairingProgressHandler:Create "
- + item.getMacAddress() + " for pairing");
+ Log.v(TAG, "PairingProgressHandler:Create " + item.getMacAddress() + " for pairing");
return pairingProgressHandlerBase;
}
-
/**
* Function calls when pairing start.
*/
public void onPairingStarted() {
- Log.v("PairingHandler", "PairingProgressHandler:onPairingStarted");
+ Log.v(TAG, "PairingProgressHandler:onPairingStarted");
}
/**
* Waits for screen to unlock.
*/
public void onWaitForScreenUnlock() {
- Log.v("PairingHandler", "PairingProgressHandler:onWaitForScreenUnlock");
+ Log.v(TAG, "PairingProgressHandler:onWaitForScreenUnlock");
}
/**
* Function calls when screen unlock.
*/
public void onScreenUnlocked() {
- Log.v("PairingHandler", "PairingProgressHandler:onScreenUnlocked");
+ Log.v(TAG, "PairingProgressHandler:onScreenUnlocked");
}
/**
* Calls when the handler is ready to pair.
*/
public void onReadyToPair() {
- Log.v("PairingHandler", "PairingProgressHandler:onReadyToPair");
+ Log.v(TAG, "PairingProgressHandler:onReadyToPair");
}
/**
* Helps to set up pairing preference.
*/
public void onSetupPreferencesBuilder(Preferences.Builder builder) {
- Log.v("PairingHandler", "PairingProgressHandler:onSetupPreferencesBuilder");
+ Log.v(TAG, "PairingProgressHandler:onSetupPreferencesBuilder");
}
/**
* Calls when pairing setup complete.
*/
public void onPairingSetupCompleted() {
- Log.v("PairingHandler", "PairingProgressHandler:onPairingSetupCompleted");
+ Log.v(TAG, "PairingProgressHandler:onPairingSetupCompleted");
}
/** Called while pairing if needs to handle the passkey confirmation by Ui. */
public void onHandlePasskeyConfirmation(BluetoothDevice device, int passkey) {
- Log.v("PairingHandler", "PairingProgressHandler:onHandlePasskeyConfirmation");
+ Log.v(TAG, "PairingProgressHandler:onHandlePasskeyConfirmation");
}
/**
@@ -149,7 +145,7 @@
byte[] accountKey,
FootprintsDeviceManager footprints,
String address) {
- Log.v("PairingHandler",
+ Log.v(TAG,
"PairingProgressHandler:onPairedCallbackCalled with address: "
+ address);
@@ -166,7 +162,7 @@
public byte[] getKeyForLocalCache(
byte[] accountKey, FastPairConnection connection,
FastPairConnection.SharedSecret sharedSecret) {
- Log.v("PairingHandler", "PairingProgressHandler:getKeyForLocalCache");
+ Log.v(TAG, "PairingProgressHandler:getKeyForLocalCache");
return accountKey != null ? accountKey : connection.getExistingAccountKey();
}
@@ -174,14 +170,14 @@
* Function handles pairing fail.
*/
public void onPairingFailed(Throwable throwable) {
- Log.w("PairingHandler", "PairingProgressHandler:onPairingFailed");
+ Log.w(TAG, "PairingProgressHandler:onPairingFailed");
}
/**
* Function handles pairing success.
*/
public void onPairingSuccess(String address) {
- Log.v("PairingHandler", "PairingProgressHandler:onPairingSuccess with address: "
+ Log.v(TAG, "PairingProgressHandler:onPairingSuccess with address: "
+ maskBluetoothAddress(address));
}
@@ -193,7 +189,7 @@
@Nullable byte[] existingAccountKey) {
if (isThroughFastPair2InitialPairing(item, accountKey) && existingAccountKey == null) {
// enable the save to footprint
- Log.v("PairingHandler", "footprint should call opt in here");
+ Log.v(TAG, "footprint should call opt in here");
}
}
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
index 7fcf8f8..0c41426 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,7 +21,6 @@
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;
@@ -30,272 +29,66 @@
import android.nearby.PresenceScanFilter;
import android.nearby.ScanFilter;
import android.nearby.ScanRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
+import android.nearby.aidl.IOffloadCallback;
import android.util.Log;
-import com.android.internal.annotations.GuardedBy;
+import androidx.annotation.NonNull;
+
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.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.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.Collection;
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.stream.Collectors;
+
+import javax.annotation.concurrent.GuardedBy;
/** Manages all aspects of discovery providers. */
-public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
+public class DiscoveryProviderManager extends
+ ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> implements
+ AbstractDiscoveryProvider.Listener,
+ DiscoveryManager {
protected final Object mLock = new Object();
- private final Context mContext;
- private final BleDiscoveryProvider mBleDiscoveryProvider;
@VisibleForTesting
@Nullable
final ChreDiscoveryProvider mChreDiscoveryProvider;
- private @ScanRequest.ScanMode int mScanMode;
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
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());
- 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);
- }
- }
- }
- }
+ private final Executor mExecutor;
public DiscoveryProviderManager(Context context, Injector injector) {
+ Log.v(TAG, "DiscoveryProviderManager: ");
mContext = context;
mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
- Executor executor = Executors.newSingleThreadExecutor();
- mChreDiscoveryProvider =
- new ChreDiscoveryProvider(
- mContext, new ChreCommunication(injector, mContext, executor), executor);
- mScanTypeScanListenerRecordMap = new HashMap<>();
+ mExecutor = Executors.newSingleThreadExecutor();
+ mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
+ new ChreCommunication(injector, mContext, mExecutor), mExecutor);
mInjector = injector;
}
@VisibleForTesting
- DiscoveryProviderManager(Context context, Injector injector,
+ DiscoveryProviderManager(Context context, Executor executor, Injector injector,
BleDiscoveryProvider bleDiscoveryProvider,
- ChreDiscoveryProvider chreDiscoveryProvider,
- Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+ ChreDiscoveryProvider chreDiscoveryProvider) {
mContext = context;
+ mExecutor = executor;
mInjector = injector;
mBleDiscoveryProvider = bleDiscoveryProvider;
mChreDiscoveryProvider = chreDiscoveryProvider;
- mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
}
- /** 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();
- }
- }
- }
- }
-
- /**
- * @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 static boolean isChreOnly(List<ScanFilter> scanFilters) {
+ private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
for (ScanFilter scanFilter : scanFilters) {
List<DataElement> dataElements =
((PresenceScanFilter) scanFilter).getExtendedProperties();
@@ -316,41 +109,157 @@
return false;
}
- private void startBleProvider(List<ScanFilter> scanFilters) {
+ @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(mScanMode);
- mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ mBleDiscoveryProvider.getController().setProviderScanFilters(
+ new ArrayList<>(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);
+ @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 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();
@@ -368,96 +277,40 @@
@VisibleForTesting
void invalidateProviderScanMode() {
if (mBleDiscoveryProvider.getController().isStarted()) {
- mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ synchronized (mMultiplexerLock) {
+ mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+ }
} else {
- Log.d(
- TAG,
- "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
- + "started.");
+ Log.d(TAG, "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
}
}
- @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;
+ @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;
}
}
- return false;
+ builder.setScanMode(scanMode);
+ return builder.build();
}
- /**
- * 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);
- }
- }
-
- @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);
- }
+ @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/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
index 8b2db50..deb5167 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -38,6 +38,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.Constant;
import java.util.Arrays;
import java.util.List;
@@ -72,6 +73,11 @@
@Override
public void onLost(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onError(int errorCode) {
+ Log.w(Constant.TAG, "[PresenceManager] Scan error is " + errorCode);
+ }
};
private final BroadcastReceiver mScreenBroadcastReceiver =
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 64c55f0..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;
@@ -88,6 +89,11 @@
* 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 {
@@ -104,6 +110,7 @@
public boolean isStarted() {
return mStarted;
}
+
@Override
public void start() {
if (mStarted) {
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 55176ba..c9098a6 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 android.annotation.Nullable;
@@ -42,12 +44,14 @@
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.presence.PresenceConstants;
+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;
@@ -84,10 +88,12 @@
@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) {
@@ -115,6 +121,7 @@
@Override
public void onScanFailed(int errorCode) {
Log.w(TAG, "BLE 5.0 Scan failed with error code " + errorCode);
+ mExecutor.execute(() -> mListener.onError(ERROR_UNKNOWN));
}
};
@@ -297,6 +304,9 @@
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 b24e7d9..020c7b2 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -39,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
@@ -47,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. */
@@ -72,8 +76,10 @@
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, Context context, Executor executor) {
mInjector = injector;
@@ -169,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);
@@ -245,6 +270,7 @@
return;
}
+ mCountDownLatch = new CountDownLatch(1);
if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
for (NanoAppState state : response.getContents()) {
long version = state.getNanoAppVersion();
@@ -264,6 +290,8 @@
mExecutor, ChreCommunication.this);
mChreSupport = true;
mCallback.started(true);
+ mNanoAppVersion = version;
+ mCountDownLatch.countDown();
return;
}
}
@@ -281,6 +309,7 @@
}
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()) {
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 1b82af3..7ab0523 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -29,10 +29,13 @@
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;
@@ -41,6 +44,7 @@
import com.google.protobuf.ByteString;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
@@ -141,6 +145,23 @@
return mChreCommunication.available();
}
+ /**
+ * 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) {
@@ -342,6 +363,7 @@
NearbyDeviceParcelable device =
new NearbyDeviceParcelable.Builder()
+ .setDeviceId(Arrays.hashCode(secretId))
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.setMedium(NearbyDevice.Medium.BLE)
.setTxPower(filterResult.getTxPower())
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/Clock.java b/nearby/service/java/com/android/server/nearby/util/Clock.java
new file mode 100644
index 0000000..037b6f9
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/Clock.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;
+
+import android.os.SystemClock;
+
+/** Wrapper interface for time operations. Allows replacement of clock operations for testing. */
+public interface Clock {
+
+ /**
+ * Get the current time of the clock in milliseconds.
+ *
+ * @return Current time in milliseconds.
+ */
+ long currentTimeMillis();
+
+ /**
+ * Returns milliseconds since boot, including time spent in sleep.
+ *
+ * @return Current time since boot in milliseconds.
+ */
+ long elapsedRealtime();
+
+ /**
+ * Returns the current timestamp of the most precise timer available on the local system, in
+ * nanoseconds.
+ *
+ * @return Current time in nanoseconds.
+ */
+ long nanoTime();
+
+ /**
+ * Returns the time spent in the current thread, in milliseconds
+ *
+ * @return Thread time in milliseconds.
+ */
+ @SuppressWarnings("StaticOrDefaultInterfaceMethod")
+ default long currentThreadTimeMillis() {
+ return SystemClock.currentThreadTimeMillis();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/DataUtils.java b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
index c3bae08..12bf384 100644
--- a/nearby/service/java/com/android/server/nearby/util/DataUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
@@ -38,11 +38,12 @@
*/
public static ScanFastPairStoreItem toScanFastPairStoreItem(
GetObservedDeviceResponse observedDeviceResponse,
- @NonNull String bleAddress, @Nullable String account) {
+ @NonNull String bleAddress, @NonNull String modelId, @Nullable String account) {
Device device = observedDeviceResponse.getDevice();
String deviceName = device.getName();
return ScanFastPairStoreItem.newBuilder()
.setAddress(bleAddress)
+ .setModelId(modelId)
.setActionUrl(device.getIntentUri())
.setDeviceName(deviceName)
.setIconPng(observedDeviceResponse.getImage())
diff --git a/nearby/service/java/com/android/server/nearby/util/DefaultClock.java b/nearby/service/java/com/android/server/nearby/util/DefaultClock.java
new file mode 100644
index 0000000..61998e9
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/DefaultClock.java
@@ -0,0 +1,52 @@
+/*
+ * 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 android.os.SystemClock;
+
+/** Default implementation of Clock. Instances of this class handle time operations. */
+public class DefaultClock implements Clock {
+
+ private static final DefaultClock sInstance = new DefaultClock();
+
+ /** Returns an instance of DefaultClock. */
+ public static Clock getsInstance() {
+ return sInstance;
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public long nanoTime() {
+ return System.nanoTime();
+ }
+
+ @Override
+ public long currentThreadTimeMillis() {
+ return SystemClock.currentThreadTimeMillis();
+ }
+
+ public DefaultClock() {}
+}
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/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 0410cd5..a61d180 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -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/NearbyDeviceParcelableTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
deleted file mode 100644
index 2907e56..0000000
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.nearby.cts;
-
-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.nearby.PublicCredential;
-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;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-
-@RunWith(AndroidJUnit4.class)
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
-public class NearbyDeviceParcelableTest {
-
- 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 NearbyDeviceParcelable.Builder mBuilder;
-
- @Before
- public void setUp() {
- mBuilder =
- new NearbyDeviceParcelable.Builder()
- .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
- .setName("testDevice")
- .setMedium(NearbyDevice.Medium.BLE)
- .setRssi(RSSI)
- .setFastPairModelId(FAST_PAIR_MODEL_ID)
- .setBluetoothAddress(BLUETOOTH_ADDRESS)
- .setData(SCAN_DATA);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
- 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)
- .setPublicCredential(publicCredential)
- .setAction(ACTION)
- .setRssi(RSSI)
- .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
- .setTxPower(TX_POWER)
- .setSalt(SALT)
- .build();
-
- 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.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();
-
- Parcel parcel = Parcel.obtain();
- nearbyDeviceParcelable.writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
- NearbyDeviceParcelable actualNearbyDevice =
- NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
- parcel.recycle();
-
- assertThat(actualNearbyDevice.getRssi()).isEqualTo(RSSI);
- assertThat(actualNearbyDevice.getFastPairModelId()).isEqualTo(FAST_PAIR_MODEL_ID);
- assertThat(actualNearbyDevice.getBluetoothAddress()).isEqualTo(BLUETOOTH_ADDRESS);
- assertThat(Arrays.equals(actualNearbyDevice.getData(), SCAN_DATA)).isTrue();
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
- public void testWriteParcel_nullModelId() {
- NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setFastPairModelId(null).build();
-
- Parcel parcel = Parcel.obtain();
- nearbyDeviceParcelable.writeToParcel(parcel, 0);
- parcel.setDataPosition(0);
- NearbyDeviceParcelable actualNearbyDevice =
- NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
- parcel.recycle();
-
- assertThat(actualNearbyDevice.getFastPairModelId()).isNull();
- }
-
- @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);
- NearbyDeviceParcelable actualNearbyDevice =
- NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
- parcel.recycle();
-
- assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
- public void describeContents() {
- NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
- assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
- }
-
- @Test
- @SdkSuppress(minSdkVersion = 33, codeName = "T")
- 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/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 462d05a..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();
@@ -159,12 +174,19 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void setFastPairScanEnabled() {
- mNearbyManager.setFastPairScanEnabled(mContext, true);
- assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isTrue();
- mNearbyManager.setFastPairScanEnabled(mContext, false);
- assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isFalse();
+ @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() {
@@ -174,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/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index de4b1c3..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();
@@ -172,7 +160,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void describeContents() {
ScanRequest request = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_FAST_PAIR)
@@ -181,7 +169,7 @@
}
@Test
- @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testCreatorNewArray() {
ScanRequest[] requests =
ScanRequest.CREATOR.newArray(2);
@@ -207,8 +195,4 @@
.addPresenceAction(action)
.build();
}
-
- private static WorkSource getWorkSource() {
- return new WorkSource(UID, APP_NAME);
- }
}
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/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt b/nearby/tests/integration/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt
index 8b19d5c..4098865 100644
--- a/nearby/tests/integration/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt
+++ b/nearby/tests/integration/ui/src/android/nearby/integration/ui/NearbyHalfSheetUiMap.kt
@@ -25,13 +25,13 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
-import com.android.server.nearby.fastpair.FastPairManager
import com.android.server.nearby.util.Environment
import com.google.common.truth.Truth.assertThat
/** UiMap for Nearby Mainline Half Sheet. */
object NearbyHalfSheetUiMap {
private val PACKAGE_NAME: String = getHalfSheetApkPkgName()
+ private const val ACTION_RESOURCES_APK = "android.nearby.SHOW_HALFSHEET"
private const val ANDROID_WIDGET_BUTTON = "android.widget.Button"
private const val ANDROID_WIDGET_IMAGE_VIEW = "android.widget.ImageView"
private const val ANDROID_WIDGET_TEXT_VIEW = "android.widget.TextView"
@@ -54,8 +54,7 @@
fun getHalfSheetApkPkgName(): String {
val appContext = ApplicationProvider.getApplicationContext<Context>()
val resolveInfos: MutableList<ResolveInfo> =
- appContext.packageManager.queryIntentActivities(
- Intent(FastPairManager.ACTION_RESOURCES_APK),
+ appContext.packageManager.queryIntentActivities(Intent(ACTION_RESOURCES_APK),
ResolveInfoFlags.of(MATCH_SYSTEM_ONLY.toLong())
)
@@ -68,4 +67,4 @@
Log.i("NearbyHalfSheetUiMap", "Found half-sheet APK at: $halfSheetApkPkgName")
return halfSheetApkPkgName
}
-}
\ No newline at end of file
+}
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
index 363355f..02847b5 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/ScanCallbackEvents.kt
@@ -40,4 +40,10 @@
putString("device", device.toString())
}
}
-}
\ No newline at end of file
+
+ override fun onError(errorCode: Int) {
+ postSnippetEvent(callbackId, "onError") {
+ putString("error", errorCode.toString())
+ }
+ }
+}
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 9b35452..8a8aeab 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -29,6 +29,7 @@
"android.test.base",
"android.test.mock",
"android.test.runner",
+ "HalfSheetUX",
],
compile_multilib: "both",
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/common/CancelableAlarmTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
index c7d3e25..719e816 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
@@ -80,14 +80,14 @@
@Test
public void cancelOfRunAlarmReturnsFalse() throws InterruptedException {
TestCountDownLatch latch = new TestCountDownLatch(1);
- long delayMillis = 1000;
+ long delayMillis = 500;
CancelableAlarm alarm =
CancelableAlarm.createSingleAlarm(
"cancelOfRunAlarmReturnsFalse",
new CountDownRunnable(latch),
delayMillis,
mExecutor);
- latch.awaitAndExpectDelay(delayMillis);
+ latch.awaitAndExpectDelay(delayMillis - 1);
assertThat(alarm.cancel()).isFalse();
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
index d259851..3443b53 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
@@ -93,7 +93,7 @@
assertThat(sighting.getTimestampNanos())
.isEqualTo(TimeUnit.MILLISECONDS.toNanos(TIME_EPOCH_MILLIS));
assertThat(sighting.toString()).isEqualTo(
- "BleSighting{device=00:11:22:33:44:55,"
+ "BleSighting{device=" + mBluetoothDevice1 + ","
+ " bleRecord=BleRecord [advertiseFlags=-1,"
+ " serviceUuids=[],"
+ " manufacturerSpecificData={}, serviceData={},"
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
index 1b63ad0..f10f66d 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
@@ -63,7 +63,7 @@
assertThat(event.isFailure()).isTrue();
assertThat(event.toString()).isEqualTo(
"Event{eventCode=1120, timestamp=1234, profile=1, "
- + "bluetoothDevice=11:22:33:44:55:66, "
+ + "bluetoothDevice=" + BLUETOOTH_DEVICE + ", "
+ "exception=java.lang.Exception: Test exception}");
Parcel parcel = Parcel.obtain();
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
index 39ea5a9..900b618 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
@@ -19,15 +19,19 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.accounts.Account;
import android.content.Context;
import android.nearby.FastPairDevice;
import com.android.server.nearby.common.bloomfilter.BloomFilter;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
import com.android.server.nearby.provider.FastPairDataProvider;
@@ -40,6 +44,9 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.time.Clock;
+import java.util.List;
+
import service.proto.Cache;
import service.proto.Data;
import service.proto.Rpcs;
@@ -54,20 +61,38 @@
@Mock
private FastPairNotificationManager mFastPairNotificationManager;
@Mock
+ private FastPairCacheManager mFastPairCacheManager;
+ @Mock
+ private FastPairController mFastPairController;
+ @Mock
+ private Data.FastPairDeviceWithAccountKey mFastPairDeviceWithAccountKey;
+ @Mock
private BloomFilter mBloomFilter;
@Mock
- Cache.StoredDiscoveryItem mStoredDiscoveryItem;
- @Mock
Cache.StoredFastPairItem mStoredFastPairItem;
- @Mock
- Data.FastPairDeviceWithAccountKey mFastPairDeviceWithAccountKey;
- private static final byte[] ACCOUNT_KEY = new byte[] {0, 1, 2};
+ @Mock private Clock mClock;
+
+ private final Account mAccount = new Account("test1@gmail.com", "com.google");
+ private static final byte[] ACCOUNT_KEY =
+ new byte[] {4, 65, 90, -26, -5, -38, -128, 40, -103, 101, 95, 55, 8, -42, -120, 78};
+ private static final byte[] ACCOUNT_KEY_2 = new byte[] {0, 1, 2};
private static final String BLUETOOTH_ADDRESS = "AA:BB:CC:DD";
+ private static final String MODEL_ID = "MODEL_ID";
private static final int CLOSE_RSSI = -80;
private static final int FAR_AWAY_RSSI = -120;
private static final int TX_POWER = -70;
private static final byte[] INITIAL_BYTE_ARRAY = new byte[]{0x01, 0x02, 0x03};
+ private static final byte[] SUBSEQUENT_DATA_BYTES = new byte[]{
+ 0, -112, -63, 32, 37, -20, 36, 0, -60, 0, -96, 17, -10, 51, -28, -28, 100};
+ private static final byte[] SUBSEQUENT_DATA_BYTES_INVALID = new byte[]{
+ 0, -112, -63, 32, 37, -20, 48, 0, -60, 0, 90, 17, -10, 51, -28, -28, 100};
private static final byte[] SALT = new byte[]{0x01};
+ private static final Cache.StoredDiscoveryItem STORED_DISCOVERY_ITEM =
+ Cache.StoredDiscoveryItem.newBuilder()
+ .setDeviceName("Device Name")
+ .setTxPower(TX_POWER)
+ .setMacAddress(BLUETOOTH_ADDRESS)
+ .build();
LocatorContextWrapper mLocatorContextWrapper;
FastPairAdvHandler mFastPairAdvHandler;
@@ -83,8 +108,29 @@
mLocatorContextWrapper.getLocator().overrideBindingForTest(
FastPairNotificationManager.class, mFastPairNotificationManager
);
+ mLocatorContextWrapper.getLocator().overrideBindingForTest(
+ FastPairCacheManager.class, mFastPairCacheManager
+ );
+ mLocatorContextWrapper.getLocator().overrideBindingForTest(
+ FastPairController.class, mFastPairController);
+ mLocatorContextWrapper.getLocator().overrideBindingForTest(Clock.class, mClock);
+
when(mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(any()))
.thenReturn(Rpcs.GetObservedDeviceResponse.getDefaultInstance());
+ when(mFastPairDataProvider.loadFastPairEligibleAccounts()).thenReturn(List.of(mAccount));
+ when(mFastPairDataProvider.loadFastPairDeviceWithAccountKey(mAccount))
+ .thenReturn(List.of(mFastPairDeviceWithAccountKey));
+ when(mFastPairDataProvider.loadFastPairDeviceWithAccountKey(eq(mAccount), any()))
+ .thenReturn(List.of(mFastPairDeviceWithAccountKey));
+ when(mFastPairDeviceWithAccountKey.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY));
+ when(mFastPairDeviceWithAccountKey.getDiscoveryItem())
+ .thenReturn(STORED_DISCOVERY_ITEM);
+ when(mStoredFastPairItem.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY_2), ByteString.copyFrom(ACCOUNT_KEY_2));
+ when(mFastPairCacheManager.getAllSavedStoredFastPairItem())
+ .thenReturn(List.of(mStoredFastPairItem));
+
mFastPairAdvHandler = new FastPairAdvHandler(mLocatorContextWrapper, mFastPairDataProvider);
}
@@ -93,6 +139,7 @@
FastPairDevice fastPairDevice = new FastPairDevice.Builder()
.setData(INITIAL_BYTE_ARRAY)
.setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setModelId(MODEL_ID)
.setRssi(CLOSE_RSSI)
.setTxPower(TX_POWER)
.build();
@@ -107,6 +154,7 @@
FastPairDevice fastPairDevice = new FastPairDevice.Builder()
.setData(INITIAL_BYTE_ARRAY)
.setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setModelId(MODEL_ID)
.setRssi(FAR_AWAY_RSSI)
.setTxPower(TX_POWER)
.build();
@@ -117,37 +165,68 @@
}
@Test
- public void testSubsequentBroadcast_notShowHalfSheet() {
- byte[] fastPairRecordWithBloomFilter =
- new byte[]{
- (byte) 0x02,
- (byte) 0x01,
- (byte) 0x02, // Flags
- (byte) 0x02,
- (byte) 0x0A,
- (byte) 0xEB, // Tx Power (-20)
- (byte) 0x0B,
- (byte) 0x16,
- (byte) 0x2C,
- (byte) 0xFE, // FastPair Service Data
- (byte) 0x00, // Flags (model ID length = 3)
- (byte) 0x40, // Account key hash flags (length = 4, type = 0)
- (byte) 0x11,
- (byte) 0x22,
- (byte) 0x33,
- (byte) 0x44, // Account key hash (0x11223344)
- (byte) 0x11, // Account key salt flags (length = 1, type = 1)
- (byte) 0x55, // Account key salt
- };
+ public void testSubsequentBroadcast_showNotification() {
FastPairDevice fastPairDevice = new FastPairDevice.Builder()
- .setData(fastPairRecordWithBloomFilter)
+ .setData(SUBSEQUENT_DATA_BYTES)
.setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setModelId(MODEL_ID)
.setRssi(CLOSE_RSSI)
.setTxPower(TX_POWER)
.build();
-
mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+ DiscoveryItem discoveryItem =
+ new DiscoveryItem(mLocatorContextWrapper, STORED_DISCOVERY_ITEM);
+ verify(mFastPairNotificationManager).showDiscoveryNotification(eq(discoveryItem),
+ eq(ACCOUNT_KEY));
+ verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
+ }
+
+ @Test
+ public void testSubsequentBroadcast_tooFar_notShowNotification() {
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .setData(SUBSEQUENT_DATA_BYTES)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setModelId(MODEL_ID)
+ .setRssi(FAR_AWAY_RSSI)
+ .setTxPower(TX_POWER)
+ .build();
+ mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+
+ verify(mFastPairController, never()).pair(any(), any(), any());
+ verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
+ }
+
+ @Test
+ public void testSubsequentBroadcast_notRecognize_notShowNotification() {
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .setData(SUBSEQUENT_DATA_BYTES_INVALID)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setModelId(MODEL_ID)
+ .setRssi(FAR_AWAY_RSSI)
+ .setTxPower(TX_POWER)
+ .build();
+ mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+
+ verify(mFastPairController, never()).pair(any(), any(), any());
+ verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
+ }
+
+ @Test
+ public void testSubsequentBroadcast_cached_notShowNotification() {
+ when(mStoredFastPairItem.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+
+ FastPairDevice fastPairDevice = new FastPairDevice.Builder()
+ .setData(SUBSEQUENT_DATA_BYTES_INVALID)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setModelId(MODEL_ID)
+ .setRssi(FAR_AWAY_RSSI)
+ .setTxPower(TX_POWER)
+ .build();
+ mFastPairAdvHandler.handleBroadcast(fastPairDevice);
+
+ verify(mFastPairController, never()).pair(any(), any(), any());
verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
}
@@ -190,4 +269,12 @@
assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNotNull();
}
+
+ @Test
+ public void testGenerateBatteryData_correct() {
+ byte[] data = new byte[]
+ {0, -112, 96, 5, -125, 45, 35, 98, 98, 81, 13, 17, 3, 51, -28, -28, -28};
+ assertThat(FastPairAdvHandler.generateBatteryData(data))
+ .isEqualTo(new byte[]{51, -28, -28, -28});
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
index 26d1847..00df1b9 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairManagerTest.java
@@ -17,6 +17,7 @@
package com.android.server.nearby.fastpair;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -54,7 +55,7 @@
public void testFastPairInit() {
mFastPairManager.initiate();
- verify(mContext, times(1)).registerReceiver(any(), any());
+ verify(mContext, times(1)).registerReceiver(any(), any(), anyInt());
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklistTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklistTest.java
new file mode 100644
index 0000000..f3afbe7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetBlocklistTest.java
@@ -0,0 +1,283 @@
+/*
+ * 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.fastpair.halfsheet;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import com.android.server.nearby.fastpair.blocklist.Blocklist;
+import com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState;
+import com.android.server.nearby.fastpair.blocklist.BlocklistElement;
+import com.android.server.nearby.util.DefaultClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairHalfSheetBlocklistTest {
+
+ @Mock
+ private DefaultClock mClock;
+ private FastPairHalfSheetBlocklist mFastPairHalfSheetBlocklist;
+ private static final int SIZE_OF_BLOCKLIST = 2;
+ private static final long CURRENT_TIME = 1000000L;
+ private static final long BLOCKLIST_CANCEL_TIMEOUT_MILLIS = 30000L;
+ private static final long SUPPRESS_ALL_DURATION_MILLIS = 60000L;
+ private static final long DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND = 86400000;
+ private static final long STATE_EXPIRATION_MILLISECOND = 86400000;
+ private static final int HALFSHEET_ID = 1;
+ private static final long DURATION_MILLI_SECONDS_LONG = 86400000;
+ private static final int DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS = 1;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME);
+ mFastPairHalfSheetBlocklist = new FastPairHalfSheetBlocklist(SIZE_OF_BLOCKLIST, mClock);
+ }
+
+ @Test
+ public void testUpdateState() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
+
+ boolean initiallyBlocklisted =
+ mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
+
+ mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID, Blocklist.BlocklistState.ACTIVE);
+ boolean isBlockListedWhenActive =
+ mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
+
+ mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID, Blocklist.BlocklistState.DISMISSED);
+ boolean isBlockListedAfterDismissed =
+ mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
+
+ mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID,
+ Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN);
+ boolean isBlockListedAfterDoNotShowAgain =
+ mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
+
+ mFastPairHalfSheetBlocklist.updateState(HALFSHEET_ID,
+ Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG);
+ boolean isBlockListedAfterDoNotShowAgainLong =
+ mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS);
+
+ assertThat(initiallyBlocklisted).isFalse();
+ assertThat(isBlockListedWhenActive).isFalse();
+ assertThat(isBlockListedAfterDismissed).isTrue();
+ assertThat(isBlockListedAfterDoNotShowAgain).isTrue();
+ assertThat(isBlockListedAfterDoNotShowAgainLong).isTrue();
+ }
+
+ @Test
+ public void testBlocklist_overflow() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DISMISSED, CURRENT_TIME));
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID + 1,
+ new BlocklistElement(Blocklist.BlocklistState.UNKNOWN, CURRENT_TIME));
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID + 2,
+ new BlocklistElement(Blocklist.BlocklistState.UNKNOWN, CURRENT_TIME));
+
+ // blocklist should have evicted HALFSHEET_ID making it no longer blocklisted, this is
+ // because for the test we initialize the size of the blocklist cache to be max = 2
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+
+ @Test
+ public void removeHalfSheetDismissState() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DISMISSED, CURRENT_TIME));
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID)).isTrue();
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID + 1)).isFalse();
+ }
+
+ @Test
+ public void removeHalfSheetBanState() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID)).isTrue();
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID + 1)).isFalse();
+ }
+
+ @Test
+ public void testHalfSheetTimeOutReleaseBan() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ when(mClock.elapsedRealtime())
+ .thenReturn(CURRENT_TIME + BLOCKLIST_CANCEL_TIMEOUT_MILLIS + 1);
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+
+ @Test
+ public void testHalfSheetDoNotShowAgainLong() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(
+ Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG, CURRENT_TIME));
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID)).isTrue();
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ assertThat(mFastPairHalfSheetBlocklist.removeBlocklist(HALFSHEET_ID + 1)).isFalse();
+ }
+
+ @Test
+ public void testHalfSheetDoNotShowAgainLongTimeout() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + DURATION_MILLI_SECONDS_LONG + 1);
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+
+ @Test
+ public void banAllItem_blockHalfSheet() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
+
+ mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
+ when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS - 1);
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ }
+
+ @Test
+ public void banAllItem_invokeAgainWithShorterDurationTime_blockHalfSheet() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
+
+ mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
+ // The 2nd invocation time is shorter than the original one so it's ignored.
+ mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS - 1);
+ when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS - 1);
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ }
+
+ @Test
+ public void banAllItem_releaseHalfSheet() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
+
+ mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
+ when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS);
+
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+
+ @Test
+ public void banAllItem_extendEndTime_blockHalfSheet() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.ACTIVE, CURRENT_TIME));
+
+ mFastPairHalfSheetBlocklist.banAllItem(SUPPRESS_ALL_DURATION_MILLIS);
+ when(mClock.elapsedRealtime()).thenReturn(CURRENT_TIME + SUPPRESS_ALL_DURATION_MILLIS);
+ // Another banAllItem comes so the end time is extended.
+ mFastPairHalfSheetBlocklist.banAllItem(/* banDurationTimeMillis= */ 1);
+
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ }
+
+ @Test
+ public void testHalfSheetTimeOutFirstDismissWithInDuration() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ when(mClock.elapsedRealtime())
+ .thenReturn(CURRENT_TIME + DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND - 1);
+
+ assertThat(
+ mFastPairHalfSheetBlocklist.isBlocklisted(
+ HALFSHEET_ID, (int) DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND))
+ .isTrue();
+ }
+
+ @Test
+ public void testHalfSheetTimeOutFirstDismissOutOfDuration() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ when(mClock.elapsedRealtime())
+ .thenReturn(CURRENT_TIME + DURATION_RESURFACE_DISMISS_HALF_SHEET_MILLISECOND + 1);
+
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+
+ @Test
+ public void testHalfSheetReset() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ mFastPairHalfSheetBlocklist.resetBlockState(HALFSHEET_ID);
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+
+ @Test
+ public void testIsStateExpired() {
+ mFastPairHalfSheetBlocklist.put(
+ HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ when(mClock.elapsedRealtime())
+ .thenReturn(CURRENT_TIME + 1);
+ assertThat(mFastPairHalfSheetBlocklist.isStateExpired(HALFSHEET_ID)).isFalse();
+ when(mClock.elapsedRealtime())
+ .thenReturn(CURRENT_TIME + STATE_EXPIRATION_MILLISECOND + 1);
+ assertThat(mFastPairHalfSheetBlocklist.isStateExpired(HALFSHEET_ID)).isTrue();
+ }
+
+ @Test
+ public void testForceUpdateState() {
+ mFastPairHalfSheetBlocklist.put(HALFSHEET_ID,
+ new BlocklistElement(Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN, CURRENT_TIME));
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isTrue();
+ mFastPairHalfSheetBlocklist.forceUpdateState(HALFSHEET_ID, BlocklistState.ACTIVE);
+ assertThat(mFastPairHalfSheetBlocklist.isBlocklisted(HALFSHEET_ID,
+ DURATION_RESURFACE_HALFSHEET_FIRST_DISMISS_MILLI_SECONDS)).isFalse();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
index b51a295..82b9070 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
@@ -16,14 +16,23 @@
package com.android.server.nearby.fastpair.halfsheet;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.ACTIVE;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DISMISSED;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN;
+import static com.android.server.nearby.fastpair.blocklist.Blocklist.BlocklistState.DO_NOT_SHOW_AGAIN_LONG;
+import static com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager.DISMISS_HALFSHEET_RUNNABLE_NAME;
+import static com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager.SHOW_TOAST_RUNNABLE_NAME;
+
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.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
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 static org.mockito.Mockito.when;
@@ -33,10 +42,14 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.nearby.FastPairStatusCallback;
+import android.nearby.PairStatusMetadata;
import android.os.UserHandle;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.common.eventloop.NamedRunnable;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.FastPairController;
@@ -53,11 +66,22 @@
import service.proto.Cache;
public class FastPairHalfSheetManagerTest {
- private static final String BLEADDRESS = "11:22:44:66";
+ private static final String MODEL_ID = "model_id";
+ private static final String BLE_ADDRESS = "11:22:44:66";
+ private static final String MODEL_ID_1 = "model_id_1";
+ private static final String BLE_ADDRESS_1 = "99:99:99:99";
private static final String NAME = "device_name";
private static final int PASSKEY = 1234;
+ private static final int SUCCESS = 1001;
+ private static final int FAIL = 1002;
+ private static final String EXTRA_HALF_SHEET_CONTENT =
+ "com.android.nearby.halfsheet.HALF_SHEET_CONTENT";
+ private static final String RESULT_FAIL = "RESULT_FAIL";
private FastPairHalfSheetManager mFastPairHalfSheetManager;
private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+ private ResolveInfo mResolveInfo;
+ private List<ResolveInfo> mResolveInfoList;
+ private ApplicationInfo mApplicationInfo;
@Mock private Context mContext;
@Mock
LocatorContextWrapper mContextWrapper;
@@ -67,40 +91,42 @@
Locator mLocator;
@Mock
FastPairController mFastPairController;
+ @Mock
+ EventLoop mEventLoop;
+ @Mock
+ FastPairStatusCallback mFastPairStatusCallback;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
+ mLocator.overrideBindingForTest(EventLoop.class, mEventLoop);
+
+ mResolveInfo = new ResolveInfo();
+ mResolveInfoList = new ArrayList<>();
+ mResolveInfo.activityInfo = new ActivityInfo();
+ mApplicationInfo = new ApplicationInfo();
+ mPackageManager = mock(PackageManager.class);
+
when(mContext.getContentResolver()).thenReturn(
InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+ when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
+ when(mContextWrapper.getLocator()).thenReturn(mLocator);
+ when(mLocator.get(EventLoop.class)).thenReturn(mEventLoop);
+ when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(mResolveInfoList);
+ when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
+
mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
- .setAddress(BLEADDRESS)
+ .setModelId(MODEL_ID)
+ .setAddress(BLE_ADDRESS)
.setDeviceName(NAME)
.build();
}
@Test
public void verifyFastPairHalfSheetManagerBehavior() {
- mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
- ResolveInfo resolveInfo = new ResolveInfo();
- List<ResolveInfo> resolveInfoList = new ArrayList<>();
-
- mPackageManager = mock(PackageManager.class);
- when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
- resolveInfo.activityInfo = new ActivityInfo();
- ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.sourceDir = "/apex/com.android.tethering";
- applicationInfo.packageName = "test.package";
- resolveInfo.activityInfo.applicationInfo = applicationInfo;
- resolveInfoList.add(resolveInfo);
- when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(resolveInfoList);
- when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
-
- mFastPairHalfSheetManager =
- new FastPairHalfSheetManager(mContextWrapper);
-
- when(mContextWrapper.getLocator()).thenReturn(mLocator);
-
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
@@ -111,27 +137,12 @@
@Test
public void verifyFastPairHalfSheetManagerHalfSheetApkNotValidBehavior() {
- mLocator.overrideBindingForTest(FastPairController.class, mFastPairController);
- ResolveInfo resolveInfo = new ResolveInfo();
- List<ResolveInfo> resolveInfoList = new ArrayList<>();
-
- mPackageManager = mock(PackageManager.class);
- when(mContextWrapper.getPackageManager()).thenReturn(mPackageManager);
- resolveInfo.activityInfo = new ActivityInfo();
- ApplicationInfo applicationInfo = new ApplicationInfo();
// application directory is wrong
- applicationInfo.sourceDir = "/apex/com.android.nearby";
- applicationInfo.packageName = "test.package";
- resolveInfo.activityInfo.applicationInfo = applicationInfo;
- resolveInfoList.add(resolveInfo);
- when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(resolveInfoList);
- when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
-
- mFastPairHalfSheetManager =
- new FastPairHalfSheetManager(mContextWrapper);
-
- when(mContextWrapper.getLocator()).thenReturn(mLocator);
-
+ mApplicationInfo.sourceDir = "/apex/com.android.nearby";
+ mApplicationInfo.packageName = "test.package";
+ mResolveInfo.activityInfo.applicationInfo = mApplicationInfo;
+ mResolveInfoList.add(mResolveInfo);
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
@@ -141,22 +152,418 @@
}
@Test
- public void getHalfSheetForegroundState() {
+ public void testHalfSheetForegroundState() {
+ configResolveInfoList();
mFastPairHalfSheetManager =
new FastPairHalfSheetManager(mContextWrapper);
- assertThat(mFastPairHalfSheetManager.getHalfSheetForegroundState()).isTrue();
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ assertThat(mFastPairHalfSheetManager.getHalfSheetForeground()).isTrue();
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+ assertThat(mFastPairHalfSheetManager.getHalfSheetForeground()).isFalse();
}
@Test
public void testEmptyMethods() {
- mFastPairHalfSheetManager =
- new FastPairHalfSheetManager(mContextWrapper);
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
mFastPairHalfSheetManager.destroyBluetoothPairController();
- mFastPairHalfSheetManager.disableDismissRunnable();
- mFastPairHalfSheetManager.notifyPairingProcessDone(true, BLEADDRESS, null);
+ mFastPairHalfSheetManager.notifyPairingProcessDone(true, BLE_ADDRESS, null);
mFastPairHalfSheetManager.showPairingFailed();
mFastPairHalfSheetManager.showPairingHalfSheet(null);
- mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLEADDRESS);
+ mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLE_ADDRESS);
mFastPairHalfSheetManager.showPasskeyConfirmation(null, PASSKEY);
}
+
+ @Test
+ public void showInitialPairingHalfSheetThenDismissOnce_stateDISMISSED() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ //First time dismiss -> state: DISMISSED
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(DISMISSED);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheetThenBan_stateDO_NOT_SHOW_AGAIN() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ //First time ban -> state: DO_NOT_SHOW_AGAIN
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(DO_NOT_SHOW_AGAIN);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheetThenBanTwice_stateDO_NOT_SHOW_AGAIN_LONG() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+ mFastPairHalfSheetManager.dismiss(MODEL_ID);
+
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ //Second time ban -> state: DO_NOT_SHOW_AGAIN
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState())
+ .isEqualTo(DO_NOT_SHOW_AGAIN_LONG);
+ }
+
+ @Test
+ public void testResetBanSate_resetDISMISSEDtoACTIVE() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ mHalfSheetBlocklist.updateState(halfSheetId, DISMISSED);
+ mFastPairHalfSheetManager.resetBanState(MODEL_ID);
+
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
+ }
+
+ @Test
+ public void testResetBanSate_resetDO_NOT_SHOW_AGAINtoACTIVE() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ mHalfSheetBlocklist.updateState(halfSheetId, DO_NOT_SHOW_AGAIN);
+ mFastPairHalfSheetManager.resetBanState(MODEL_ID);
+
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
+ }
+
+ @Test
+ public void testResetBanSate_resetDO_NOT_SHOW_AGAIN_LONGtoACTIVE() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ mHalfSheetBlocklist.updateState(halfSheetId, DO_NOT_SHOW_AGAIN_LONG
+ );
+ mFastPairHalfSheetManager.resetBanState(MODEL_ID);
+
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
+ }
+
+ @Test
+ public void testReportDonePairing() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ assertThat(mFastPairHalfSheetManager.getHalfSheetBlocklist().size()).isEqualTo(1);
+
+ mFastPairHalfSheetManager
+ .reportDonePairing(mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID));
+
+ assertThat(mFastPairHalfSheetManager.getHalfSheetBlocklist().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_AutoDismiss() throws InterruptedException {
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ verifyInitialPairingNameRunnablePostedTimes(1);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_whenUiShownAndItemWithTheSameAddress() {
+ Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setModelId(MODEL_ID)
+ .setAddress(BLE_ADDRESS)
+ .setDeviceName(NAME)
+ .build();
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ verifyHalfSheetActivityIntent(1);
+ verifyInitialPairingNameRunnablePostedTimes(1);
+
+ mFastPairHalfSheetManager.showHalfSheet(testItem);
+ // When half sheet shown and receives broadcast from the same address,
+ // DO NOT request start-activity to avoid unnecessary memory usage,
+ // Just reset the auto dismiss timeout for the new request
+ verifyHalfSheetActivityIntent(1);
+ verifyInitialPairingNameRunnablePostedTimes(2);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_whenUiShowAndItemWithDifferentAddressSameModelId() {
+ Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setModelId(MODEL_ID)
+ .setAddress(BLE_ADDRESS_1)
+ .setDeviceName(NAME)
+ .build();
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ verifyHalfSheetActivityIntent(1);
+ verifyInitialPairingNameRunnablePostedTimes(1);
+
+ mFastPairHalfSheetManager.showHalfSheet(testItem);
+ // When half sheet shown and receives broadcast from the same model id
+ // but with different address, DO NOT rest the auto dismiss timeout. No action is required.
+ verifyHalfSheetActivityIntent(1);
+ verifyInitialPairingNameRunnablePostedTimes(1);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_whenUiShowAndItemWithDifferentModelId() {
+ Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setModelId(MODEL_ID_1)
+ .setAddress(BLE_ADDRESS_1)
+ .setDeviceName(NAME)
+ .build();
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+
+ verifyInitialPairingNameRunnablePostedTimes(1);
+ verifyHalfSheetActivityIntent(1);
+
+ mFastPairHalfSheetManager.showHalfSheet(testItem);
+ // When half sheet shown and receives broadcast from a different model id,
+ // the new request should be ignored. No action is required.
+ verifyHalfSheetActivityIntent(1);
+ verifyInitialPairingNameRunnablePostedTimes(1);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_whenUiNotShownAndIsPairingWithTheSameAddress() {
+ Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setModelId(MODEL_ID)
+ .setAddress(BLE_ADDRESS)
+ .setDeviceName(NAME)
+ .build();
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.setHalfSheetForeground(/* state= */ false);
+ mFastPairHalfSheetManager.setIsActivePairing(true);
+ mFastPairHalfSheetManager.showHalfSheet(testItem);
+
+ // If the half sheet is not in foreground but the system is still pairing the same device,
+ // mark as duplicate request and skip.
+ verifyHalfSheetActivityIntent(1);
+ verifyInitialPairingNameRunnablePostedTimes(1);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_whenUiNotShownAndIsPairingWithADifferentAddress() {
+ Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setModelId(MODEL_ID_1)
+ .setAddress(BLE_ADDRESS_1)
+ .setDeviceName(NAME)
+ .build();
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.setHalfSheetForeground(/* state= */ false);
+ mFastPairHalfSheetManager.setIsActivePairing(true);
+ mFastPairHalfSheetManager.showHalfSheet(testItem);
+
+ // shouldShowHalfSheet
+ verifyHalfSheetActivityIntent(2);
+ verifyInitialPairingNameRunnablePostedTimes(2);
+ }
+
+ @Test
+ public void showInitialPairingHalfSheet_whenUiNotShownAndIsNotPairingWithTheSameAddress() {
+ Cache.ScanFastPairStoreItem testItem = Cache.ScanFastPairStoreItem.newBuilder()
+ .setModelId(MODEL_ID)
+ .setAddress(BLE_ADDRESS)
+ .setDeviceName(NAME)
+ .build();
+ configResolveInfoList();
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.setHalfSheetForeground(/* state= */ false);
+ mFastPairHalfSheetManager.setIsActivePairing(false);
+ mFastPairHalfSheetManager.showHalfSheet(testItem);
+
+ // shouldShowHalfSheet
+ verifyHalfSheetActivityIntent(2);
+ verifyInitialPairingNameRunnablePostedTimes(2);
+ }
+
+ @Test
+ public void testReportActivelyPairing() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+
+ assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
+
+ mFastPairHalfSheetManager.reportActivelyPairing();
+
+ assertThat(mFastPairHalfSheetManager.isActivePairing()).isTrue();
+ }
+
+ @Test
+ public void showPairingSuccessHalfSheetHalfSheetActivityActive_ChangeUIToShowSuccessInfo() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ mFastPairHalfSheetManager.mFastPairUiService
+ .setFastPairStatusCallback(mFastPairStatusCallback);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLE_ADDRESS);
+
+ verifyFastPairStatusCallback(1, SUCCESS);
+ assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
+ }
+
+ @Test
+ public void showPairingSuccessHalfSheetHalfSheetActivityNotActive_showToast() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.setHalfSheetForeground(false);
+ mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLE_ADDRESS);
+
+ ArgumentCaptor<NamedRunnable> captor = ArgumentCaptor.forClass(NamedRunnable.class);
+
+ verify(mEventLoop).postRunnable(captor.capture());
+ assertThat(
+ captor.getAllValues().stream()
+ .filter(r -> r.name.equals(SHOW_TOAST_RUNNABLE_NAME))
+ .count())
+ .isEqualTo(1);
+ assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
+ }
+
+ @Test
+ public void showPairingFailedHalfSheetHalfSheetActivityActive_ChangeUIToShowFailedInfo() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ mFastPairHalfSheetManager.mFastPairUiService
+ .setFastPairStatusCallback(mFastPairStatusCallback);
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.showPairingFailed();
+
+ verifyFastPairStatusCallback(1, FAIL);
+ assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
+ }
+
+ @Test
+ public void showPairingFailedHalfSheetActivityNotActive_StartHalfSheetToShowFailedInfo() {
+ configResolveInfoList();
+ mFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ FastPairHalfSheetBlocklist mHalfSheetBlocklist =
+ mFastPairHalfSheetManager.getHalfSheetBlocklist();
+
+ mFastPairHalfSheetManager.showHalfSheet(mScanFastPairStoreItem);
+ mFastPairHalfSheetManager.setHalfSheetForeground(false);
+ mFastPairHalfSheetManager.showPairingFailed();
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ Integer halfSheetId = mFastPairHalfSheetManager.mModelIdMap.get(MODEL_ID);
+
+ verify(mContextWrapper, times(2))
+ .startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT));
+ assertThat(
+ captor.getAllValues().stream()
+ .filter(r ->
+ r.getStringExtra(EXTRA_HALF_SHEET_CONTENT) != null
+ && r.getStringExtra(EXTRA_HALF_SHEET_CONTENT)
+ .equals(RESULT_FAIL))
+
+ .count())
+ .isEqualTo(1);
+ assertThat(mFastPairHalfSheetManager.isActivePairing()).isFalse();
+ assertThat(mHalfSheetBlocklist.get(halfSheetId).getState()).isEqualTo(ACTIVE);
+ }
+
+ private void verifyInitialPairingNameRunnablePostedTimes(int times) {
+ ArgumentCaptor<NamedRunnable> captor = ArgumentCaptor.forClass(NamedRunnable.class);
+
+ verify(mEventLoop, times(times)).postRunnableDelayed(captor.capture(), anyLong());
+ assertThat(
+ captor.getAllValues().stream()
+ .filter(r -> r.name.equals(DISMISS_HALFSHEET_RUNNABLE_NAME))
+ .count())
+ .isEqualTo(times);
+ }
+
+ private void verifyHalfSheetActivityIntent(int times) {
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+
+ verify(mContextWrapper, times(times))
+ .startActivityAsUser(captor.capture(), eq(UserHandle.CURRENT));
+ assertThat(
+ captor.getAllValues().stream()
+ .filter(r -> r.getAction().equals("android.nearby.SHOW_HALFSHEET"))
+ .count())
+ .isEqualTo(times);
+ }
+
+ private void verifyFastPairStatusCallback(int times, int status) {
+ ArgumentCaptor<PairStatusMetadata> captor =
+ ArgumentCaptor.forClass(PairStatusMetadata.class);
+ verify(mFastPairStatusCallback, times(times)).onPairUpdate(any(), captor.capture());
+ assertThat(
+ captor.getAllValues().stream()
+ .filter(r -> r.getStatus() == status)
+ .count())
+ .isEqualTo(times);
+ }
+
+ private void configResolveInfoList() {
+ mApplicationInfo.sourceDir = "/apex/com.android.tethering";
+ mApplicationInfo.packageName = "test.package";
+ mResolveInfo.activityInfo.applicationInfo = mApplicationInfo;
+ mResolveInfoList.add(mResolveInfo);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
new file mode 100644
index 0000000..d995969
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.fastpair.notification;
+
+import static com.android.server.nearby.fastpair.notification.FastPairNotificationBuilder.NOTIFICATION_OVERRIDE_NAME_EXTRA;
+
+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.when;
+
+import android.app.Notification;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.nearby.halfsheet.R;
+import com.android.server.nearby.fastpair.HalfSheetResources;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FastPairNotificationBuilderTest {
+
+ private static final String STRING_DEVICE = "Devices";
+ private static final String STRING_NEARBY = "Nearby";
+
+ @Mock private Context mContext;
+ @Mock private PackageManager mPackageManager;
+ @Mock private Resources mResources;
+
+ private ResolveInfo mResolveInfo;
+ private List<ResolveInfo> mResolveInfoList;
+ private ApplicationInfo mApplicationInfo;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ HalfSheetResources.setResourcesContextForTest(mContext);
+
+ mResolveInfo = new ResolveInfo();
+ mResolveInfoList = new ArrayList<>();
+ mResolveInfo.activityInfo = new ActivityInfo();
+ mApplicationInfo = new ApplicationInfo();
+
+ when(mContext.getResources()).thenReturn(mResources);
+ when(mContext.getApplicationInfo())
+ .thenReturn(InstrumentationRegistry
+ .getInstrumentation().getContext().getApplicationInfo());
+ when(mContext.getContentResolver()).thenReturn(
+ InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(mResolveInfoList);
+ when(mPackageManager.canRequestPackageInstalls()).thenReturn(false);
+ mApplicationInfo.sourceDir = "/apex/com.android.nearby";
+ mApplicationInfo.packageName = "test.package";
+ mResolveInfo.activityInfo.applicationInfo = mApplicationInfo;
+ mResolveInfoList.add(mResolveInfo);
+
+ when(mResources.getString(eq(R.string.common_devices))).thenReturn(STRING_DEVICE);
+ when(mResources.getString(eq(R.string.common_nearby_title))).thenReturn(STRING_NEARBY);
+ }
+
+ @Test
+ public void setIsDevice_true() {
+ Notification notification =
+ new FastPairNotificationBuilder(mContext, "channelId")
+ .setIsDevice(true).build();
+ assertThat(notification.extras.getString(NOTIFICATION_OVERRIDE_NAME_EXTRA))
+ .isEqualTo(STRING_DEVICE);
+ }
+
+ @Test
+ public void setIsDevice_false() {
+ Notification notification =
+ new FastPairNotificationBuilder(mContext, "channelId")
+ .setIsDevice(false).build();
+ assertThat(notification.extras.getString(NOTIFICATION_OVERRIDE_NAME_EXTRA))
+ .isEqualTo(STRING_NEARBY);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
index 4fb6b37..9670a3f 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
@@ -18,38 +18,62 @@
import static org.mockito.Mockito.when;
+import android.app.NotificationManager;
import android.content.Context;
+import android.content.res.Resources;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import service.proto.Cache;
+
public class FastPairNotificationManagerTest {
- @Mock private Context mContext;
- private static final boolean USE_LARGE_ICON = true;
+ @Mock
+ private Context mContext;
+ @Mock
+ NotificationManager mNotificationManager;
+ @Mock
+ Resources mResources;
+ @Mock
+ private LocatorContextWrapper mLocatorContextWrapper;
+ @Mock
+ private Locator mLocator;
+
private static final int NOTIFICATION_ID = 1;
- private static final String COMPANION_APP = "companionApp";
private static final int BATTERY_LEVEL = 1;
private static final String DEVICE_NAME = "deviceName";
- private static final String ADDRESS = "address";
private FastPairNotificationManager mFastPairNotificationManager;
- private LocatorContextWrapper mLocatorContextWrapper;
+ private DiscoveryItem mItem;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mLocatorContextWrapper = new LocatorContextWrapper(mContext);
when(mContext.getContentResolver()).thenReturn(
InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+ when(mLocatorContextWrapper.getResources()).thenReturn(mResources);
+ when(mLocatorContextWrapper.getLocator()).thenReturn(mLocator);
+ HalfSheetResources.setResourcesContextForTest(mLocatorContextWrapper);
+ // Real context is needed
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ FastPairNotificationManager fastPairNotificationManager =
mFastPairNotificationManager =
- new FastPairNotificationManager(mLocatorContextWrapper, null,
- USE_LARGE_ICON, NOTIFICATION_ID);
+ new FastPairNotificationManager(context, NOTIFICATION_ID, mNotificationManager,
+ new HalfSheetResources(mLocatorContextWrapper));
+ mLocator.overrideBindingForTest(FastPairNotificationManager.class,
+ fastPairNotificationManager);
+
+ mItem = new DiscoveryItem(mLocatorContextWrapper,
+ Cache.StoredDiscoveryItem.newBuilder().setTitle("Device Name").build());
}
@Test
@@ -60,17 +84,18 @@
@Test
public void showConnectingNotification() {
- mFastPairNotificationManager.showConnectingNotification();
+ mFastPairNotificationManager.showConnectingNotification(mItem);
}
@Test
public void showPairingFailedNotification() {
- mFastPairNotificationManager.showPairingFailedNotification(new byte[]{1});
+ mFastPairNotificationManager
+ .showPairingFailedNotification(mItem, new byte[]{1});
}
@Test
public void showPairingSucceededNotification() {
- mFastPairNotificationManager.showPairingSucceededNotification(COMPANION_APP,
- BATTERY_LEVEL, DEVICE_NAME, ADDRESS);
+ mFastPairNotificationManager
+ .showPairingSucceededNotification(mItem, BATTERY_LEVEL, DEVICE_NAME);
}
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationsTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationsTest.java
new file mode 100644
index 0000000..cfebbde
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationsTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.fastpair.notification;
+
+import static com.android.server.nearby.fastpair.notification.FastPairNotificationManager.DEVICES_WITHIN_REACH_CHANNEL_ID;
+
+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.when;
+
+import android.app.Notification;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.nearby.halfsheet.R;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Cache;
+
+public class FastPairNotificationsTest {
+ private static final Cache.StoredDiscoveryItem SCAN_FAST_PAIR_ITEM =
+ Cache.StoredDiscoveryItem.newBuilder()
+ .setDeviceName("TestName")
+ .build();
+ private static final String STRING_DEVICE = "Devices";
+ private static final String STRING_NEARBY = "Nearby";
+ private static final String STRING_YOUR_DEVICE = "Your saved device is available";
+ private static final String STRING_CONNECTING = "Connecting";
+ private static final String STRING_DEVICE_READY = "Device connected";
+
+ private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+ @Mock
+ LocatorContextWrapper mContextWrapper;
+ @Mock
+ Locator mLocator;
+ @Mock
+ private Context mContext;
+ @Mock
+ private Resources mResources;
+ @Mock
+ private Drawable mDrawable;
+
+ private DiscoveryItem mItem;
+ private HalfSheetResources mHalfSheetResources;
+ private FastPairNotifications mFastPairNotifications;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mHalfSheetResources = new HalfSheetResources(mContext);
+ Context realContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mFastPairNotifications =
+ new FastPairNotifications(realContext, mHalfSheetResources);
+ HalfSheetResources.setResourcesContextForTest(mContext);
+
+ when(mContextWrapper.getLocator()).thenReturn(mLocator);
+ when(mContext.getResources()).thenReturn(mResources);
+
+ when(mResources.getString(eq(R.string.common_devices))).thenReturn(STRING_DEVICE);
+ when(mResources.getString(eq(R.string.common_nearby_title))).thenReturn(STRING_NEARBY);
+ when(mResources.getString(eq(R.string.fast_pair_your_device)))
+ .thenReturn(STRING_YOUR_DEVICE);
+ when(mResources.getString(eq(R.string.common_connecting))).thenReturn(STRING_CONNECTING);
+ when(mResources.getString(eq(R.string.fast_pair_device_ready)))
+ .thenReturn(STRING_DEVICE_READY);
+ when(mResources.getDrawable(eq(R.drawable.quantum_ic_devices_other_vd_theme_24), any()))
+ .thenReturn(mDrawable);
+
+ mItem = new DiscoveryItem(mContextWrapper, SCAN_FAST_PAIR_ITEM);
+ }
+
+ @Test
+ public void verify_progressNotification() {
+ Notification notification = mFastPairNotifications.progressNotification(mItem);
+
+ assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
+ assertThat(notification.getSmallIcon().getResId())
+ .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
+ assertThat(notification.category).isEqualTo(Notification.CATEGORY_PROGRESS);
+ assertThat(notification.tickerText.toString()).isEqualTo(STRING_CONNECTING);
+ }
+
+ @Test
+ public void verify_discoveryNotification() {
+ Notification notification =
+ mFastPairNotifications.discoveryNotification(mItem, ACCOUNT_KEY);
+
+ assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
+ assertThat(notification.getSmallIcon().getResId())
+ .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
+ assertThat(notification.category).isEqualTo(Notification.CATEGORY_RECOMMENDATION);
+ }
+
+ @Test
+ public void verify_succeededNotification() {
+ Notification notification = mFastPairNotifications
+ .pairingSucceededNotification(101, null, "model name", mItem);
+
+ assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
+ assertThat(notification.getSmallIcon().getResId())
+ .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
+ assertThat(notification.tickerText.toString()).isEqualTo(STRING_DEVICE_READY);
+ assertThat(notification.category).isEqualTo(Notification.CATEGORY_STATUS);
+ }
+
+ @Test
+ public void verify_failedNotification() {
+ Notification notification =
+ mFastPairNotifications.showPairingFailedNotification(mItem, ACCOUNT_KEY);
+
+ assertThat(notification.getChannelId()).isEqualTo(DEVICES_WITHIN_REACH_CHANNEL_ID);
+ assertThat(notification.getSmallIcon().getResId())
+ .isEqualTo(R.drawable.quantum_ic_devices_other_vd_theme_24);
+ assertThat(notification.category).isEqualTo(Notification.CATEGORY_ERROR);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
index bfe009c..2d496fd 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
@@ -24,6 +24,7 @@
import android.bluetooth.BluetoothDevice;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.eventloop.EventLoop;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -57,6 +58,8 @@
FastPairConnection mFastPairConnection;
@Mock
FootprintsDeviceManager mFootprintsDeviceManager;
+ @Mock
+ EventLoop mEventLoop;
private static final String MAC_ADDRESS = "00:11:22:33:44:55";
private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
@@ -66,6 +69,7 @@
private static HalfSheetPairingProgressHandler sHalfSheetPairingProgressHandler;
private static DiscoveryItem sDiscoveryItem;
private static BluetoothDevice sBluetoothDevice;
+ private static FastPairHalfSheetManager sFastPairHalfSheetManager;
@Before
public void setup() {
@@ -73,10 +77,10 @@
when(mContextWrapper.getLocator()).thenReturn(mLocator);
mLocator.overrideBindingForTest(FastPairCacheManager.class, mFastPairCacheManager);
mLocator.overrideBindingForTest(Clock.class, mClock);
- FastPairHalfSheetManager mfastPairHalfSheetManager =
- new FastPairHalfSheetManager(mContextWrapper);
- mLocator.bind(FastPairHalfSheetManager.class, mfastPairHalfSheetManager);
- when(mLocator.get(FastPairHalfSheetManager.class)).thenReturn(mfastPairHalfSheetManager);
+ sFastPairHalfSheetManager = new FastPairHalfSheetManager(mContextWrapper);
+ mLocator.bind(FastPairHalfSheetManager.class, sFastPairHalfSheetManager);
+ when(mLocator.get(FastPairHalfSheetManager.class)).thenReturn(sFastPairHalfSheetManager);
+ when(mLocator.get(EventLoop.class)).thenReturn(mEventLoop);
sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
sDiscoveryItem.setStoredItemForTest(
sDiscoveryItem.getStoredItemForTest().toBuilder()
@@ -126,6 +130,7 @@
@Test
public void testonPairingStarted() {
sHalfSheetPairingProgressHandler.onPairingStarted();
+ assertThat(sFastPairHalfSheetManager.isActivePairing()).isTrue();
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
index 24f296c..5c61ddb 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
@@ -20,13 +20,18 @@
import static org.mockito.Mockito.when;
+import android.app.NotificationManager;
import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
@@ -62,6 +67,10 @@
FootprintsDeviceManager mFootprintsDeviceManager;
@Mock
android.bluetooth.BluetoothManager mBluetoothManager;
+ @Mock
+ NotificationManager mNotificationManager;
+ @Mock
+ Resources mResources;
private static final String MAC_ADDRESS = "00:11:22:33:44:55";
private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
@@ -76,6 +85,9 @@
when(mContextWrapper.getSystemService(BluetoothManager.class))
.thenReturn(mBluetoothManager);
when(mContextWrapper.getLocator()).thenReturn(mLocator);
+ when(mContextWrapper.getResources()).thenReturn(mResources);
+ HalfSheetResources.setResourcesContextForTest(mContextWrapper);
+
mLocator.overrideBindingForTest(FastPairCacheManager.class,
mFastPairCacheManager);
mLocator.overrideBindingForTest(Clock.class, mClock);
@@ -126,11 +138,17 @@
private NotificationPairingProgressHandler createProgressHandler(
@Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
- FastPairNotificationManager fastPairNotificationManager =
- new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
FastPairHalfSheetManager fastPairHalfSheetManager =
new FastPairHalfSheetManager(mContextWrapper);
+ // Real context is needed
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ FastPairNotificationManager fastPairNotificationManager =
+ new FastPairNotificationManager(context, 1234, mNotificationManager,
+ new HalfSheetResources(mContextWrapper));
mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
+ mLocator.overrideBindingForTest(FastPairNotificationManager.class,
+ fastPairNotificationManager);
+
NotificationPairingProgressHandler mNotificationPairingProgressHandler =
new NotificationPairingProgressHandler(
mContextWrapper,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
index a3eb50c..6d769df 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
@@ -20,16 +20,21 @@
import static org.mockito.Mockito.when;
+import android.app.NotificationManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
@@ -64,6 +69,10 @@
FastPairConnection mFastPairConnection;
@Mock
BluetoothManager mBluetoothManager;
+ @Mock
+ Resources mResources;
+ @Mock
+ NotificationManager mNotificationManager;
private static final String MAC_ADDRESS = "00:11:22:33:44:55";
private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
@@ -81,6 +90,9 @@
mLocator.overrideBindingForTest(FastPairCacheManager.class,
mFastPairCacheManager);
mLocator.overrideBindingForTest(Clock.class, mClock);
+ when(mContextWrapper.getResources()).thenReturn(mResources);
+ HalfSheetResources.setResourcesContextForTest(mContextWrapper);
+
sBluetoothDevice =
BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
@@ -205,10 +217,17 @@
private PairingProgressHandlerBase createProgressHandler(
@Nullable byte[] accountKey, DiscoveryItem fastPairItem, boolean isRetroactivePair) {
- FastPairNotificationManager fastPairNotificationManager =
- new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
+
FastPairHalfSheetManager fastPairHalfSheetManager =
new FastPairHalfSheetManager(mContextWrapper);
+ // Real context is needed
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ FastPairNotificationManager fastPairNotificationManager =
+ new FastPairNotificationManager(context, 1234, mNotificationManager,
+ new HalfSheetResources(mContextWrapper));
+
+ mLocator.overrideBindingForTest(FastPairNotificationManager.class,
+ fastPairNotificationManager);
mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
PairingProgressHandlerBase pairingProgressHandlerBase =
PairingProgressHandlerBase.create(
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
index 542ceed..7ecf631 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -23,6 +23,7 @@
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;
@@ -48,10 +49,8 @@
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.Executor;
import java.util.concurrent.Executors;
public class DiscoveryProviderManagerTest {
@@ -80,12 +79,9 @@
@Mock
CallerIdentity mCallerIdentity;
@Mock
- DiscoveryProviderManager.ScanListenerDeathRecipient mScanListenerDeathRecipient;
- @Mock
IBinder mIBinder;
+ private Executor mExecutor;
private DiscoveryProviderManager mDiscoveryProviderManager;
- private Map<IBinder, DiscoveryProviderManager.ScanListenerRecord>
- mScanTypeScanListenerRecordMap;
private static PresenceScanFilter getPresenceScanFilter() {
final byte[] secretId = new byte[]{1, 2, 3, 4};
@@ -133,16 +129,16 @@
@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);
- mScanTypeScanListenerRecordMap = new HashMap<>();
mDiscoveryProviderManager =
- new DiscoveryProviderManager(mContext, mInjector,
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
mBleDiscoveryProvider,
- mChreDiscoveryProvider,
- mScanTypeScanListenerRecordMap);
+ mChreDiscoveryProvider);
mCallerIdentity = CallerIdentity
.forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
}
@@ -162,55 +158,45 @@
@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();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ 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())
- .addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ 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();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isFalse();
}
@@ -218,17 +204,14 @@
@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();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isTrue();
}
@@ -236,17 +219,14 @@
@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();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, atLeastOnce()).start();
assertThat(start).isTrue();
}
@@ -254,17 +234,14 @@
@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();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, never()).start();
assertThat(start).isNull();
}
@@ -272,17 +249,14 @@
@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();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
- Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ Boolean start = mDiscoveryProviderManager.startProviders();
verify(mBluetoothController, atLeastOnce()).start();
assertThat(start).isTrue();
}
@@ -291,22 +265,16 @@
public void test_stopChreProvider_clearFilters() throws Exception {
// Cannot use mocked ChreDiscoveryProvider,
// so we cannot use class variable mDiscoveryProviderManager
- ExecutorService executor = Executors.newSingleThreadExecutor();
DiscoveryProviderManager manager =
- new DiscoveryProviderManager(mContext, mInjector,
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
mBleDiscoveryProvider,
new ChreDiscoveryProvider(
mContext,
- new ChreCommunication(mInjector, mContext, executor), executor),
- mScanTypeScanListenerRecordMap);
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(
- scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ 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.
@@ -328,21 +296,17 @@
public void test_restartChreProvider() throws Exception {
// Cannot use mocked ChreDiscoveryProvider,
// so we cannot use class variable mDiscoveryProviderManager
- ExecutorService executor = Executors.newSingleThreadExecutor();
DiscoveryProviderManager manager =
- new DiscoveryProviderManager(mContext, mInjector,
+ new DiscoveryProviderManager(mContext, mExecutor, mInjector,
mBleDiscoveryProvider,
new ChreDiscoveryProvider(
mContext,
- new ChreCommunication(mInjector, mContext, executor), executor),
- mScanTypeScanListenerRecordMap);
+ new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
ScanRequest scanRequest = new ScanRequest.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
.addScanFilter(getPresenceScanFilter()).build();
- DiscoveryProviderManager.ScanListenerRecord record =
- new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
- mCallerIdentity, mScanListenerDeathRecipient);
- mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ 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.
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/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index ebb897e..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,7 @@
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;
@@ -56,6 +57,8 @@
private BleDiscoveryProvider mBleDiscoveryProvider;
@Mock
private AbstractDiscoveryProvider.Listener mListener;
+// @Mock
+// private BluetoothAdapter mBluetoothAdapter;
@Before
public void setup() {
@@ -68,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()
@@ -77,7 +80,18 @@
// Wait for callback to be invoked
Thread.sleep(500);
verify(mListener, times(1)).onNearbyDeviceDiscovered(any());
+ }
+
+ @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
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 a863619..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
@@ -20,6 +20,7 @@
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;
@@ -35,8 +36,10 @@
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
+import android.os.Build;
import android.provider.DeviceConfig;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.server.nearby.NearbyConfiguration;
@@ -55,6 +58,7 @@
public class ChreCommunicationTest {
private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+ private static final int APP_VERSION = 1;
@Mock Injector mInjector;
@Mock Context mContext;
@@ -85,7 +89,10 @@
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, mContext, new InlineExecutor());
}
@@ -122,6 +129,19 @@
}
@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));
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 83fec44..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
@@ -25,11 +25,17 @@
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.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;
@@ -56,6 +62,7 @@
public class ChreDiscoveryProviderTest {
@Mock AbstractDiscoveryProvider.Listener mListener;
@Mock ChreCommunication mChreCommunication;
+ @Mock IBinder mIBinder;
@Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
@Captor ArgumentCaptor<NearbyDeviceParcelable> mNearbyDevice;
@@ -70,7 +77,9 @@
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() {
@@ -92,6 +101,64 @@
}
@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
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testOnNearbyDeviceDiscovered() {
Blefilter.PublicCredential credential =
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
index 0fe28df..a759baf 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -59,4 +59,22 @@
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/DataUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
index a742b3d..ac90b9f 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
@@ -30,6 +30,7 @@
public final class DataUtilsTest {
private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final String MODEL_ID = "model_id";
private static final String APP_PACKAGE = "test_package";
private static final String APP_ACTION_URL =
"intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
@@ -61,7 +62,7 @@
@Test
public void test_toScanFastPairStoreItem_withAccount() {
Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
- createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+ createObservedDeviceResponse(), BLUETOOTH_ADDRESS, MODEL_ID, ACCOUNT);
assertThat(item.getAddress()).isEqualTo(BLUETOOTH_ADDRESS);
assertThat(item.getActionUrl()).isEqualTo(APP_ACTION_URL);
assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
@@ -93,7 +94,7 @@
@Test
public void test_toScanFastPairStoreItem_withoutAccount() {
Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
- createObservedDeviceResponse(), BLUETOOTH_ADDRESS, /* account= */ null);
+ createObservedDeviceResponse(), BLUETOOTH_ADDRESS, MODEL_ID, /* account= */ null);
FastPairStrings strings = item.getFastPairStrings();
assertThat(strings.getInitialPairingDescription())
.isEqualTo(MESSAGE_INIT_NOTIFY_DESCRIPTION_NO_ACCOUNT);
@@ -102,7 +103,7 @@
@Test
public void test_toString() {
Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
- createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+ createObservedDeviceResponse(), BLUETOOTH_ADDRESS, MODEL_ID, ACCOUNT);
assertThat(DataUtils.toString(item))
.isEqualTo("ScanFastPairStoreItem=[address:00:11:22:33:FF:EE, "
diff --git a/netd/Android.bp b/netd/Android.bp
index 473460d..4325d89 100644
--- a/netd/Android.bp
+++ b/netd/Android.bp
@@ -35,6 +35,9 @@
"BpfHandler.cpp",
"NetdUpdatable.cpp",
],
+ static_libs: [
+ "libmodules-utils-build",
+ ],
shared_libs: [
"libbase",
"liblog",
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 8081d12..6409374 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -21,6 +21,7 @@
#include <linux/bpf.h>
#include <android-base/unique_fd.h>
+#include <android-modules-utils/sdk_level.h>
#include <bpf/WaitForProgsLoaded.h>
#include <log/log.h>
#include <netdutils/UidConstants.h>
@@ -74,9 +75,11 @@
}
static Status initPrograms(const char* cg2_path) {
+ if (modules::sdklevel::IsAtLeastU() && !!strcmp(cg2_path, "/sys/fs/cgroup")) abort();
+
unique_fd cg_fd(open(cg2_path, O_DIRECTORY | O_RDONLY | O_CLOEXEC));
if (cg_fd == -1) {
- int ret = errno;
+ const int ret = errno;
ALOGE("Failed to open the cgroup directory: %s", strerror(ret));
return statusFromErrno(ret, "Open the cgroup directory failed");
}
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/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index f40d388..0dfd0af 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -30,6 +30,7 @@
],
shared_libs: [
"libbase",
+ "libcutils",
"liblog",
],
static_libs: [
@@ -81,6 +82,7 @@
shared_libs: [
"libbase",
"liblog",
+ "libcutils",
"libandroid_net",
],
compile_multilib: "both",
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
index 6aa0fb4..c5f9631 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -149,6 +149,18 @@
if (mIsTest) return; // Don't touch non-hermetic bpf in test.
if (mStarted) sPoller.Stop();
mStarted = false;
+
+ // Although this shouldn't be required, there seems to be some cases when we
+ // don't fill enough of a Perfetto Chunk for Perfetto to automatically commit
+ // the traced data. This manually flushes OnStop so we commit at least once.
+ NetworkTraceHandler::Trace([&](NetworkTraceHandler::TraceContext ctx) {
+ perfetto::LockedHandle<NetworkTraceHandler> handle =
+ ctx.GetDataSourceLocked();
+ // Trace is called for all active handlers, only flush our context. Since
+ // handle doesn't have a `.get()`, use `*` and `&` to get what it points to.
+ if (&(*handle) != this) return;
+ ctx.Flush();
+ });
}
void NetworkTraceHandler::Write(const std::vector<PacketTrace>& packets,
diff --git a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
index 3de9897..d538368 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTracePoller.cpp
@@ -15,10 +15,12 @@
*/
#define LOG_TAG "NetworkTrace"
+#define ATRACE_TAG ATRACE_TAG_NETWORK
#include "netdbpf/NetworkTracePoller.h"
#include <bpf/BpfUtils.h>
+#include <cutils/trace.h>
#include <log/log.h>
#include <perfetto/tracing/platform.h>
#include <perfetto/tracing/tracing.h>
@@ -133,6 +135,8 @@
return false;
}
+ ATRACE_INT("NetworkTracePackets", packets.size());
+
mCallback(packets);
return true;
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 1d99190..47a1022 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -19,7 +19,7 @@
import static android.net.ConnectivityManager.NETID_UNSET;
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
-import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -55,6 +55,7 @@
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
@@ -72,7 +73,6 @@
import com.android.server.connectivity.mdns.MdnsSearchOptions;
import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
import com.android.server.connectivity.mdns.MdnsServiceInfo;
-import com.android.server.connectivity.mdns.MdnsSocketClientBase;
import com.android.server.connectivity.mdns.MdnsSocketProvider;
import com.android.server.connectivity.mdns.util.MdnsUtils;
@@ -84,6 +84,7 @@
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -143,6 +144,7 @@
public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final long CLEANUP_DELAY_MS = 10000;
private static final int IFACE_IDX_ANY = 0;
+ private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
private final Context mContext;
private final NsdStateMachine mNsdStateMachine;
@@ -158,7 +160,7 @@
private final MdnsSocketProvider mMdnsSocketProvider;
@NonNull
private final MdnsAdvertiser mAdvertiser;
- private final SharedLog mServiceLogs = new SharedLog(TAG);
+ private final SharedLog mServiceLogs = LOGGER.forSubComponent(TAG);
// WARNING : Accessing these values in any thread is not safe, it must only be changed in the
// state machine thread. If change this outside state machine, it will need to introduce
// synchronization.
@@ -242,14 +244,14 @@
public void onServiceNameDiscovered(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_FOUND,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
@Override
public void onServiceNameRemoved(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_LOST,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
}
@@ -264,7 +266,7 @@
public void onServiceFound(MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.RESOLVE_SERVICE_SUCCEEDED,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
}
@@ -279,21 +281,21 @@
public void onServiceFound(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_UPDATED,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
@Override
public void onServiceUpdated(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_UPDATED,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
@Override
public void onServiceRemoved(@NonNull MdnsServiceInfo serviceInfo) {
mNsdStateMachine.sendMessage(MDNS_DISCOVERY_MANAGER_EVENT, mTransactionId,
NsdManager.SERVICE_UPDATED_LOST,
- new MdnsEvent(mClientId, mReqServiceInfo.getServiceType(), serviceInfo));
+ new MdnsEvent(mClientId, serviceInfo));
}
}
@@ -303,14 +305,10 @@
private static class MdnsEvent {
final int mClientId;
@NonNull
- final String mRequestedServiceType;
- @NonNull
final MdnsServiceInfo mMdnsServiceInfo;
- MdnsEvent(int clientId, @NonNull String requestedServiceType,
- @NonNull MdnsServiceInfo mdnsServiceInfo) {
+ MdnsEvent(int clientId, @NonNull MdnsServiceInfo mdnsServiceInfo) {
mClientId = clientId;
- mRequestedServiceType = requestedServiceType;
mMdnsServiceInfo = mdnsServiceInfo;
}
}
@@ -601,7 +599,10 @@
final NsdServiceInfo info = args.serviceInfo;
id = getUniqueId();
- final String serviceType = constructServiceType(info.getServiceType());
+ final Pair<String, String> typeAndSubtype =
+ parseTypeAndSubtype(info.getServiceType());
+ final String serviceType = typeAndSubtype == null
+ ? null : typeAndSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
@@ -615,12 +616,18 @@
maybeStartMonitoringSockets();
final MdnsListener listener =
new DiscoveryListener(clientId, id, info, listenServiceType);
- final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
- .setNetwork(info.getNetwork())
- .setIsPassiveMode(true)
- .build();
+ final MdnsSearchOptions.Builder optionsBuilder =
+ MdnsSearchOptions.newBuilder()
+ .setNetwork(info.getNetwork())
+ .setRemoveExpiredService(true)
+ .setIsPassiveMode(true);
+ if (typeAndSubtype.second != null) {
+ // The parsing ensures subtype starts with an underscore.
+ // MdnsSearchOptions expects the underscore to not be present.
+ optionsBuilder.addSubtype(typeAndSubtype.second.substring(1));
+ }
mMdnsDiscoveryManager.registerListener(
- listenServiceType, listener, options);
+ listenServiceType, listener, optionsBuilder.build());
storeDiscoveryManagerRequestMap(clientId, id, listener, clientInfo);
clientInfo.onDiscoverServicesStarted(clientId, info);
clientInfo.log("Register a DiscoveryListener " + id
@@ -699,7 +706,9 @@
id = getUniqueId();
final NsdServiceInfo serviceInfo = args.serviceInfo;
final String serviceType = serviceInfo.getServiceType();
- final String registerServiceType = constructServiceType(serviceType);
+ final Pair<String, String> typeSubtype = parseTypeAndSubtype(serviceType);
+ final String registerServiceType = typeSubtype == null
+ ? null : typeSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsAdvertiserEnabled(mContext)
|| useAdvertiserForType(registerServiceType)) {
@@ -714,7 +723,11 @@
serviceInfo.getServiceName()));
maybeStartMonitoringSockets();
- mAdvertiser.addService(id, serviceInfo);
+ // TODO: pass in the subtype as well. Including the subtype in the
+ // service type would generate service instance names like
+ // Name._subtype._sub._type._tcp, which is incorrect
+ // (it should be Name._type._tcp).
+ mAdvertiser.addService(id, serviceInfo, typeSubtype.second);
storeAdvertiserRequestMap(clientId, id, clientInfo);
} else {
maybeStartDaemon();
@@ -780,7 +793,10 @@
final NsdServiceInfo info = args.serviceInfo;
id = getUniqueId();
- final String serviceType = constructServiceType(info.getServiceType());
+ final Pair<String, String> typeSubtype =
+ parseTypeAndSubtype(info.getServiceType());
+ final String serviceType = typeSubtype == null
+ ? null : typeSubtype.first;
if (clientInfo.mUseJavaBackend
|| mDeps.isMdnsDiscoveryManagerEnabled(mContext)
|| useDiscoveryManagerForType(serviceType)) {
@@ -798,6 +814,7 @@
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
.setResolveInstanceName(info.getServiceName())
+ .setRemoveExpiredService(true)
.build();
mMdnsDiscoveryManager.registerListener(
resolveServiceType, listener, options);
@@ -873,7 +890,10 @@
final NsdServiceInfo info = args.serviceInfo;
id = getUniqueId();
- final String serviceType = constructServiceType(info.getServiceType());
+ final Pair<String, String> typeAndSubtype =
+ parseTypeAndSubtype(info.getServiceType());
+ final String serviceType = typeAndSubtype == null
+ ? null : typeAndSubtype.first;
if (serviceType == null) {
clientInfo.onServiceInfoCallbackRegistrationFailed(clientId,
NsdManager.FAILURE_BAD_PARAMETERS);
@@ -888,6 +908,7 @@
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
.setResolveInstanceName(info.getServiceName())
+ .setRemoveExpiredService(true)
.build();
mMdnsDiscoveryManager.registerListener(
resolveServiceType, listener, options);
@@ -1104,9 +1125,38 @@
return true;
}
- private NsdServiceInfo buildNsdServiceInfoFromMdnsEvent(final MdnsEvent event) {
+ @Nullable
+ private NsdServiceInfo buildNsdServiceInfoFromMdnsEvent(
+ final MdnsEvent event, int code) {
final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo;
- final String serviceType = event.mRequestedServiceType;
+ final String[] typeArray = serviceInfo.getServiceType();
+ final String joinedType;
+ if (typeArray.length == 0
+ || !typeArray[typeArray.length - 1].equals(LOCAL_DOMAIN_NAME)) {
+ Log.wtf(TAG, "MdnsServiceInfo type does not end in .local: "
+ + Arrays.toString(typeArray));
+ return null;
+ } else {
+ joinedType = TextUtils.join(".",
+ Arrays.copyOfRange(typeArray, 0, typeArray.length - 1));
+ }
+ final String serviceType;
+ switch (code) {
+ case NsdManager.SERVICE_FOUND:
+ case NsdManager.SERVICE_LOST:
+ // For consistency with historical behavior, discovered service types have
+ // a dot at the end.
+ serviceType = joinedType + ".";
+ break;
+ case RESOLVE_SERVICE_SUCCEEDED:
+ // For consistency with historical behavior, resolved service types have
+ // a dot at the beginning.
+ serviceType = "." + joinedType;
+ break;
+ default:
+ serviceType = joinedType;
+ break;
+ }
final String serviceName = serviceInfo.getServiceInstanceName();
final NsdServiceInfo servInfo = new NsdServiceInfo(serviceName, serviceType);
final Network network = serviceInfo.getNetwork();
@@ -1131,7 +1181,9 @@
final MdnsEvent event = (MdnsEvent) obj;
final int clientId = event.mClientId;
- final NsdServiceInfo info = buildNsdServiceInfoFromMdnsEvent(event);
+ final NsdServiceInfo info = buildNsdServiceInfoFromMdnsEvent(event, code);
+ // Errors are already logged if null
+ if (info == null) return false;
if (DBG) {
Log.d(TAG, String.format("MdnsDiscoveryManager event code=%s transactionId=%d",
NsdManager.nameOf(code), transactionId));
@@ -1150,8 +1202,6 @@
break;
}
final MdnsServiceInfo serviceInfo = event.mMdnsServiceInfo;
- // Add '.' in front of the service type that aligns with historical behavior
- info.setServiceType("." + event.mRequestedServiceType);
info.setPort(serviceInfo.getPort());
Map<String, String> attrs = serviceInfo.getAttributes();
@@ -1288,28 +1338,39 @@
* Check the given service type is valid and construct it to a service type
* which can use for discovery / resolution service.
*
- * <p> The valid service type should be 2 labels, or 3 labels if the query is for a
+ * <p>The valid service type should be 2 labels, or 3 labels if the query is for a
* subtype (see RFC6763 7.1). Each label is up to 63 characters and must start with an
* underscore; they are alphanumerical characters or dashes or underscore, except the
* last one that is just alphanumerical. The last label must be _tcp or _udp.
*
+ * <p>The subtype may also be specified with a comma after the service type, for example
+ * _type._tcp,_subtype.
+ *
* @param serviceType the request service type for discovery / resolution service
* @return constructed service type or null if the given service type is invalid.
*/
@Nullable
- public static String constructServiceType(String serviceType) {
+ public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
+ final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
final Pattern serviceTypePattern = Pattern.compile(
- "^(_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]\\.)?"
- + "(_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]\\._(?:tcp|udp))"
+ // Optional leading subtype (_subtype._type._tcp)
+ // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+ "^(?:(" + typeOrSubtypePattern + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
// Drop '.' at the end of service type that is compatible with old backend.
- + "\\.?$");
+ // e.g. allow "_type._tcp.local."
+ + "\\.?"
+ // Optional subtype after comma, for "_type._tcp,_subtype" format
+ + "(?:,(" + typeOrSubtypePattern + "))?"
+ + "$");
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
- return matcher.group(1) == null
- ? matcher.group(2)
- : matcher.group(1) + "_sub." + matcher.group(2);
+ // Use the subtype either at the beginning or after the comma
+ final String subtype = matcher.group(1) != null ? matcher.group(1) : matcher.group(3);
+ return new Pair<>(matcher.group(2), subtype);
}
@VisibleForTesting
@@ -1327,17 +1388,18 @@
mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
mDeps = deps;
- mMdnsSocketProvider = deps.makeMdnsSocketProvider(ctx, handler.getLooper());
+ mMdnsSocketProvider = deps.makeMdnsSocketProvider(ctx, handler.getLooper(),
+ LOGGER.forSubComponent("MdnsSocketProvider"));
// Netlink monitor starts on boot, and intentionally never stopped, to ensure that all
// address events are received.
handler.post(mMdnsSocketProvider::startNetLinkMonitor);
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider);
- mMdnsDiscoveryManager =
- deps.makeMdnsDiscoveryManager(new ExecutorProvider(), mMdnsSocketClient);
+ mMdnsDiscoveryManager = deps.makeMdnsDiscoveryManager(new ExecutorProvider(),
+ mMdnsSocketClient, LOGGER.forSubComponent("MdnsDiscoveryManager"));
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
- new AdvertiserCallback());
+ new AdvertiserCallback(), LOGGER.forSubComponent("MdnsAdvertiser"));
}
/**
@@ -1352,8 +1414,8 @@
* @return true if the MdnsDiscoveryManager feature is enabled.
*/
public boolean isMdnsDiscoveryManagerEnabled(Context context) {
- return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context,
- NAMESPACE_CONNECTIVITY, MDNS_DISCOVERY_MANAGER_VERSION,
+ return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
+ MDNS_DISCOVERY_MANAGER_VERSION, DeviceConfigUtils.TETHERING_MODULE_NAME,
false /* defaultEnabled */);
}
@@ -1364,8 +1426,9 @@
* @return true if the MdnsAdvertiser feature is enabled.
*/
public boolean isMdnsAdvertiserEnabled(Context context) {
- return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context,
- NAMESPACE_CONNECTIVITY, MDNS_ADVERTISER_VERSION, false /* defaultEnabled */);
+ return isAtLeastU() || DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING,
+ MDNS_ADVERTISER_VERSION, DeviceConfigUtils.TETHERING_MODULE_NAME,
+ false /* defaultEnabled */);
}
/**
@@ -1390,8 +1453,9 @@
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
- ExecutorProvider executorProvider, MdnsSocketClientBase socketClient) {
- return new MdnsDiscoveryManager(executorProvider, socketClient);
+ @NonNull ExecutorProvider executorProvider,
+ @NonNull MdnsMultinetworkSocketClient socketClient, @NonNull SharedLog sharedLog) {
+ return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
}
/**
@@ -1399,15 +1463,16 @@
*/
public MdnsAdvertiser makeMdnsAdvertiser(
@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
- @NonNull MdnsAdvertiser.AdvertiserCallback cb) {
- return new MdnsAdvertiser(looper, socketProvider, cb);
+ @NonNull MdnsAdvertiser.AdvertiserCallback cb, @NonNull SharedLog sharedLog) {
+ return new MdnsAdvertiser(looper, socketProvider, cb, sharedLog);
}
/**
* @see MdnsSocketProvider
*/
- public MdnsSocketProvider makeMdnsSocketProvider(Context context, Looper looper) {
- return new MdnsSocketProvider(context, looper);
+ public MdnsSocketProvider makeMdnsSocketProvider(@NonNull Context context,
+ @NonNull Looper looper, @NonNull SharedLog sharedLog) {
+ return new MdnsSocketProvider(context, looper, sharedLog);
}
}
@@ -1769,30 +1834,10 @@
// Dump service and clients logs
pw.println();
+ pw.println("Logs:");
pw.increaseIndent();
mServiceLogs.reverseDump(pw);
pw.decreaseIndent();
-
- // Dump advertiser related logs
- pw.println();
- pw.println("Advertiser:");
- pw.increaseIndent();
- mAdvertiser.dump(pw);
- pw.decreaseIndent();
-
- // Dump discoverymanager related logs
- pw.println();
- pw.println("DiscoveryManager:");
- pw.increaseIndent();
- mMdnsDiscoveryManager.dump(pw);
- pw.decreaseIndent();
-
- // Dump socketprovider related logs
- pw.println();
- pw.println("SocketProvider:");
- pw.increaseIndent();
- mMdnsSocketProvider.dump(pw);
- pw.decreaseIndent();
}
private abstract static class ClientRequest {
diff --git a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
index 9a67007..84faf12 100644
--- a/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
+++ b/service-t/src/com/android/server/connectivity/mdns/EnqueueMdnsQueryCallable.java
@@ -24,6 +24,7 @@
import android.util.Pair;
import com.android.server.connectivity.mdns.util.MdnsLogger;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.lang.ref.WeakReference;
@@ -75,6 +76,8 @@
private final boolean sendDiscoveryQueries;
@NonNull
private final List<MdnsResponse> servicesToResolve;
+ @NonNull
+ private final MdnsResponseDecoder.Clock clock;
EnqueueMdnsQueryCallable(
@NonNull MdnsSocketClientBase requestSender,
@@ -85,7 +88,8 @@
int transactionId,
@Nullable Network network,
boolean sendDiscoveryQueries,
- @NonNull Collection<MdnsResponse> servicesToResolve) {
+ @NonNull Collection<MdnsResponse> servicesToResolve,
+ @NonNull MdnsResponseDecoder.Clock clock) {
weakRequestSender = new WeakReference<>(requestSender);
this.packetWriter = packetWriter;
serviceTypeLabels = TextUtils.split(serviceType, "\\.");
@@ -95,6 +99,7 @@
this.network = network;
this.sendDiscoveryQueries = sendDiscoveryQueries;
this.servicesToResolve = new ArrayList<>(servicesToResolve);
+ this.clock = clock;
}
// Incompatible return type for override of Callable#call().
@@ -119,22 +124,24 @@
// List of (name, type) to query
final ArrayList<Pair<String[], Integer>> missingKnownAnswerRecords = new ArrayList<>();
+ final long now = clock.elapsedRealtime();
for (MdnsResponse response : servicesToResolve) {
- // TODO: also send queries to renew record TTL (as per RFC6762 7.1 no need to query
- // if remaining TTL is more than half the original one, so send the queries if half
- // the TTL has passed).
- if (response.isComplete()) continue;
final String[] serviceName = response.getServiceName();
if (serviceName == null) continue;
- if (!response.hasTextRecord()) {
+ if (!response.hasTextRecord() || MdnsUtils.isRecordRenewalNeeded(
+ response.getTextRecord(), now)) {
missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_TXT));
}
- if (!response.hasServiceRecord()) {
+ if (!response.hasServiceRecord() || MdnsUtils.isRecordRenewalNeeded(
+ response.getServiceRecord(), now)) {
missingKnownAnswerRecords.add(new Pair<>(serviceName, MdnsRecord.TYPE_SRV));
// The hostname is not yet known, so queries for address records will be sent
// the next time the EnqueueMdnsQueryCallable is enqueued if the reply does not
// contain them. In practice, advertisers should include the address records
// when queried for SRV, although it's not a MUST requirement (RFC6763 12.2).
+ // TODO: Figure out how to renew the A/AAAA record. Usually A/AAAA record will
+ // be included in the response to the SRV record so in high chances there is
+ // no need to renew them individually.
} else if (!response.hasInet4AddressRecord() && !response.hasInet6AddressRecord()) {
final String[] host = response.getServiceRecord().getServiceHost();
missingKnownAnswerRecords.add(new Pair<>(host, MdnsRecord.TYPE_A));
@@ -211,7 +218,7 @@
| (expectUnicastResponse ? MdnsConstants.QCLASS_UNICAST : 0));
}
- private void sendPacketTo(MdnsSocketClientBase requestSender, InetSocketAddress address)
+ private void sendPacketTo(MdnsSocketClient requestSender, InetSocketAddress address)
throws IOException {
DatagramPacket packet = packetWriter.getPacket(address);
if (expectUnicastResponse) {
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 a332da7..cc08ea1 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -33,7 +33,6 @@
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.util.MdnsUtils;
-import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -51,7 +50,7 @@
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
- private static final SharedLog LOGGER = new SharedLog(TAG);
+
private final Looper mLooper;
private final AdvertiserCallback mCb;
@@ -71,6 +70,7 @@
private final Dependencies mDeps;
private String[] mDeviceHostName;
+ @NonNull private final SharedLog mSharedLog;
/**
* Dependencies for {@link MdnsAdvertiser}, useful for testing.
@@ -84,11 +84,11 @@
@NonNull List<LinkAddress> initialAddresses,
@NonNull Looper looper, @NonNull byte[] packetCreationBuffer,
@NonNull MdnsInterfaceAdvertiser.Callback cb,
- @NonNull String[] deviceHostName) {
+ @NonNull String[] deviceHostName,
+ @NonNull SharedLog sharedLog) {
// Note NetworkInterface is final and not mockable
- final String logTag = socket.getInterface().getName();
- return new MdnsInterfaceAdvertiser(logTag, socket, initialAddresses, looper,
- packetCreationBuffer, cb, deviceHostName, LOGGER.forSubComponent(logTag));
+ return new MdnsInterfaceAdvertiser(socket, initialAddresses, looper,
+ packetCreationBuffer, cb, deviceHostName, sharedLog);
}
/**
@@ -135,7 +135,7 @@
@Override
public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
- LOGGER.i("Found conflict, restarted probing for service " + serviceId);
+ mSharedLog.i("Found conflict, restarted probing for service " + serviceId);
final Registration registration = mRegistrations.get(serviceId);
if (registration == null) return;
@@ -270,7 +270,8 @@
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+ mAdvertisers.valueAt(i).addService(
+ id, registration.getServiceInfo(), registration.getSubtype());
} catch (NameConflictException e) {
Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
}
@@ -291,15 +292,17 @@
MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.get(socket);
if (advertiser == null) {
advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer,
- mInterfaceAdvertiserCb, mDeviceHostName);
+ mInterfaceAdvertiserCb, mDeviceHostName,
+ mSharedLog.forSubComponent(socket.getInterface().getName()));
mAllAdvertisers.put(socket, advertiser);
advertiser.start();
}
mAdvertisers.put(socket, advertiser);
for (int i = 0; i < mPendingRegistrations.size(); i++) {
+ final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- mPendingRegistrations.valueAt(i).getServiceInfo());
+ registration.getServiceInfo(), registration.getSubtype());
} catch (NameConflictException e) {
Log.wtf(TAG, "Name conflict adding services that should have unique names", e);
}
@@ -328,10 +331,13 @@
private int mConflictCount;
@NonNull
private NsdServiceInfo mServiceInfo;
+ @Nullable
+ private final String mSubtype;
- private Registration(@NonNull NsdServiceInfo serviceInfo) {
+ private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
this.mOriginalName = serviceInfo.getServiceName();
this.mServiceInfo = serviceInfo;
+ this.mSubtype = subtype;
}
/**
@@ -386,6 +392,11 @@
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
+
+ @Nullable
+ public String getSubtype() {
+ return mSubtype;
+ }
}
/**
@@ -416,18 +427,20 @@
}
public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
- @NonNull AdvertiserCallback cb) {
- this(looper, socketProvider, cb, new Dependencies());
+ @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog) {
+ this(looper, socketProvider, cb, new Dependencies(), sharedLog);
}
@VisibleForTesting
MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
- @NonNull AdvertiserCallback cb, @NonNull Dependencies deps) {
+ @NonNull AdvertiserCallback cb, @NonNull Dependencies deps,
+ @NonNull SharedLog sharedLog) {
mLooper = looper;
mCb = cb;
mSocketProvider = socketProvider;
mDeps = deps;
mDeviceHostName = deps.generateHostname();
+ mSharedLog = sharedLog;
}
private void checkThread() {
@@ -440,8 +453,9 @@
* Add a service to advertise.
* @param id A unique ID for the service.
* @param service The service info to advertise.
+ * @param subtype An optional subtype to advertise the service with.
*/
- public void addService(int id, NsdServiceInfo service) {
+ public void addService(int id, NsdServiceInfo service, @Nullable String subtype) {
checkThread();
if (mRegistrations.get(id) != null) {
Log.e(TAG, "Adding duplicate registration for " + service);
@@ -450,10 +464,10 @@
return;
}
- LOGGER.i("Adding service " + service + " with ID " + id);
+ mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype);
final Network network = service.getNetwork();
- final Registration registration = new Registration(service);
+ final Registration registration = new Registration(service, subtype);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
@@ -482,7 +496,7 @@
public void removeService(int id) {
checkThread();
if (!mRegistrations.contains(id)) return;
- LOGGER.i("Removing service with ID " + id);
+ mSharedLog.i("Removing service with ID " + id);
for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
final InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.valueAt(i);
advertiser.removeService(id);
@@ -494,10 +508,6 @@
}
}
- /** Dump info to dumpsys */
- public void dump(PrintWriter pw) {
- LOGGER.reverseDump(pw);
- }
private static <K, V> boolean any(@NonNull ArrayMap<K, V> map,
@NonNull BiPredicate<K, V> predicate) {
for (int i = 0; i < map.size(); i++) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java b/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java
index 75c7e6c..761c477 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsConfigs.java
@@ -94,10 +94,6 @@
return false;
}
- public static boolean allowSearchOptionsToRemoveExpiredService() {
- return false;
- }
-
public static boolean allowNetworkInterfaceIndexPropagation() {
return true;
}
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 64985ad..c3deeca 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -16,25 +16,26 @@
package com.android.server.connectivity.mdns;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
+import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.net.Network;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.SharedLog;
import java.io.IOException;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
@@ -43,13 +44,14 @@
public class MdnsDiscoveryManager implements MdnsSocketClientBase.Callback {
private static final String TAG = MdnsDiscoveryManager.class.getSimpleName();
public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
- private static final SharedLog LOGGER = new SharedLog(TAG);
private final ExecutorProvider executorProvider;
private final MdnsSocketClientBase socketClient;
+ @NonNull private final SharedLog sharedLog;
- @GuardedBy("this")
@NonNull private final PerNetworkServiceTypeClients perNetworkServiceTypeClients;
+ @NonNull private final Handler handler;
+ @Nullable private final HandlerThread handlerThread;
private static class PerNetworkServiceTypeClients {
private final ArrayMap<Pair<String, Network>, MdnsServiceTypeClient> clients =
@@ -78,11 +80,12 @@
return list;
}
- public List<MdnsServiceTypeClient> getByMatchingNetwork(@Nullable Network network) {
+ public List<MdnsServiceTypeClient> getByNetwork(@Nullable Network network) {
final List<MdnsServiceTypeClient> list = new ArrayList<>();
for (int i = 0; i < clients.size(); i++) {
final Pair<String, Network> perNetworkServiceType = clients.keyAt(i);
- if (isNetworkMatched(network, perNetworkServiceType.second)) {
+ final Network serviceTypeNetwork = perNetworkServiceType.second;
+ if (Objects.equals(network, serviceTypeNetwork)) {
list.add(clients.valueAt(i));
}
}
@@ -100,10 +103,36 @@
}
public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
- @NonNull MdnsSocketClientBase socketClient) {
+ @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
this.executorProvider = executorProvider;
this.socketClient = socketClient;
- perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
+ this.sharedLog = sharedLog;
+ this.perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
+ if (socketClient.getLooper() != null) {
+ this.handlerThread = null;
+ this.handler = new Handler(socketClient.getLooper());
+ } else {
+ this.handlerThread = new HandlerThread(MdnsDiscoveryManager.class.getSimpleName());
+ this.handlerThread.start();
+ this.handler = new Handler(handlerThread.getLooper());
+ }
+ }
+
+ private void checkAndRunOnHandlerThread(@NonNull Runnable function) {
+ if (this.handlerThread == null) {
+ function.run();
+ } else {
+ handler.post(function);
+ }
+ }
+
+ /**
+ * Do the cleanup of the MdnsDiscoveryManager
+ */
+ public void shutDown() {
+ if (this.handlerThread != null) {
+ this.handlerThread.quitSafely();
+ }
}
/**
@@ -116,33 +145,57 @@
* serviceType}.
*/
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
- public synchronized void registerListener(
+ public void registerListener(
@NonNull String serviceType,
@NonNull MdnsServiceBrowserListener listener,
@NonNull MdnsSearchOptions searchOptions) {
- LOGGER.i("Registering listener for serviceType: " + serviceType);
+ sharedLog.i("Registering listener for serviceType: " + serviceType);
+ checkAndRunOnHandlerThread(() ->
+ handleRegisterListener(serviceType, listener, searchOptions));
+ }
+
+ private void handleRegisterListener(
+ @NonNull String serviceType,
+ @NonNull MdnsServiceBrowserListener listener,
+ @NonNull MdnsSearchOptions searchOptions) {
if (perNetworkServiceTypeClients.isEmpty()) {
// First listener. Starts the socket client.
try {
socketClient.startDiscovery();
} catch (IOException e) {
- LOGGER.e("Failed to start discover.", e);
+ sharedLog.e("Failed to start discover.", e);
return;
}
}
// Request the network for discovery.
- socketClient.notifyNetworkRequested(listener, searchOptions.getNetwork(), network -> {
- synchronized (this) {
- // All listeners of the same service types shares the same MdnsServiceTypeClient.
- MdnsServiceTypeClient serviceTypeClient =
- perNetworkServiceTypeClients.get(serviceType, network);
- if (serviceTypeClient == null) {
- serviceTypeClient = createServiceTypeClient(serviceType, network);
- perNetworkServiceTypeClients.put(serviceType, network, serviceTypeClient);
- }
- serviceTypeClient.startSendAndReceive(listener, searchOptions);
- }
- });
+ socketClient.notifyNetworkRequested(listener, searchOptions.getNetwork(),
+ new MdnsSocketClientBase.SocketCreationCallback() {
+ @Override
+ public void onSocketCreated(@Nullable Network network) {
+ ensureRunningOnHandlerThread(handler);
+ // All listeners of the same service types shares the same
+ // MdnsServiceTypeClient.
+ MdnsServiceTypeClient serviceTypeClient =
+ perNetworkServiceTypeClients.get(serviceType, network);
+ if (serviceTypeClient == null) {
+ serviceTypeClient = createServiceTypeClient(serviceType, network);
+ perNetworkServiceTypeClients.put(serviceType, network,
+ serviceTypeClient);
+ }
+ serviceTypeClient.startSendAndReceive(listener, searchOptions);
+ }
+
+ @Override
+ public void onAllSocketsDestroyed(@Nullable Network network) {
+ ensureRunningOnHandlerThread(handler);
+ final MdnsServiceTypeClient serviceTypeClient =
+ perNetworkServiceTypeClients.get(serviceType, network);
+ if (serviceTypeClient == null) return;
+ // Notify all listeners that all services are removed from this socket.
+ serviceTypeClient.notifySocketDestroyed();
+ perNetworkServiceTypeClients.remove(serviceTypeClient);
+ }
+ });
}
/**
@@ -153,9 +206,17 @@
* @param listener The {@link MdnsServiceBrowserListener} listener.
*/
@RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
- public synchronized void unregisterListener(
+ public void unregisterListener(
@NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
- LOGGER.i("Unregistering listener for serviceType:" + serviceType);
+ sharedLog.i("Unregistering listener for serviceType:" + serviceType);
+ checkAndRunOnHandlerThread(() -> handleUnregisterListener(serviceType, listener));
+ }
+
+ private void handleUnregisterListener(
+ @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
+ // Unrequested the network.
+ socketClient.notifyNetworkUnrequested(listener);
+
final List<MdnsServiceTypeClient> serviceTypeClients =
perNetworkServiceTypeClients.getByServiceType(serviceType);
if (serviceTypeClients.isEmpty()) {
@@ -167,47 +228,51 @@
// No listener is registered for the service type anymore, remove it from the list
// of the service type clients.
perNetworkServiceTypeClients.remove(serviceTypeClient);
- if (perNetworkServiceTypeClients.isEmpty()) {
- // No discovery request. Stops the socket client.
- socketClient.stopDiscovery();
- }
}
}
- // Unrequested the network.
- socketClient.notifyNetworkUnrequested(listener);
+ if (perNetworkServiceTypeClients.isEmpty()) {
+ // No discovery request. Stops the socket client.
+ socketClient.stopDiscovery();
+ }
}
@Override
- public synchronized void onResponseReceived(@NonNull MdnsPacket packet,
- int interfaceIndex, Network network) {
+ public void onResponseReceived(@NonNull MdnsPacket packet,
+ int interfaceIndex, @Nullable Network network) {
+ checkAndRunOnHandlerThread(() ->
+ handleOnResponseReceived(packet, interfaceIndex, network));
+ }
+
+ private void handleOnResponseReceived(@NonNull MdnsPacket packet, int interfaceIndex,
+ @Nullable Network network) {
for (MdnsServiceTypeClient serviceTypeClient
- : perNetworkServiceTypeClients.getByMatchingNetwork(network)) {
+ : perNetworkServiceTypeClients.getByNetwork(network)) {
serviceTypeClient.processResponse(packet, interfaceIndex, network);
}
}
@Override
- public synchronized void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
- Network network) {
- for (MdnsServiceTypeClient serviceTypeClient
- : perNetworkServiceTypeClients.getByMatchingNetwork(network)) {
- serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
- }
+ public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
+ @Nullable Network network) {
+ checkAndRunOnHandlerThread(() ->
+ handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, network));
}
- /** Dump info to dumpsys */
- public void dump(PrintWriter pw) {
- LOGGER.reverseDump(pw);
+ private void handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
+ @Nullable Network network) {
+ for (MdnsServiceTypeClient serviceTypeClient
+ : perNetworkServiceTypeClients.getByNetwork(network)) {
+ serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
+ }
}
@VisibleForTesting
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@Nullable Network network) {
- LOGGER.log("createServiceTypeClient for serviceType:" + serviceType
- + " network:" + network);
+ sharedLog.log("createServiceTypeClient for type:" + serviceType + ", net:" + network);
return new MdnsServiceTypeClient(
serviceType, socketClient,
executorProvider.newServiceTypeClientSchedulerExecutor(), network,
- LOGGER.forSubComponent(serviceType + "-" + network));
+ sharedLog.forSubComponent(serviceType + "-" + network));
}
}
\ No newline at end of file
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 9eaa580..724a704 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -170,29 +170,29 @@
}
}
- public MdnsInterfaceAdvertiser(@NonNull String logTag,
- @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses,
- @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
+ public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket,
+ @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper,
+ @NonNull byte[] packetCreationBuffer, @NonNull Callback cb,
@NonNull String[] deviceHostName, @NonNull SharedLog sharedLog) {
- this(logTag, socket, initialAddresses, looper, packetCreationBuffer, cb,
+ this(socket, initialAddresses, looper, packetCreationBuffer, cb,
new Dependencies(), deviceHostName, sharedLog);
}
- public MdnsInterfaceAdvertiser(@NonNull String logTag,
- @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) {
- mTag = MdnsInterfaceAdvertiser.class.getSimpleName() + "/" + logTag;
+ 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) {
+ mTag = MdnsInterfaceAdvertiser.class.getSimpleName() + "/" + sharedLog.getTag();
mRecordRepository = deps.makeRecordRepository(looper, deviceHostName);
mRecordRepository.updateAddresses(initialAddresses);
mSocket = socket;
mCb = cb;
mCbHandler = new Handler(looper);
- mReplySender = deps.makeReplySender(logTag, looper, socket, packetCreationBuffer);
- mAnnouncer = deps.makeMdnsAnnouncer(logTag, looper, mReplySender,
+ mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
+ packetCreationBuffer);
+ mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
mAnnouncingCallback);
- mProber = deps.makeMdnsProber(logTag, looper, mReplySender, mProbingCallback);
+ mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback);
mSharedLog = sharedLog;
}
@@ -212,8 +212,9 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service) throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service);
+ public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
+ throws NameConflictException {
+ final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
// Cancel announcements for the existing service. This only happens for exiting services
// (so cancelling exiting announcements), as per RecordRepository.addService.
if (replacedExitingService >= 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 4504bb6..d9bfb26 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -17,7 +17,6 @@
package com.android.server.connectivity.mdns;
import static com.android.server.connectivity.mdns.util.MdnsUtils.ensureRunningOnHandlerThread;
-import static com.android.server.connectivity.mdns.util.MdnsUtils.isNetworkMatched;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -34,7 +33,7 @@
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.util.List;
-import java.util.Map;
+import java.util.Objects;
/**
* The {@link MdnsMultinetworkSocketClient} manages the multinetwork socket for mDns
@@ -48,9 +47,8 @@
@NonNull private final Handler mHandler;
@NonNull private final MdnsSocketProvider mSocketProvider;
- private final Map<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
+ private final ArrayMap<MdnsServiceBrowserListener, InterfaceSocketCallback> mRequestedNetworks =
new ArrayMap<>();
- private final ArrayMap<MdnsInterfaceSocket, Network> mActiveNetworkSockets = new ArrayMap<>();
private final ArrayMap<MdnsInterfaceSocket, ReadPacketHandler> mSocketPacketHandlers =
new ArrayMap<>();
private MdnsSocketClientBase.Callback mCallback = null;
@@ -63,7 +61,11 @@
}
private class InterfaceSocketCallback implements MdnsSocketProvider.SocketCallback {
+ @NonNull
private final SocketCreationCallback mSocketCreationCallback;
+ @NonNull
+ private final ArrayMap<MdnsInterfaceSocket, Network> mActiveNetworkSockets =
+ new ArrayMap<>();
InterfaceSocketCallback(SocketCreationCallback socketCreationCallback) {
mSocketCreationCallback = socketCreationCallback;
@@ -88,9 +90,59 @@
@Override
public void onInterfaceDestroyed(@Nullable Network network,
@NonNull MdnsInterfaceSocket socket) {
- mSocketPacketHandlers.remove(socket);
- mActiveNetworkSockets.remove(socket);
+ notifySocketDestroyed(socket);
+ maybeCleanupPacketHandler(socket);
}
+
+ private void notifySocketDestroyed(@NonNull MdnsInterfaceSocket socket) {
+ final Network network = mActiveNetworkSockets.remove(socket);
+ if (!isAnySocketActive(network)) {
+ mSocketCreationCallback.onAllSocketsDestroyed(network);
+ }
+ }
+
+ void onNetworkUnrequested() {
+ for (int i = mActiveNetworkSockets.size() - 1; i >= 0; i--) {
+ // Iterate from the end so the socket can be removed
+ final MdnsInterfaceSocket socket = mActiveNetworkSockets.keyAt(i);
+ notifySocketDestroyed(socket);
+ maybeCleanupPacketHandler(socket);
+ }
+ }
+ }
+
+ private boolean isSocketActive(@NonNull MdnsInterfaceSocket socket) {
+ for (int i = 0; i < mRequestedNetworks.size(); i++) {
+ final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
+ if (isc.mActiveNetworkSockets.containsKey(socket)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isAnySocketActive(@Nullable Network network) {
+ for (int i = 0; i < mRequestedNetworks.size(); i++) {
+ final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
+ if (isc.mActiveNetworkSockets.containsValue(network)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private ArrayMap<MdnsInterfaceSocket, Network> getActiveSockets() {
+ final ArrayMap<MdnsInterfaceSocket, Network> sockets = new ArrayMap<>();
+ for (int i = 0; i < mRequestedNetworks.size(); i++) {
+ final InterfaceSocketCallback isc = mRequestedNetworks.valueAt(i);
+ sockets.putAll(isc.mActiveNetworkSockets);
+ }
+ return sockets;
+ }
+
+ private void maybeCleanupPacketHandler(@NonNull MdnsInterfaceSocket socket) {
+ if (isSocketActive(socket)) return;
+ mSocketPacketHandlers.remove(socket);
}
private class ReadPacketHandler implements MulticastPacketReader.PacketHandler {
@@ -142,25 +194,37 @@
@Override
public void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) {
ensureRunningOnHandlerThread(mHandler);
- final InterfaceSocketCallback callback = mRequestedNetworks.remove(listener);
+ final InterfaceSocketCallback callback = mRequestedNetworks.get(listener);
if (callback == null) {
Log.e(TAG, "Can not be unrequested with unknown listener=" + listener);
return;
}
+ callback.onNetworkUnrequested();
+ // onNetworkUnrequested does cleanups based on mRequestedNetworks, only remove afterwards
+ mRequestedNetworks.remove(listener);
mSocketProvider.unrequestSocket(callback);
}
+ @Override
+ public Looper getLooper() {
+ return mHandler.getLooper();
+ }
+
private void sendMdnsPacket(@NonNull DatagramPacket packet, @Nullable Network targetNetwork) {
final boolean isIpv6 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
instanceof Inet6Address;
final boolean isIpv4 = ((InetSocketAddress) packet.getSocketAddress()).getAddress()
instanceof Inet4Address;
- for (int i = 0; i < mActiveNetworkSockets.size(); i++) {
- final MdnsInterfaceSocket socket = mActiveNetworkSockets.keyAt(i);
- final Network network = mActiveNetworkSockets.valueAt(i);
+ final ArrayMap<MdnsInterfaceSocket, Network> activeSockets = getActiveSockets();
+ for (int i = 0; i < activeSockets.size(); i++) {
+ final MdnsInterfaceSocket socket = activeSockets.keyAt(i);
+ final Network network = activeSockets.valueAt(i);
// Check ip capability and network before sending packet
if (((isIpv6 && socket.hasJoinedIpv6()) || (isIpv4 && socket.hasJoinedIpv4()))
- && isNetworkMatched(targetNetwork, network)) {
+ // Contrary to MdnsUtils.isNetworkMatched, only send packets targeting
+ // the null network to interfaces that have the null network (tethering
+ // downstream interfaces).
+ && Objects.equals(network, targetNetwork)) {
try {
socket.send(packet);
} catch (IOException e) {
@@ -192,12 +256,6 @@
}
}
- /** Sends a mDNS request packet that asks for multicast response. */
- @Override
- public void sendMulticastPacket(@NonNull DatagramPacket packet) {
- sendMulticastPacket(packet, null /* network */);
- }
-
/**
* Sends a mDNS request packet via given network that asks for multicast response. Null network
* means sending packet via all networks.
@@ -207,12 +265,6 @@
mHandler.post(() -> sendMdnsPacket(packet, network));
}
- /** Sends a mDNS request packet that asks for unicast response. */
- @Override
- public void sendUnicastPacket(@NonNull DatagramPacket packet) {
- sendUnicastPacket(packet, null /* network */);
- }
-
/**
* Sends a mDNS request packet via given network that asks for unicast response. Null network
* means sending packet via all networks.
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 1329172..f756459 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -69,6 +69,8 @@
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
+ // Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
+ private static final String SUBTYPE_SEPARATOR = "_sub";
// Service type for service enumeration (RFC6763 9.)
private static final String[] DNS_SD_SERVICE_TYPE =
@@ -156,13 +158,15 @@
@NonNull
public final List<RecordInfo<?>> allRecords;
@NonNull
- public final RecordInfo<MdnsPointerRecord> ptrRecord;
+ public final List<RecordInfo<MdnsPointerRecord>> ptrRecords;
@NonNull
public final RecordInfo<MdnsServiceRecord> srvRecord;
@NonNull
public final RecordInfo<MdnsTextRecord> txtRecord;
@NonNull
public final NsdServiceInfo serviceInfo;
+ @Nullable
+ public final String subtype;
/**
* Whether the service is sending exit announcements and will be destroyed soon.
@@ -175,14 +179,16 @@
* @param deviceHostname Hostname of the device (for the interface used)
* @param serviceInfo Service to advertise
*/
- ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo) {
+ ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
+ @Nullable String subtype) {
this.serviceInfo = serviceInfo;
+ this.subtype = subtype;
final String[] serviceType = splitServiceType(serviceInfo);
final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
// Service PTR record
- ptrRecord = new RecordInfo<>(
+ final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
serviceInfo,
new MdnsPointerRecord(
serviceType,
@@ -192,6 +198,26 @@
serviceName),
true /* sharedName */, true /* probing */);
+ if (subtype == null) {
+ this.ptrRecords = Collections.singletonList(ptrRecord);
+ } else {
+ final String[] subtypeName = new String[serviceType.length + 2];
+ System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
+ subtypeName[0] = subtype;
+ subtypeName[1] = SUBTYPE_SEPARATOR;
+ final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
+ serviceInfo,
+ new MdnsPointerRecord(
+ subtypeName,
+ 0L /* receiptTimeMillis */,
+ false /* cacheFlush */,
+ NON_NAME_RECORDS_TTL_MILLIS,
+ serviceName),
+ true /* sharedName */, true /* probing */);
+
+ this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+ }
+
srvRecord = new RecordInfo<>(
serviceInfo,
new MdnsServiceRecord(serviceName,
@@ -211,8 +237,8 @@
attrsToTextEntries(serviceInfo.getAttributes())),
false /* sharedName */, true /* probing */);
- final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(4);
- allRecords.add(ptrRecord);
+ final ArrayList<RecordInfo<?>> allRecords = new ArrayList<>(5);
+ allRecords.addAll(ptrRecords);
allRecords.add(srvRecord);
allRecords.add(txtRecord);
// Service type enumeration record (RFC6763 9.)
@@ -275,7 +301,8 @@
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
+ throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@@ -288,7 +315,7 @@
}
final ServiceRegistration registration = new ServiceRegistration(
- mDeviceHostname, serviceInfo);
+ mDeviceHostname, serviceInfo, subtype);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -344,24 +371,25 @@
if (registration == null) return null;
if (registration.exiting) return null;
- // Send exit (TTL 0) for the PTR record, if the record was sent (in particular don't send
+ // Send exit (TTL 0) for the PTR records, if at least one was sent (in particular don't send
// if still probing)
- if (registration.ptrRecord.lastSentTimeMs == 0L) {
+ if (CollectionUtils.all(registration.ptrRecords, r -> r.lastSentTimeMs == 0L)) {
return null;
}
registration.exiting = true;
- final MdnsPointerRecord expiredRecord = new MdnsPointerRecord(
- registration.ptrRecord.record.getName(),
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- 0L /* ttlMillis */,
- registration.ptrRecord.record.getPointer());
+ final List<MdnsRecord> expiredRecords = CollectionUtils.map(registration.ptrRecords,
+ r -> new MdnsPointerRecord(
+ r.record.getName(),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 0L /* ttlMillis */,
+ r.record.getPointer()));
// Exit should be skipped if the record is still advertised by another service, but that
// would be a conflict (2 service registrations with the same service name), so it would
// not have been allowed by the repository.
- return new MdnsAnnouncer.ExitAnnouncementInfo(id, Collections.singletonList(expiredRecord));
+ return new MdnsAnnouncer.ExitAnnouncementInfo(id, expiredRecords);
}
public void removeService(int id) {
@@ -442,7 +470,7 @@
for (int i = 0; i < mServices.size(); i++) {
final ServiceRegistration registration = mServices.valueAt(i);
if (registration.exiting) continue;
- addReplyFromService(question, registration.allRecords, registration.ptrRecord,
+ addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
answerInfo, additionalAnswerRecords);
}
@@ -499,7 +527,7 @@
*/
private void addReplyFromService(@NonNull MdnsRecord question,
@NonNull List<RecordInfo<?>> serviceRecords,
- @Nullable RecordInfo<MdnsPointerRecord> servicePtrRecord,
+ @Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
@@ -531,7 +559,8 @@
}
hasKnownAnswer = true;
- hasDnsSdPtrRecordAnswer |= (info == servicePtrRecord);
+ hasDnsSdPtrRecordAnswer |= (servicePtrRecords != null
+ && CollectionUtils.any(servicePtrRecords, r -> info == r));
hasDnsSdSrvRecordAnswer |= (info == serviceSrvRecord);
// TODO: responses to probe queries should bypass this check and only ensure the
@@ -791,10 +820,11 @@
*/
@Nullable
public MdnsProber.ProbingInfo renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
- if (!mServices.contains(serviceId)) return null;
+ final ServiceRegistration existing = mServices.get(serviceId);
+ if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(
- mDeviceHostname, newInfo);
+ mDeviceHostname, newInfo, existing.subtype);
mServices.put(serviceId, newService);
return makeProbingInfo(serviceId, newService.srvRecord.record);
}
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 e0100f6..e765d53 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponse.java
@@ -84,16 +84,23 @@
private <T extends MdnsRecord> boolean addOrReplaceRecord(@NonNull T record,
@NonNull List<T> recordsList) {
final int existing = recordsList.indexOf(record);
+ boolean isSame = false;
if (existing >= 0) {
- if (recordsAreSame(record, recordsList.get(existing))) {
- return false;
- }
+ isSame = recordsAreSame(record, recordsList.get(existing));
final MdnsRecord existedRecord = recordsList.remove(existing);
records.remove(existedRecord);
}
recordsList.add(record);
records.add(record);
- return true;
+ return !isSame;
+ }
+
+ /**
+ * @return True if this response contains an identical (original TTL included) record.
+ */
+ public boolean hasIdenticalRecord(@NonNull MdnsRecord record) {
+ final int existing = records.indexOf(record);
+ return existing >= 0 && recordsAreSame(record, records.get(existing));
}
/**
@@ -163,9 +170,7 @@
/** Sets the service record. */
public synchronized boolean setServiceRecord(MdnsServiceRecord serviceRecord) {
- if (recordsAreSame(this.serviceRecord, serviceRecord)) {
- return false;
- }
+ boolean isSame = recordsAreSame(this.serviceRecord, serviceRecord);
if (this.serviceRecord != null) {
records.remove(this.serviceRecord);
}
@@ -173,7 +178,7 @@
if (this.serviceRecord != null) {
records.add(this.serviceRecord);
}
- return true;
+ return !isSame;
}
/** Gets the service record. */
@@ -187,9 +192,7 @@
/** Sets the text record. */
public synchronized boolean setTextRecord(MdnsTextRecord textRecord) {
- if (recordsAreSame(this.textRecord, textRecord)) {
- return false;
- }
+ boolean isSame = recordsAreSame(this.textRecord, textRecord);
if (this.textRecord != null) {
records.remove(this.textRecord);
}
@@ -197,7 +200,7 @@
if (this.textRecord != null) {
records.add(this.textRecord);
}
- return true;
+ return !isSame;
}
/** Gets the text record. */
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 129db7e..6648729 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsResponseDecoder.java
@@ -20,7 +20,9 @@
import android.annotation.Nullable;
import android.net.Network;
import android.os.SystemClock;
+import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.Pair;
import com.android.server.connectivity.mdns.util.MdnsLogger;
@@ -119,9 +121,14 @@
* @param interfaceIndex the network interface index (or
* {@link MdnsSocket#INTERFACE_INDEX_UNSPECIFIED} if not known) at which the packet was received
* @param network the network at which the packet was received, or null if it is unknown.
- * @return The set of response instances that were modified or newly added.
+ * @return The pair of 1) set of response instances that were modified or newly added. *not*
+ * including those which records were only updated with newer receive
+ * timestamps.
+ * 2) A copy of the original responses with some of them have records
+ * update or only contains receive time updated.
*/
- public ArraySet<MdnsResponse> augmentResponses(@NonNull MdnsPacket mdnsPacket,
+ public Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentResponses(
+ @NonNull MdnsPacket mdnsPacket,
@NonNull Collection<MdnsResponse> existingResponses, int interfaceIndex,
@Nullable Network network) {
final ArrayList<MdnsRecord> records = new ArrayList<>(
@@ -133,8 +140,11 @@
final ArraySet<MdnsResponse> modified = new ArraySet<>();
final ArrayList<MdnsResponse> responses = new ArrayList<>(existingResponses.size());
+ final ArrayMap<MdnsResponse, MdnsResponse> augmentedToOriginal = new ArrayMap<>();
for (MdnsResponse existing : existingResponses) {
- responses.add(new MdnsResponse(existing));
+ final MdnsResponse copy = new MdnsResponse(existing);
+ responses.add(copy);
+ augmentedToOriginal.put(copy, existing);
}
// The response records are structured in a hierarchy, where some records reference
// others, as follows:
@@ -160,7 +170,7 @@
// A: host name -> IP address
// Loop 1: find PTR records, which identify distinct service instances.
- long now = SystemClock.elapsedRealtime();
+ long now = clock.elapsedRealtime();
for (MdnsRecord record : records) {
if (record instanceof MdnsPointerRecord) {
String[] name = record.getName();
@@ -179,7 +189,6 @@
network);
responses.add(response);
}
-
if (response.addPointerRecord((MdnsPointerRecord) record)) {
modified.add(response);
}
@@ -257,7 +266,11 @@
findResponsesWithHostName(responses, inetRecord.getName());
for (MdnsResponse response : matchingResponses) {
if (assignInetRecord(response, inetRecord)) {
- modified.add(response);
+ final MdnsResponse originalResponse = augmentedToOriginal.get(response);
+ if (originalResponse == null
+ || !originalResponse.hasIdenticalRecord(inetRecord)) {
+ modified.add(response);
+ }
}
}
} else {
@@ -265,13 +278,27 @@
findResponseWithHostName(responses, inetRecord.getName());
if (response != null) {
if (assignInetRecord(response, inetRecord)) {
- modified.add(response);
+ final MdnsResponse originalResponse = augmentedToOriginal.get(response);
+ if (originalResponse == null
+ || !originalResponse.hasIdenticalRecord(inetRecord)) {
+ modified.add(response);
+ }
}
}
}
}
- return modified;
+ // Only responses that have new or modified address records were added to the modified set.
+ // Make sure responses that have lost address records are added to the set too.
+ for (int i = 0; i < augmentedToOriginal.size(); i++) {
+ final MdnsResponse augmented = augmentedToOriginal.keyAt(i);
+ final MdnsResponse original = augmentedToOriginal.valueAt(i);
+ if (augmented.getRecords().size() != original.getRecords().size()) {
+ modified.add(augmented);
+ }
+ }
+
+ return Pair.create(modified, responses);
}
private static boolean assignInetRecord(
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 72b931d..14302c2 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -28,13 +28,16 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
+import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -60,12 +63,10 @@
private final Object lock = new Object();
private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
new ArrayMap<>();
+ // TODO: change instanceNameToResponse to TreeMap with case insensitive comparator.
private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
- private final boolean allowSearchOptionsToRemoveExpiredService =
- MdnsConfigs.allowSearchOptionsToRemoveExpiredService();
-
private final MdnsResponseDecoder.Clock clock;
@Nullable private MdnsSearchOptions searchOptions;
@@ -175,6 +176,7 @@
@NonNull MdnsSearchOptions searchOptions) {
synchronized (lock) {
this.searchOptions = searchOptions;
+ boolean hadReply = false;
if (listeners.put(listener, searchOptions) == null) {
for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
@@ -183,6 +185,7 @@
listener.onServiceNameDiscovered(info);
if (existingResponse.isComplete()) {
listener.onServiceFound(info);
+ hadReply = true;
}
}
}
@@ -192,22 +195,37 @@
}
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
// interested anymore.
- requestTaskFuture =
- executor.submit(
- new QueryTask(
- new QueryTaskConfig(
- searchOptions.getSubtypes(),
- searchOptions.isPassiveMode(),
- ++currentSessionId,
- searchOptions.getNetwork())));
+ final QueryTaskConfig taskConfig = new QueryTaskConfig(
+ searchOptions.getSubtypes(),
+ searchOptions.isPassiveMode(),
+ ++currentSessionId,
+ network);
+ if (hadReply) {
+ requestTaskFuture = scheduleNextRunLocked(taskConfig);
+ } else {
+ requestTaskFuture = executor.submit(new QueryTask(taskConfig));
+ }
}
}
private boolean responseMatchesOptions(@NonNull MdnsResponse response,
@NonNull MdnsSearchOptions options) {
- if (options.getResolveInstanceName() == null) return true;
- // DNS is case-insensitive, so ignore case in the comparison
- return options.getResolveInstanceName().equalsIgnoreCase(response.getServiceInstanceName());
+ final boolean matchesInstanceName = options.getResolveInstanceName() == null
+ // DNS is case-insensitive, so ignore case in the comparison
+ || MdnsUtils.equalsIgnoreDnsCase(options.getResolveInstanceName(),
+ response.getServiceInstanceName());
+
+ // If discovery is requiring some subtypes, the response must have one that matches a
+ // requested one.
+ final List<String> responseSubtypes = response.getSubtypes() == null
+ ? Collections.emptyList() : response.getSubtypes();
+ final boolean matchesSubtype = options.getSubtypes().size() == 0
+ || CollectionUtils.any(options.getSubtypes(), requiredSub ->
+ CollectionUtils.any(responseSubtypes, actualSub ->
+ MdnsUtils.equalsIgnoreDnsCase(
+ MdnsConstants.SUBTYPE_PREFIX + requiredSub, actualSub)));
+
+ return matchesInstanceName && matchesSubtype;
}
/**
@@ -243,15 +261,32 @@
// Augment the list of current known responses, and generated responses for resolve
// requests if there is no known response
final List<MdnsResponse> currentList = new ArrayList<>(instanceNameToResponse.values());
- currentList.addAll(makeResponsesForResolveIfUnknown(interfaceIndex, network));
- final ArraySet<MdnsResponse> modifiedResponses = responseDecoder.augmentResponses(
- packet, currentList, interfaceIndex, network);
- for (MdnsResponse modified : modifiedResponses) {
- if (modified.isGoodbye()) {
- onGoodbyeReceived(modified.getServiceInstanceName());
- } else {
- onResponseModified(modified);
+ List<MdnsResponse> additionalResponses = makeResponsesForResolve(interfaceIndex,
+ network);
+ for (MdnsResponse additionalResponse : additionalResponses) {
+ if (!instanceNameToResponse.containsKey(
+ additionalResponse.getServiceInstanceName())) {
+ currentList.add(additionalResponse);
+ }
+ }
+ final Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult =
+ responseDecoder.augmentResponses(packet, currentList, interfaceIndex, network);
+
+ final ArraySet<MdnsResponse> modifiedResponse = augmentedResult.first;
+ final ArrayList<MdnsResponse> allResponses = augmentedResult.second;
+
+ for (MdnsResponse response : allResponses) {
+ if (modifiedResponse.contains(response)) {
+ if (response.isGoodbye()) {
+ onGoodbyeReceived(response.getServiceInstanceName());
+ } else {
+ onResponseModified(response);
+ }
+ } else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) {
+ // If the response is not modified and already in the cache. The cache will
+ // need to be updated to refresh the last receipt time.
+ instanceNameToResponse.put(response.getServiceInstanceName(), response);
}
}
}
@@ -263,6 +298,33 @@
}
}
+ /** Notify all services are removed because the socket is destroyed. */
+ public void notifySocketDestroyed() {
+ synchronized (lock) {
+ for (MdnsResponse response : instanceNameToResponse.values()) {
+ 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);
+ if (response.isComplete()) {
+ sharedLog.log("Socket destroyed. onServiceRemoved: " + name);
+ listener.onServiceRemoved(serviceInfo);
+ }
+ sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
+ listener.onServiceNameRemoved(serviceInfo);
+ }
+ }
+
+ if (requestTaskFuture != null) {
+ requestTaskFuture.cancel(true);
+ requestTaskFuture = null;
+ }
+ }
+ }
+
private void onResponseModified(@NonNull MdnsResponse response) {
final String serviceInstanceName = response.getServiceInstanceName();
final MdnsResponse currentResponse =
@@ -288,16 +350,16 @@
if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
if (newServiceFound) {
- sharedLog.log("onServiceNameDiscovered: " + serviceInstanceName);
+ sharedLog.log("onServiceNameDiscovered: " + serviceInfo);
listener.onServiceNameDiscovered(serviceInfo);
}
if (response.isComplete()) {
if (newServiceFound || serviceBecomesComplete) {
- sharedLog.log("onServiceFound: " + serviceInstanceName);
+ sharedLog.log("onServiceFound: " + serviceInfo);
listener.onServiceFound(serviceInfo);
} else {
- sharedLog.log("onServiceUpdated: " + serviceInstanceName);
+ sharedLog.log("onServiceUpdated: " + serviceInfo);
listener.onServiceUpdated(serviceInfo);
}
}
@@ -315,10 +377,10 @@
final MdnsServiceInfo serviceInfo =
buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
if (response.isComplete()) {
- sharedLog.log("onServiceRemoved: " + serviceInstanceName);
+ sharedLog.log("onServiceRemoved: " + serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
- sharedLog.log("onServiceNameRemoved: " + serviceInstanceName);
+ sharedLog.log("onServiceNameRemoved: " + serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
@@ -327,9 +389,7 @@
if (removeServiceAfterTtlExpires) {
return true;
}
- return allowSearchOptionsToRemoveExpiredService
- && searchOptions != null
- && searchOptions.removeExpiredService();
+ return searchOptions != null && searchOptions.removeExpiredService();
}
@VisibleForTesting
@@ -432,7 +492,7 @@
}
}
- private List<MdnsResponse> makeResponsesForResolveIfUnknown(int interfaceIndex,
+ private List<MdnsResponse> makeResponsesForResolve(int interfaceIndex,
@NonNull Network network) {
final List<MdnsResponse> resolveResponses = new ArrayList<>();
for (int i = 0; i < listeners.size(); i++) {
@@ -474,7 +534,7 @@
// queried to complete it.
// Only the names are used to know which queries to send, other parameters like
// interfaceIndex do not matter.
- servicesToResolve = makeResponsesForResolveIfUnknown(
+ servicesToResolve = makeResponsesForResolve(
0 /* interfaceIndex */, config.network);
sendDiscoveryQueries = servicesToResolve.size() < listeners.size();
}
@@ -490,7 +550,8 @@
config.transactionId,
config.network,
sendDiscoveryQueries,
- servicesToResolve)
+ servicesToResolve,
+ clock)
.call();
} catch (RuntimeException e) {
sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
@@ -535,30 +596,31 @@
continue;
}
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
- String serviceInstanceName =
- existingResponse.getServiceInstanceName();
- if (serviceInstanceName != null) {
+ if (existingResponse.getServiceInstanceName() != null) {
final MdnsServiceInfo serviceInfo =
buildMdnsServiceInfoFromResponse(
existingResponse, serviceTypeLabels);
if (existingResponse.isComplete()) {
sharedLog.log("TTL expired. onServiceRemoved: "
- + serviceInstanceName);
+ + serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
sharedLog.log("TTL expired. onServiceNameRemoved: "
- + serviceInstanceName);
+ + serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
}
}
}
- QueryTaskConfig config = this.config.getConfigForNextRun();
- requestTaskFuture =
- executor.schedule(
- new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+ requestTaskFuture = scheduleNextRunLocked(this.config);
}
}
}
+
+ @NonNull
+ private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig lastRunConfig) {
+ QueryTaskConfig config = lastRunConfig.getConfigForNextRun();
+ return executor.schedule(new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+ }
}
\ No newline at end of file
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 783b18a..1144d16 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClient.java
@@ -195,13 +195,11 @@
}
/** Sends a mDNS request packet that asks for multicast response. */
- @Override
public void sendMulticastPacket(@NonNull DatagramPacket packet) {
sendMdnsPacket(packet, multicastPacketQueue);
}
/** Sends a mDNS request packet that asks for unicast response. */
- @Override
public void sendUnicastPacket(DatagramPacket packet) {
if (useSeparateSocketForUnicast) {
sendMdnsPacket(packet, unicastPacketQueue);
@@ -210,6 +208,36 @@
}
}
+ @Override
+ public void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+ if (network != null) {
+ throw new IllegalArgumentException("This socket client does not support sending to "
+ + "specific networks");
+ }
+ sendMulticastPacket(packet);
+ }
+
+ @Override
+ public void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
+ if (network != null) {
+ throw new IllegalArgumentException("This socket client does not support sending to "
+ + "specific networks");
+ }
+ sendUnicastPacket(packet);
+ }
+
+ @Override
+ public void notifyNetworkRequested(
+ @NonNull MdnsServiceBrowserListener listener,
+ @Nullable Network network,
+ @NonNull SocketCreationCallback socketCreationCallback) {
+ if (network != null) {
+ throw new IllegalArgumentException("This socket client does not support requesting "
+ + "specific networks");
+ }
+ socketCreationCallback.onSocketCreated(null);
+ }
+
private void sendMdnsPacket(DatagramPacket packet, Queue<DatagramPacket> packetQueueToUse) {
if (shouldStopSocketLoop && !MdnsConfigs.allowAddMdnsPacketAfterDiscoveryStops()) {
LOGGER.w("sendMdnsPacket() is called after discovery already stopped");
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
index ebafc49..deadc58 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
+import android.os.Looper;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -38,39 +39,34 @@
/*** Set callback for receiving mDns response */
void setCallback(@Nullable Callback callback);
- /*** Sends a mDNS request packet that asks for multicast response. */
- void sendMulticastPacket(@NonNull DatagramPacket packet);
+ /**
+ * Send a mDNS request packet via given network that asks for multicast response.
+ *
+ * <p>The socket client may use a null network to identify some or all interfaces, in which case
+ * passing null sends the packet to these.
+ */
+ void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network);
/**
- * Sends a mDNS request packet via given network that asks for multicast response. Null network
- * means sending packet via all networks.
+ * Send a mDNS request packet via given network that asks for unicast response.
+ *
+ * <p>The socket client may use a null network to identify some or all interfaces, in which case
+ * passing null sends the packet to these.
*/
- default void sendMulticastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
- throw new UnsupportedOperationException(
- "This socket client doesn't support per-network sending");
- }
-
- /*** Sends a mDNS request packet that asks for unicast response. */
- void sendUnicastPacket(@NonNull DatagramPacket packet);
-
- /**
- * Sends a mDNS request packet via given network that asks for unicast response. Null network
- * means sending packet via all networks.
- */
- default void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network) {
- throw new UnsupportedOperationException(
- "This socket client doesn't support per-network sending");
- }
+ void sendUnicastPacket(@NonNull DatagramPacket packet, @Nullable Network network);
/*** Notify that the given network is requested for mdns discovery / resolution */
- default void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
- @Nullable Network network, @NonNull SocketCreationCallback socketCreationCallback) {
- socketCreationCallback.onSocketCreated(network);
- }
+ void notifyNetworkRequested(@NonNull MdnsServiceBrowserListener listener,
+ @Nullable Network network, @NonNull SocketCreationCallback socketCreationCallback);
/*** Notify that the network is unrequested */
default void notifyNetworkUnrequested(@NonNull MdnsServiceBrowserListener listener) { }
+ /*** Gets looper that used by the socket client */
+ default Looper getLooper() {
+ return null;
+ }
+
/*** Callback for mdns response */
interface Callback {
/*** Receive a mdns response */
@@ -86,5 +82,8 @@
interface SocketCreationCallback {
/*** Notify requested socket is created */
void onSocketCreated(@Nullable Network network);
+
+ /*** Notify requested socket is destroyed */
+ void onAllSocketsDestroyed(@Nullable Network network);
}
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
index d090a4d..e67c655 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -25,7 +25,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.LinkAddress;
@@ -35,6 +38,9 @@
import android.net.NetworkRequest;
import android.net.TetheringManager;
import android.net.TetheringManager.TetheringEventCallback;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
@@ -47,11 +53,11 @@
import com.android.net.module.util.SharedLog;
import java.io.IOException;
-import java.io.PrintWriter;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* The {@link MdnsSocketProvider} manages the multiple sockets for mDns.
@@ -69,7 +75,6 @@
// But 1440 should generally be enough because of standard Ethernet.
// Note: mdnsresponder mDNSEmbeddedAPI.h uses 8940 for Ethernet jumbo frames.
private static final int READ_BUFFER_SIZE = 2048;
- private static final SharedLog LOGGER = new SharedLog(TAG);
private static final int IFACE_IDX_NOT_EXIST = -1;
@NonNull private final Context mContext;
@NonNull private final Looper mLooper;
@@ -78,6 +83,7 @@
@NonNull private final NetworkCallback mNetworkCallback;
@NonNull private final TetheringEventCallback mTetheringEventCallback;
@NonNull private final AbstractSocketNetlink mSocketNetlinkMonitor;
+ @NonNull private final SharedLog mSharedLog;
private final ArrayMap<Network, SocketInfo> mNetworkSockets = new ArrayMap<>();
private final ArrayMap<String, SocketInfo> mTetherInterfaceSockets = new ArrayMap<>();
private final ArrayMap<Network, LinkProperties> mActiveNetworksLinkProperties =
@@ -93,17 +99,73 @@
private final byte[] mPacketReadBuffer = new byte[READ_BUFFER_SIZE];
private boolean mMonitoringSockets = false;
private boolean mRequestStop = false;
+ private String mWifiP2pTetherInterface = null;
- public MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper) {
- this(context, looper, new Dependencies());
+ private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String newP2pIface = getWifiP2pInterface(intent);
+
+ if (!mMonitoringSockets || !hasAllNetworksRequest()) {
+ mWifiP2pTetherInterface = newP2pIface;
+ return;
+ }
+
+ // If already serving from the correct interface, nothing to do.
+ if (Objects.equals(mWifiP2pTetherInterface, newP2pIface)) return;
+
+ if (mWifiP2pTetherInterface != null) {
+ if (newP2pIface != null) {
+ Log.wtf(TAG, "Wifi p2p interface is changed from " + mWifiP2pTetherInterface
+ + " to " + newP2pIface + " without null broadcast");
+ }
+ // Remove the socket.
+ removeTetherInterfaceSocket(mWifiP2pTetherInterface);
+ }
+
+ // Update mWifiP2pTetherInterface
+ mWifiP2pTetherInterface = newP2pIface;
+
+ // Check whether the socket for wifi p2p interface is created or not.
+ final boolean socketAlreadyExists = mTetherInterfaceSockets.get(newP2pIface) != null;
+ if (newP2pIface != null && !socketAlreadyExists) {
+ // Create a socket for wifi p2p interface.
+ final int ifaceIndex =
+ mDependencies.getNetworkInterfaceIndexByName(newP2pIface);
+ createSocket(LOCAL_NET, createLPForTetheredInterface(newP2pIface, ifaceIndex));
+ }
+ }
+ };
+
+ @Nullable
+ private static String getWifiP2pInterface(final Intent intent) {
+ final WifiP2pGroup group =
+ intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
+ final WifiP2pInfo p2pInfo =
+ intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
+ if (group == null || p2pInfo == null) {
+ return null;
+ }
+
+ if (!p2pInfo.groupFormed) {
+ return null;
+ } else {
+ return group.getInterface();
+ }
+ }
+
+ public MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
+ @NonNull SharedLog sharedLog) {
+ this(context, looper, new Dependencies(), sharedLog);
}
MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper,
- @NonNull Dependencies deps) {
+ @NonNull Dependencies deps, @NonNull SharedLog sharedLog) {
mContext = context;
mLooper = looper;
mHandler = new Handler(looper);
mDependencies = deps;
+ mSharedLog = sharedLog;
mNetworkCallback = new NetworkCallback() {
@Override
public void onLost(Network network) {
@@ -135,8 +197,20 @@
}
};
- mSocketNetlinkMonitor = mDependencies.createSocketNetlinkMonitor(mHandler, LOGGER,
- new NetLinkMessageProcessor());
+ mSocketNetlinkMonitor = mDependencies.createSocketNetlinkMonitor(mHandler,
+ mSharedLog.forSubComponent("NetlinkMonitor"), new NetLinkMessageProcessor());
+
+ // Register a intent receiver to listen wifi p2p interface changes.
+ // Note: The wifi p2p interface change is only notified via
+ // TetheringEventCallback#onLocalOnlyInterfacesChanged if the device is the wifi p2p group
+ // owner. In this case, MdnsSocketProvider will receive duplicate interface changes and must
+ // ignore the later notification because the socket has already been created. There is only
+ // one notification from the wifi p2p connection change intent if the device is not the wifi
+ // p2p group owner.
+ final IntentFilter intentFilter =
+ new IntentFilter(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+ mContext.registerReceiver(
+ mIntentReceiver, intentFilter, null /* broadcastPermission */, mHandler);
}
/**
@@ -253,7 +327,7 @@
Log.d(TAG, "Already monitoring sockets.");
return;
}
- LOGGER.i("Start monitoring sockets.");
+ mSharedLog.i("Start monitoring sockets.");
mContext.getSystemService(ConnectivityManager.class).registerNetworkCallback(
new NetworkRequest.Builder().clearCapabilities().build(),
mNetworkCallback, mHandler);
@@ -282,7 +356,7 @@
// Only unregister the network callback if there is no socket request.
if (mCallbacksToRequestedNetworks.isEmpty()) {
- LOGGER.i("Stop monitoring sockets.");
+ mSharedLog.i("Stop monitoring sockets.");
mContext.getSystemService(ConnectivityManager.class)
.unregisterNetworkCallback(mNetworkCallback);
@@ -375,17 +449,28 @@
if (!hasAllNetworksRequest()) {
// Currently, the network for tethering can not be requested, so the sockets for
// tethering are only created if there is a request for all networks (interfaces).
- // Therefore, this change can skip if there is no such request.
+ // Therefore, only update the interface list and skip this change if no such request.
if (DBG) {
Log.d(TAG, "Ignore tether interfaces change. There is no request for all"
+ " networks.");
}
+ current.clear();
+ current.addAll(updated);
return;
}
final CompareResult<String> interfaceDiff = new CompareResult<>(
current, updated);
for (String name : interfaceDiff.added) {
+ // Check if a socket has been created for the interface
+ final SocketInfo socketInfo = mTetherInterfaceSockets.get(name);
+ if (socketInfo != null) {
+ if (DBG) {
+ mSharedLog.i("Socket is existed for interface:" + name);
+ }
+ continue;
+ }
+
int ifaceIndex = mDependencies.getNetworkInterfaceIndexByName(name);
createSocket(LOCAL_NET, createLPForTetheredInterface(name, ifaceIndex));
}
@@ -420,7 +505,7 @@
return;
}
- LOGGER.log("Create socket on net:" + networkKey + ", ifName:" + interfaceName);
+ mSharedLog.log("Create socket on net:" + networkKey + ", ifName:" + interfaceName);
final MdnsInterfaceSocket socket = mDependencies.createMdnsInterfaceSocket(
networkInterface.getNetworkInterface(), MdnsConstants.MDNS_PORT, mLooper,
mPacketReadBuffer);
@@ -441,7 +526,7 @@
notifySocketCreated(((NetworkAsKey) networkKey).mNetwork, socket, addresses);
}
} catch (IOException e) {
- LOGGER.e("Create socket failed ifName:" + interfaceName, e);
+ mSharedLog.e("Create socket failed ifName:" + interfaceName, e);
}
}
@@ -470,7 +555,7 @@
// transports above in priority.
return iface.supportsMulticast();
} catch (SocketException e) {
- LOGGER.e("Error checking interface flags", e);
+ mSharedLog.e("Error checking interface flags", e);
return false;
}
}
@@ -481,7 +566,7 @@
socketInfo.mSocket.destroy();
notifyInterfaceDestroyed(network, socketInfo.mSocket);
- LOGGER.log("Remove socket on net:" + network);
+ mSharedLog.log("Remove socket on net:" + network);
}
private void removeTetherInterfaceSocket(String interfaceName) {
@@ -489,7 +574,7 @@
if (socketInfo == null) return;
socketInfo.mSocket.destroy();
notifyInterfaceDestroyed(null /* network */, socketInfo.mSocket);
- LOGGER.log("Remove socket on ifName:" + interfaceName);
+ mSharedLog.log("Remove socket on ifName:" + interfaceName);
}
private void notifySocketCreated(Network network, MdnsInterfaceSocket socket,
@@ -561,6 +646,7 @@
*/
public void requestSocket(@Nullable Network network, @NonNull SocketCallback cb) {
ensureRunningOnHandlerThread(mHandler);
+ mSharedLog.log("requestSocket for net:" + network);
mCallbacksToRequestedNetworks.put(cb, network);
if (network == null) {
// Does not specify a required network, create sockets for all possible
@@ -576,6 +662,11 @@
for (String tetheredInterface : mTetheredInterfaces) {
retrieveAndNotifySocketFromInterface(tetheredInterface, cb);
}
+
+ if (mWifiP2pTetherInterface != null
+ && !mLocalOnlyInterfaces.contains(mWifiP2pTetherInterface)) {
+ retrieveAndNotifySocketFromInterface(mWifiP2pTetherInterface, cb);
+ }
} else {
retrieveAndNotifySocketFromNetwork(network, cb);
}
@@ -584,6 +675,7 @@
/*** Unrequest the socket */
public void unrequestSocket(@NonNull SocketCallback cb) {
ensureRunningOnHandlerThread(mHandler);
+ mSharedLog.log("unrequestSocket");
mCallbacksToRequestedNetworks.remove(cb);
if (hasAllNetworksRequest()) {
// Still has a request for all networks (interfaces).
@@ -596,9 +688,7 @@
if (matchRequestedNetwork(network)) continue;
final SocketInfo info = mNetworkSockets.removeAt(i);
info.mSocket.destroy();
- // Still notify to unrequester for socket destroy.
- cb.onInterfaceDestroyed(network, info.mSocket);
- LOGGER.log("Remove socket on net:" + network + " after unrequestSocket");
+ mSharedLog.log("Remove socket on net:" + network + " after unrequestSocket");
}
// Remove all sockets for tethering interface because these sockets do not have associated
@@ -607,9 +697,7 @@
for (int i = mTetherInterfaceSockets.size() - 1; i >= 0; i--) {
final SocketInfo info = mTetherInterfaceSockets.valueAt(i);
info.mSocket.destroy();
- // Still notify to unrequester for socket destroy.
- cb.onInterfaceDestroyed(null /* network */, info.mSocket);
- LOGGER.log("Remove socket on ifName:" + mTetherInterfaceSockets.keyAt(i)
+ mSharedLog.log("Remove socket on ifName:" + mTetherInterfaceSockets.keyAt(i)
+ " after unrequestSocket");
}
mTetherInterfaceSockets.clear();
@@ -618,10 +706,6 @@
maybeStopMonitoringSockets();
}
- /** Dump info to dumpsys */
- public void dump(PrintWriter pw) {
- LOGGER.reverseDump(pw);
- }
/*** Callbacks for listening socket changes */
public interface SocketCallback {
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 5cc789f..3added6 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
@@ -21,6 +21,9 @@
import android.net.Network;
import android.os.Handler;
+import com.android.server.connectivity.mdns.MdnsConstants;
+import com.android.server.connectivity.mdns.MdnsRecord;
+
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
@@ -68,12 +71,20 @@
/*** Ensure that current running thread is same as given handler thread */
public static void ensureRunningOnHandlerThread(@NonNull Handler handler) {
- if (handler.getLooper().getThread() != Thread.currentThread()) {
+ if (!isRunningOnHandlerThread(handler)) {
throw new IllegalStateException(
"Not running on Handler thread: " + Thread.currentThread().getName());
}
}
+ /*** Check that current running thread is same as given handler thread */
+ public static boolean isRunningOnHandlerThread(@NonNull Handler handler) {
+ if (handler.getLooper().getThread() == Thread.currentThread()) {
+ return true;
+ }
+ return false;
+ }
+
/*** Check whether the target network is matched current network */
public static boolean isNetworkMatched(@Nullable Network targetNetwork,
@Nullable Network currentNetwork) {
@@ -97,4 +108,15 @@
encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */);
return new String(out.array(), 0, out.position(), utf8);
}
+
+ /**
+ * Checks if the MdnsRecord needs to be renewed or not.
+ *
+ * <p>As per RFC6762 7.1 no need to query if remaining TTL is more than half the original one,
+ * so send the queries if half the TTL has passed.
+ */
+ public static boolean isRecordRenewalNeeded(@NonNull MdnsRecord mdnsRecord, final long now) {
+ return mdnsRecord.getTtl() > 0
+ && mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
+ }
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index eed9aeb..6776920 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -20,7 +20,6 @@
import android.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityManager;
-import android.net.ConnectivityResources;
import android.net.EthernetManager;
import android.net.EthernetNetworkSpecifier;
import android.net.IpConfiguration;
@@ -50,6 +49,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.net.module.util.InterfaceParams;
+import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
import java.util.Objects;
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index d520757..1f22b02 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -25,7 +25,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.net.ConnectivityResources;
import android.net.EthernetManager;
import android.net.IEthernetServiceListener;
import android.net.INetd;
@@ -57,6 +56,7 @@
import com.android.net.module.util.netlink.NetlinkMessage;
import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
import com.android.net.module.util.netlink.StructIfinfoMsg;
+import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
import java.net.InetAddress;
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index c660792..f977a27 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -83,7 +83,6 @@
import android.content.res.Resources;
import android.database.ContentObserver;
import android.net.ConnectivityManager;
-import android.net.ConnectivityResources;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsService;
@@ -175,6 +174,7 @@
import com.android.networkstack.apishim.ConstantsShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.server.BpfNetMaps;
+import com.android.server.connectivity.ConnectivityResources;
import java.io.File;
import java.io.FileDescriptor;
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 3e4c4de..08d31a3 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -38,7 +38,7 @@
#include "jni.h"
#include <android-base/stringprintf.h>
#include <android-base/unique_fd.h>
-#include <bpf/KernelVersion.h>
+#include <bpf/KernelUtils.h>
#include <nativehelper/JNIHelp.h>
#include <nativehelper/ScopedUtfChars.h>
diff --git a/service/libconnectivity/Android.bp b/service/libconnectivity/Android.bp
index 391ceac..e063af7 100644
--- a/service/libconnectivity/Android.bp
+++ b/service/libconnectivity/Android.bp
@@ -27,6 +27,7 @@
min_sdk_version: "30",
static_libs: [
"connectivity_native_aidl_interface-V1-ndk",
+ "libmodules-utils-build",
],
export_include_dirs: ["include"],
cflags: [
diff --git a/service/libconnectivity/src/connectivity_native.cpp b/service/libconnectivity/src/connectivity_native.cpp
index a476498..f39bb0a 100644
--- a/service/libconnectivity/src/connectivity_native.cpp
+++ b/service/libconnectivity/src/connectivity_native.cpp
@@ -17,6 +17,7 @@
#include "connectivity_native.h"
#include <android/binder_manager.h>
+#include <android-modules-utils/sdk_level.h>
#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h>
using aidl::android::net::connectivity::aidl::IConnectivityNative;
@@ -44,6 +45,7 @@
}
int AConnectivityNative_blockPortForBind(in_port_t port) {
+ if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
std::shared_ptr<IConnectivityNative> c = getBinder();
if (!c) {
return EAGAIN;
@@ -52,6 +54,7 @@
}
int AConnectivityNative_unblockPortForBind(in_port_t port) {
+ if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
std::shared_ptr<IConnectivityNative> c = getBinder();
if (!c) {
return EAGAIN;
@@ -60,6 +63,7 @@
}
int AConnectivityNative_unblockAllPortsForBind() {
+ if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
std::shared_ptr<IConnectivityNative> c = getBinder();
if (!c) {
return EAGAIN;
@@ -68,6 +72,7 @@
}
int AConnectivityNative_getPortsBlockedForBind(in_port_t *ports, size_t *count) {
+ if (!android::modules::sdklevel::IsAtLeastU()) return ENOSYS;
std::shared_ptr<IConnectivityNative> c = getBinder();
if (!c) {
return EAGAIN;
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/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index b4fce37..ec168dd 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -281,8 +281,8 @@
if (sEnableJavaBpfMap == null) {
sEnableJavaBpfMap = SdkLevel.isAtLeastU() ||
DeviceConfigUtils.isFeatureEnabled(context,
- DeviceConfig.NAMESPACE_TETHERING, BPF_NET_MAPS_ENABLE_JAVA_BPF_MAP,
- false /* defaultValue */);
+ DeviceConfig.NAMESPACE_TETHERING, BPF_NET_MAPS_ENABLE_JAVA_BPF_MAP,
+ DeviceConfigUtils.TETHERING_MODULE_NAME, false /* defaultValue */);
}
Log.d(TAG, "BpfNetMaps is initialized with sEnableJavaBpfMap=" + sEnableJavaBpfMap);
@@ -384,7 +384,6 @@
* 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
*/
- @VisibleForTesting
public boolean isFirewallAllowList(final int chain) {
switch (chain) {
case FIREWALL_CHAIN_DOZABLE:
@@ -745,6 +744,65 @@
}
}
+ private Set<Integer> getUidsMatchEnabled(final int childChain) throws ErrnoException {
+ final long match = getMatchByFirewallChain(childChain);
+ Set<Integer> uids = new ArraySet<>();
+ synchronized (sUidOwnerMap) {
+ sUidOwnerMap.forEach((uid, val) -> {
+ if (val == null) {
+ Log.wtf(TAG, "sUidOwnerMap entry was deleted while holding a lock");
+ } else {
+ if ((val.rule & match) != 0) {
+ uids.add(uid.val);
+ }
+ }
+ });
+ }
+ return uids;
+ }
+
+ /**
+ * Get uids that has FIREWALL_RULE_ALLOW on allowlist chain.
+ * Allowlist means the firewall denies all by default, uids must be explicitly allowed.
+ *
+ * Note that uids that has FIREWALL_RULE_DENY on allowlist chain can not be computed from the
+ * bpf map, since all the uids that does not have explicit FIREWALL_RULE_ALLOW rule in bpf map
+ * are determined to have FIREWALL_RULE_DENY.
+ *
+ * @param childChain target chain
+ * @return Set of uids
+ */
+ public Set<Integer> getUidsWithAllowRuleOnAllowListChain(final int childChain)
+ throws ErrnoException {
+ if (!isFirewallAllowList(childChain)) {
+ throw new IllegalArgumentException("getUidsWithAllowRuleOnAllowListChain is called with"
+ + " denylist chain:" + childChain);
+ }
+ // Corresponding match is enabled for uids that has FIREWALL_RULE_ALLOW on allowlist chain.
+ return getUidsMatchEnabled(childChain);
+ }
+
+ /**
+ * Get uids that has FIREWALL_RULE_DENY on denylist chain.
+ * Denylist means the firewall allows all by default, uids must be explicitly denyed
+ *
+ * Note that uids that has FIREWALL_RULE_ALLOW on denylist chain can not be computed from the
+ * bpf map, since all the uids that does not have explicit FIREWALL_RULE_DENY rule in bpf map
+ * are determined to have the FIREWALL_RULE_ALLOW.
+ *
+ * @param childChain target chain
+ * @return Set of uids
+ */
+ public Set<Integer> getUidsWithDenyRuleOnDenyListChain(final int childChain)
+ throws ErrnoException {
+ if (isFirewallAllowList(childChain)) {
+ throw new IllegalArgumentException("getUidsWithDenyRuleOnDenyListChain is called with"
+ + " allowlist chain:" + childChain);
+ }
+ // Corresponding match is enabled for uids that has FIREWALL_RULE_DENY on denylist chain.
+ return getUidsMatchEnabled(childChain);
+ }
+
/**
* Add ingress interface filtering rules to a list of UIDs
*
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index ba503e0..f85f336 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -17,6 +17,7 @@
package com.android.server;
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_WATCH;
import static android.content.pm.PackageManager.FEATURE_WIFI;
@@ -33,6 +34,7 @@
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.CALLBACK_IP_CHANGED;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
@@ -91,7 +93,7 @@
import static android.net.OemNetworkPreferences.OEM_NETWORK_PREFERENCE_TEST_ONLY;
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
-import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -110,6 +112,8 @@
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.app.ActivityManager.UidFrozenStateChangedCallback;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.app.PendingIntent;
@@ -134,7 +138,6 @@
import android.net.ConnectivityManager.BlockedReason;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.ConnectivityManager.RestrictBackgroundStatus;
-import android.net.ConnectivityResources;
import android.net.ConnectivitySettingsManager;
import android.net.DataStallReportParcelable;
import android.net.DnsResolverServiceManager;
@@ -244,6 +247,7 @@
import android.util.LocalLog;
import android.util.Log;
import android.util.Pair;
+import android.util.Range;
import android.util.SparseArray;
import android.util.SparseIntArray;
@@ -280,11 +284,13 @@
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
import com.android.server.connectivity.ClatCoordinator;
import com.android.server.connectivity.ConnectivityFlags;
+import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.DnsManager;
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
import com.android.server.connectivity.InvalidTagException;
+import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.MockableSystemProperties;
@@ -310,11 +316,14 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.io.InterruptedIOException;
import java.io.PrintWriter;
import java.io.Writer;
+import java.lang.IllegalArgumentException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
+import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -435,6 +444,8 @@
private final Context mContext;
private final ConnectivityResources mResources;
+ private final int mWakeUpMark;
+ private final int mWakeUpMask;
// The Context is created for UserHandle.ALL.
private final Context mUserAllContext;
private final Dependencies mDeps;
@@ -542,7 +553,7 @@
/**
* PAC manager has received new port.
*/
- private static final int EVENT_PROXY_HAS_CHANGED = 16;
+ private static final int EVENT_PAC_PROXY_HAS_CHANGED = 16;
/**
* used internally when registering NetworkProviders
@@ -783,6 +794,11 @@
private static final int EVENT_SET_LOW_TCP_POLLING_UNTIL = 60;
/**
+ * Event to inform the ConnectivityService handler when a uid has been frozen or unfrozen.
+ */
+ private static final int EVENT_UID_FROZEN_STATE_CHANGED = 61;
+
+ /**
* Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
* should be shown.
*/
@@ -850,8 +866,7 @@
// A helper object to track the current default HTTP proxy. ConnectivityService needs to tell
// the world when it changes.
- @VisibleForTesting
- protected final ProxyTracker mProxyTracker;
+ private final ProxyTracker mProxyTracker;
final private SettingsObserver mSettingsObserver;
@@ -1267,6 +1282,18 @@
return Binder.getCallingUid();
}
+ public boolean isAtLeastS() {
+ return SdkLevel.isAtLeastS();
+ }
+
+ public boolean isAtLeastT() {
+ return SdkLevel.isAtLeastT();
+ }
+
+ public boolean isAtLeastU() {
+ return SdkLevel.isAtLeastU();
+ }
+
/**
* Get system properties to use in ConnectivityService.
*/
@@ -1300,7 +1327,7 @@
*/
public ProxyTracker makeProxyTracker(@NonNull Context context,
@NonNull Handler connServiceHandler) {
- return new ProxyTracker(context, connServiceHandler, EVENT_PROXY_HAS_CHANGED);
+ return new ProxyTracker(context, connServiceHandler, EVENT_PAC_PROXY_HAS_CHANGED);
}
/**
@@ -1379,7 +1406,7 @@
@Nullable
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
@NonNull final Context context, @NonNull final TelephonyManager tm) {
- if (SdkLevel.isAtLeastT()) {
+ if (isAtLeastT()) {
return new CarrierPrivilegeAuthenticator(context, tm);
} else {
return null;
@@ -1389,9 +1416,9 @@
/**
* @see DeviceConfigUtils#isFeatureEnabled
*/
- public boolean isFeatureEnabled(Context context, String name, boolean defaultEnabled) {
- return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
- TETHERING_MODULE_NAME, defaultEnabled);
+ public boolean isFeatureEnabled(Context context, String name) {
+ return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_TETHERING, name,
+ TETHERING_MODULE_NAME, false /* defaultValue */);
}
/**
@@ -1485,6 +1512,53 @@
@NonNull final UserHandle user) {
return CompatChanges.isChangeEnabled(changeId, packageName, user);
}
+
+ /**
+ * As above but with a UID.
+ * @see CompatChanges#isChangeEnabled(long, int)
+ */
+ public boolean isChangeEnabled(final long changeId, final int uid) {
+ return CompatChanges.isChangeEnabled(changeId, uid);
+ }
+
+ /**
+ * Call {@link InetDiagMessage#destroyLiveTcpSockets(Set, Set)}
+ *
+ * @param ranges target uid ranges
+ * @param exemptUids uids to skip close socket
+ */
+ public void destroyLiveTcpSockets(@NonNull final Set<Range<Integer>> ranges,
+ @NonNull final Set<Integer> exemptUids)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ InetDiagMessage.destroyLiveTcpSockets(ranges, exemptUids);
+ }
+
+ /**
+ * Call {@link InetDiagMessage#destroyLiveTcpSocketsByOwnerUids(Set)}
+ *
+ * @param ownerUids target uids to close sockets
+ */
+ public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ InetDiagMessage.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+ }
+
+ /**
+ * Schedule the evaluation timeout.
+ *
+ * When a network connects, it's "not evaluated" yet. Detection events cause the network
+ * to be "evaluated" (typically, validation or detection of a captive portal). If none
+ * of these events happen, this time will run out, after which the network is considered
+ * "evaluated" even if nothing happened to it. Notionally that means the system gave up
+ * on this network and considers it won't provide connectivity. In particular, that means
+ * it's when the system prefers it to cell if it's wifi and configuration says it should
+ * prefer bad wifi to cell.
+ */
+ public void scheduleEvaluationTimeout(@NonNull Handler handler,
+ @NonNull final Network network, final long delayMs) {
+ handler.sendMessageDelayed(
+ handler.obtainMessage(EVENT_INITIAL_EVALUATION_TIMEOUT, network), delayMs);
+ }
}
public ConnectivityService(Context context) {
@@ -1541,6 +1615,29 @@
mCellularRadioTimesharingCapable =
mResources.get().getBoolean(R.bool.config_cellular_radio_timesharing_capable);
+ int mark = mResources.get().getInteger(R.integer.config_networkWakeupPacketMark);
+ int mask = mResources.get().getInteger(R.integer.config_networkWakeupPacketMask);
+
+ if (SdkLevel.isAtLeastU()) {
+ // U+ default value of both mark & mask, this is the top bit of the skb->mark,
+ // see //system/netd/include/FwMark.h union Fwmark, field ingress_cpu_wakeup
+ final int defaultUMarkMask = 0x80000000; // u32
+
+ if ((mark == 0) || (mask == 0)) {
+ // simply treat unset/disabled as the default U value
+ mark = defaultUMarkMask;
+ mask = defaultUMarkMask;
+ }
+ if ((mark != defaultUMarkMask) || (mask != defaultUMarkMask)) {
+ // invalid device overlay settings
+ throw new IllegalArgumentException(
+ "Bad config_networkWakeupPacketMark/Mask " + mark + "/" + mask);
+ }
+ }
+
+ mWakeUpMark = mark;
+ mWakeUpMask = mask;
+
mNetd = netd;
mBpfNetMaps = mDeps.getBpfNetMaps(mContext, netd);
mHandlerThread = mDeps.makeHandlerThread();
@@ -1660,7 +1757,7 @@
// Even if it could, running on S would at least require mocking out the BPF map,
// otherwise the unit tests will fail on pre-T devices where the seccomp filter blocks
// the bpf syscall. http://aosp/1907693
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
mDscpPolicyTracker = new DscpPolicyTracker();
}
} catch (ErrnoException e) {
@@ -1670,11 +1767,37 @@
mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
mContext);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
mCdmps = new CompanionDeviceManagerProxyService(context);
} else {
mCdmps = null;
}
+
+ if (mDeps.isAtLeastU()
+ && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION)) {
+ final UidFrozenStateChangedCallback frozenStateChangedCallback =
+ new UidFrozenStateChangedCallback() {
+ @Override
+ public void onUidFrozenStateChanged(int[] uids, int[] frozenStates) {
+ if (uids.length != frozenStates.length) {
+ Log.wtf(TAG, "uids has length " + uids.length
+ + " but frozenStates has length " + frozenStates.length);
+ return;
+ }
+
+ final UidFrozenStateChangedArgs args =
+ new UidFrozenStateChangedArgs(uids, frozenStates);
+
+ mHandler.sendMessage(
+ mHandler.obtainMessage(EVENT_UID_FROZEN_STATE_CHANGED, args));
+ }
+ };
+
+ final ActivityManager activityManager =
+ mContext.getSystemService(ActivityManager.class);
+ activityManager.registerUidFrozenStateChangedCallback(
+ (Runnable r) -> r.run(), frozenStateChangedCallback);
+ }
}
/**
@@ -1770,6 +1893,13 @@
mHandler.sendEmptyMessage(EVENT_INGRESS_RATE_LIMIT_CHANGED);
}
+ @VisibleForTesting
+ void simulateUpdateProxyInfo(@Nullable final Network network,
+ @NonNull final ProxyInfo proxyInfo) {
+ Message.obtain(mHandler, EVENT_PAC_PROXY_HAS_CHANGED,
+ new Pair<>(network, proxyInfo)).sendToTarget();
+ }
+
private void handleAlwaysOnNetworkRequest(
NetworkRequest networkRequest, String settingName, boolean defaultValue) {
final boolean enable = toBool(Settings.Global.getInt(
@@ -2603,7 +2733,8 @@
final ArrayList<NetworkStateSnapshot> result = new ArrayList<>();
for (Network network : getAllNetworks()) {
final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
- if (nai != null && nai.everConnected()) {
+ final boolean includeNetwork = (nai != null) && nai.isCreated();
+ if (includeNetwork) {
// TODO (b/73321673) : NetworkStateSnapshot contains a copy of the
// NetworkCapabilities, which may contain UIDs of apps to which the
// network applies. Should the UIDs be cleared so as not to leak or
@@ -2843,6 +2974,39 @@
setUidBlockedReasons(uid, blockedReasons);
}
+ static final class UidFrozenStateChangedArgs {
+ final int[] mUids;
+ final int[] mFrozenStates;
+
+ UidFrozenStateChangedArgs(int[] uids, int[] frozenStates) {
+ mUids = uids;
+ mFrozenStates = frozenStates;
+ }
+ }
+
+ private void handleFrozenUids(int[] uids, int[] frozenStates) {
+ final ArraySet<Range<Integer>> ranges = new ArraySet<>();
+
+ for (int i = 0; i < uids.length; i++) {
+ if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
+ Integer uidAsInteger = Integer.valueOf(uids[i]);
+ ranges.add(new Range(uidAsInteger, uidAsInteger));
+ }
+ }
+
+ if (!ranges.isEmpty()) {
+ final Set<Integer> exemptUids = new ArraySet<>();
+ try {
+ mDeps.destroyLiveTcpSockets(ranges, exemptUids);
+ } catch (Exception e) {
+ loge("Exception in socket destroy: " + e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static final String KEY_DESTROY_FROZEN_SOCKETS_VERSION = "destroy_frozen_sockets_version";
+
private void enforceInternetPermission() {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERNET,
@@ -3121,7 +3285,7 @@
private void applyMostRecentPolicyForConnectivityAction(BroadcastOptions options,
NetworkInfo info) {
// Delivery group policy APIs are only available on U+.
- if (!SdkLevel.isAtLeastU()) return;
+ if (!mDeps.isAtLeastU()) return;
final BroadcastOptionsShim optsShim = mDeps.makeBroadcastOptionsShim(options);
try {
@@ -3199,7 +3363,7 @@
}
// On T+ devices, register callback for statsd to pull NETWORK_BPF_MAP_INFO atom
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
mBpfNetMaps.setPullAtomCallback(mContext);
}
// Wait PermissionMonitor to finish the permission update. Then MultipathPolicyTracker won't
@@ -3796,9 +3960,9 @@
break;
}
case NetworkAgent.EVENT_UNREGISTER_AFTER_REPLACEMENT: {
- if (!nai.isCreated()) {
- Log.d(TAG, "unregisterAfterReplacement on uncreated " + nai.toShortString()
- + ", tearing down instead");
+ if (!nai.everConnected()) {
+ Log.d(TAG, "unregisterAfterReplacement on never-connected "
+ + nai.toShortString() + ", tearing down instead");
teardownUnneededNetwork(nai);
break;
}
@@ -4383,6 +4547,25 @@
}
}
+ @VisibleForTesting
+ protected static boolean shouldCreateNetworksImmediately() {
+ // Before U, physical networks are only created when the agent advances to CONNECTED.
+ // In U and above, all networks are immediately created when the agent is registered.
+ return SdkLevel.isAtLeastU();
+ }
+
+ private static boolean shouldCreateNativeNetwork(@NonNull NetworkAgentInfo nai,
+ @NonNull NetworkInfo.State state) {
+ if (nai.isCreated()) return false;
+ if (state == NetworkInfo.State.CONNECTED) return true;
+ if (state != NetworkInfo.State.CONNECTING) {
+ // TODO: throw if no WTFs are observed in the field.
+ Log.wtf(TAG, "Uncreated network in invalid state: " + state);
+ return false;
+ }
+ return nai.isVPN() || shouldCreateNetworksImmediately();
+ }
+
private static boolean shouldDestroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
return nai.isCreated() && !nai.isDestroyed();
}
@@ -4390,7 +4573,7 @@
@VisibleForTesting
boolean shouldIgnoreValidationFailureAfterRoam(NetworkAgentInfo nai) {
// T+ devices should use unregisterAfterReplacement.
- if (SdkLevel.isAtLeastT()) return false;
+ if (mDeps.isAtLeastT()) return false;
// If the network never roamed, return false. The check below is not sufficient if time
// since boot is less than blockTimeOut, though that's extremely unlikely to happen.
@@ -4433,7 +4616,7 @@
// because they lost all their requests or because their score isn't good)
// then they would disconnect organically, report their new state and then
// disconnect the channel.
- if (nai.networkInfo.isConnected()) {
+ if (nai.networkInfo.isConnected() || nai.networkInfo.isSuspended()) {
nai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
null, null);
}
@@ -4501,6 +4684,21 @@
rematchAllNetworksAndRequests();
mLingerMonitor.noteDisconnect(nai);
+ if (null == getDefaultNetwork() && nai.linkProperties.getHttpProxy() != null) {
+ // The obvious place to do this would be in makeDefault(), however makeDefault() is
+ // not called by the rematch in this case. This is because the code above unset
+ // this network from the default request's satisfier, and that is what the rematch
+ // is using as its source data to know what the old satisfier was. So as far as the
+ // rematch above is concerned, the old default network was null.
+ // Therefore if there is no new default, the default network was null and is still
+ // null, thus there was no change so makeDefault() is not called. So if the old
+ // network had a proxy and there is no new default, the proxy tracker should be told
+ // that there is no longer a default proxy.
+ // Strictly speaking this is not essential because having a proxy setting when
+ // there is no network is harmless, but it's still counter-intuitive so reset to null.
+ mProxyTracker.setDefaultProxy(null);
+ }
+
// Immediate teardown.
if (nai.teardownDelayMs == 0) {
destroyNetwork(nai);
@@ -4529,7 +4727,7 @@
// for an unnecessarily long time.
destroyNativeNetwork(nai);
}
- if (!nai.isCreated() && !SdkLevel.isAtLeastT()) {
+ if (!nai.isCreated() && !mDeps.isAtLeastT()) {
// Backwards compatibility: send onNetworkDestroyed even if network was never created.
// This can never run if the code above runs because shouldDestroyNativeNetwork is
// false if the network was never created.
@@ -4613,7 +4811,7 @@
}
private void checkNrisConsistency(final NetworkRequestInfo nri) {
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
for (final NetworkRequestInfo n : mNetworkRequests.values()) {
if (n.mBinder != null && n.mBinder == nri.mBinder) {
// Temporary help to debug b/194394697 ; TODO : remove this function when the
@@ -5160,8 +5358,7 @@
/** Schedule evaluation timeout */
@VisibleForTesting
public void scheduleEvaluationTimeout(@NonNull final Network network, final long delayMs) {
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(EVENT_INITIAL_EVALUATION_TIMEOUT, network), delayMs);
+ mDeps.scheduleEvaluationTimeout(mHandler, network, delayMs);
}
@Override
@@ -5524,9 +5721,9 @@
mProxyTracker.loadDeprecatedGlobalHttpProxy();
break;
}
- case EVENT_PROXY_HAS_CHANGED: {
+ case EVENT_PAC_PROXY_HAS_CHANGED: {
final Pair<Network, ProxyInfo> arg = (Pair<Network, ProxyInfo>) msg.obj;
- handleApplyDefaultProxy(arg.second);
+ handlePacProxyServiceStarted(arg.first, arg.second);
break;
}
case EVENT_REGISTER_NETWORK_PROVIDER: {
@@ -5706,6 +5903,10 @@
mKeepaliveTracker.handleSetTestLowTcpPollingTimer(time);
break;
}
+ case EVENT_UID_FROZEN_STATE_CHANGED:
+ UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
+ handleFrozenUids(args.mUids, args.mFrozenStates);
+ break;
}
}
}
@@ -5977,12 +6178,19 @@
return mProxyTracker.getGlobalProxy();
}
- private void handleApplyDefaultProxy(@Nullable ProxyInfo proxy) {
- if (proxy != null && TextUtils.isEmpty(proxy.getHost())
- && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
- proxy = null;
- }
+ private void handlePacProxyServiceStarted(@Nullable Network net, @Nullable ProxyInfo proxy) {
mProxyTracker.setDefaultProxy(proxy);
+ final NetworkAgentInfo nai = getDefaultNetwork();
+ // TODO : this method should check that net == nai.network, unfortunately at this point
+ // 'net' is always null in practice (see PacProxyService#sendPacBroadcast). PAC proxy
+ // is only ever installed on the default network so in practice this is okay.
+ if (null == nai) return;
+ // PAC proxies only work on the default network. Therefore, only the default network
+ // should have its link properties fixed up for PAC proxies.
+ mProxyTracker.updateDefaultNetworkProxyPortForPAC(nai.linkProperties, nai.network);
+ if (nai.everConnected()) {
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_IP_CHANGED);
+ }
}
// If the proxy has changed from oldLp to newLp, resend proxy broadcast. This method gets called
@@ -6200,7 +6408,7 @@
+ Arrays.toString(ranges) + "): netd command failed: " + e);
}
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
}
@@ -6994,7 +7202,7 @@
@NonNull final NetworkCapabilities networkCapabilities) {
// This check is added to fix the linter error for "current min is 30", which is not going
// to happen because Connectivity service always run in S+.
- if (!SdkLevel.isAtLeastS()) {
+ if (!mDeps.isAtLeastS()) {
Log.wtf(TAG, "Connectivity service should always run in at least SDK S");
return;
}
@@ -7808,6 +8016,7 @@
// updateMtu(lp, null);
// }
if (isDefaultNetwork(networkAgent)) {
+ mProxyTracker.updateDefaultNetworkProxyPortForPAC(newLp, null);
updateTcpBufferSizes(newLp.getTcpBufferSizes());
}
@@ -7820,8 +8029,8 @@
mDnsManager.updatePrivateDnsStatus(netId, newLp);
if (isDefaultNetwork(networkAgent)) {
- handleApplyDefaultProxy(newLp.getHttpProxy());
- } else {
+ mProxyTracker.setDefaultProxy(newLp.getHttpProxy());
+ } else if (networkAgent.everConnected()) {
updateProxy(newLp, oldLp);
}
@@ -7855,6 +8064,10 @@
mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
}
+ private void applyInitialLinkProperties(@NonNull NetworkAgentInfo nai) {
+ updateLinkProperties(nai, new LinkProperties(nai.linkProperties), null);
+ }
+
/**
* @param naData captive portal data from NetworkAgent
* @param apiData captive portal data from capport API
@@ -7904,34 +8117,42 @@
return captivePortalBuilder.build();
}
- private String makeNflogPrefix(String iface, long networkHandle) {
+ @VisibleForTesting
+ static String makeNflogPrefix(String iface, long networkHandle) {
// This needs to be kept in sync and backwards compatible with the decoding logic in
// NetdEventListenerService, which is non-mainline code.
return SdkLevel.isAtLeastU() ? (networkHandle + ":" + iface) : ("iface:" + iface);
}
+ private static boolean isWakeupMarkingSupported(NetworkCapabilities capabilities) {
+ if (capabilities.hasTransport(TRANSPORT_WIFI)) {
+ return true;
+ }
+ if (SdkLevel.isAtLeastU() && capabilities.hasTransport(TRANSPORT_CELLULAR)) {
+ return true;
+ }
+ return false;
+ }
+
private void wakeupModifyInterface(String iface, NetworkAgentInfo nai, boolean add) {
// Marks are only available on WiFi interfaces. Checking for
// marks on unsupported interfaces is harmless.
- if (!nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ if (!isWakeupMarkingSupported(nai.networkCapabilities)) {
return;
}
- int mark = mResources.get().getInteger(R.integer.config_networkWakeupPacketMark);
- int mask = mResources.get().getInteger(R.integer.config_networkWakeupPacketMask);
-
// Mask/mark of zero will not detect anything interesting.
// Don't install rules unless both values are nonzero.
- if (mark == 0 || mask == 0) {
+ if (mWakeUpMark == 0 || mWakeUpMask == 0) {
return;
}
final String prefix = makeNflogPrefix(iface, nai.network.getNetworkHandle());
try {
if (add) {
- mNetd.wakeupAddInterface(iface, prefix, mark, mask);
+ mNetd.wakeupAddInterface(iface, prefix, mWakeUpMark, mWakeUpMask);
} else {
- mNetd.wakeupDelInterface(iface, prefix, mark, mask);
+ mNetd.wakeupDelInterface(iface, prefix, mWakeUpMark, mWakeUpMask);
}
} catch (Exception e) {
loge("Exception modifying wakeup packet monitoring: " + e);
@@ -8416,7 +8637,7 @@
// On T and above, allow rules are needed for all VPNs. Allow rule with null iface is a
// wildcard to allow apps to receive packets on all interfaces. This is required to accept
// incoming traffic in Lockdown mode by overriding the Lockdown blocking rule.
- return SdkLevel.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
+ return mDeps.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
}
private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8448,11 +8669,19 @@
return stableRanges;
}
- private void maybeCloseSockets(NetworkAgentInfo nai, UidRangeParcel[] ranges,
- int[] exemptUids) {
+ private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges,
+ UidRangeParcel[] uidRangeParcels, int[] exemptUids) {
if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
try {
- mNetd.socketDestroy(ranges, exemptUids);
+ if (mDeps.isAtLeastU()) {
+ final Set<Integer> exemptUidSet = new ArraySet<>();
+ for (final int uid: exemptUids) {
+ exemptUidSet.add(uid);
+ }
+ mDeps.destroyLiveTcpSockets(UidRange.toIntRanges(ranges), exemptUidSet);
+ } else {
+ mNetd.socketDestroy(uidRangeParcels, exemptUids);
+ }
} catch (Exception e) {
loge("Exception in socket destroy: ", e);
}
@@ -8469,7 +8698,7 @@
UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges);
// Close sockets before modifying uid ranges so that RST packets can reach to the server.
- maybeCloseSockets(nai, ranges, exemptUids);
+ maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
try {
if (add) {
mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
@@ -8483,7 +8712,7 @@
" on netId " + nai.network.netId + ". " + e);
}
// Close sockets that established connection while requesting netd.
- maybeCloseSockets(nai, ranges, exemptUids);
+ maybeCloseSockets(nai, uidRanges, ranges, exemptUids);
}
private boolean isProxySetOnAnyDefaultNetwork() {
@@ -8666,14 +8895,14 @@
try {
if (DBG) log("Sending " + pendingIntent);
final BroadcastOptions options = BroadcastOptions.makeBasic();
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
// Explicitly disallow the receiver from starting activities, to prevent apps from
// utilizing the PendingIntent as a backdoor to do this.
options.setPendingIntentBackgroundActivityLaunchAllowed(false);
}
pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */,
null /* requiredPermission */,
- SdkLevel.isAtLeastT() ? options.toBundle() : null);
+ mDeps.isAtLeastT() ? options.toBundle() : null);
} catch (PendingIntent.CanceledException e) {
if (DBG) log(pendingIntent + " was not sent, it had been canceled.");
mPendingIntentWakeLock.release();
@@ -8854,6 +9083,17 @@
}
}
+ private void resetHttpProxyForNonDefaultNetwork(NetworkAgentInfo oldDefaultNetwork) {
+ if (null == oldDefaultNetwork) return;
+ // The network stopped being the default. If it was using a PAC proxy, then the
+ // proxy needs to be reset, otherwise HTTP requests on this network may be sent
+ // to the local proxy server, which would forward them over the newly default network.
+ final ProxyInfo proxyInfo = oldDefaultNetwork.linkProperties.getHttpProxy();
+ if (null == proxyInfo || !proxyInfo.isPacProxy()) return;
+ oldDefaultNetwork.linkProperties.setHttpProxy(new ProxyInfo(proxyInfo.getPacFileUrl()));
+ notifyNetworkCallbacks(oldDefaultNetwork, CALLBACK_IP_CHANGED);
+ }
+
private void makeDefault(@NonNull final NetworkRequestInfo nri,
@Nullable final NetworkAgentInfo oldDefaultNetwork,
@Nullable final NetworkAgentInfo newDefaultNetwork) {
@@ -8879,8 +9119,9 @@
mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
}
mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
- handleApplyDefaultProxy(null != newDefaultNetwork
+ mProxyTracker.setDefaultProxy(null != newDefaultNetwork
? newDefaultNetwork.linkProperties.getHttpProxy() : null);
+ resetHttpProxyForNonDefaultNetwork(oldDefaultNetwork);
updateTcpBufferSizes(null != newDefaultNetwork
? newDefaultNetwork.linkProperties.getTcpBufferSizes() : null);
notifyIfacesChangedForNetworkStats();
@@ -8952,7 +9193,7 @@
private void updateProfileAllowedNetworks() {
// Netd command is not implemented before U.
- if (!SdkLevel.isAtLeastU()) return;
+ if (!mDeps.isAtLeastU()) return;
ensureRunningOnConnectivityServiceThread();
final ArrayList<NativeUidRangeConfig> configs = new ArrayList<>();
@@ -9606,21 +9847,32 @@
+ oldInfo.getState() + " to " + state);
}
- if (!networkAgent.isCreated()
- && (state == NetworkInfo.State.CONNECTED
- || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
-
+ if (shouldCreateNativeNetwork(networkAgent, state)) {
// A network that has just connected has zero requests and is thus a foreground network.
networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
if (!createNativeNetwork(networkAgent)) return;
+
+ networkAgent.setCreated();
+
+ // If the network is created immediately on register, then apply the LinkProperties now.
+ // Otherwise, this is done further down when the network goes into connected state.
+ // Applying the LinkProperties means that the network is ready to carry traffic -
+ // interfaces and routing rules have been added, DNS servers programmed, etc.
+ // For VPNs, this must be done before the capabilities are updated, because as soon as
+ // that happens, UIDs are routed to the network.
+ if (shouldCreateNetworksImmediately()) {
+ applyInitialLinkProperties(networkAgent);
+ }
+
+ // TODO: should this move earlier? It doesn't seem to have anything to do with whether
+ // a network is created or not.
if (networkAgent.propagateUnderlyingCapabilities()) {
// Initialize the network's capabilities to their starting values according to the
// underlying networks. This ensures that the capabilities are correct before
// anything happens to the network.
updateCapabilitiesForNetwork(networkAgent);
}
- networkAgent.setCreated();
networkAgent.onNetworkCreated();
updateAllowedUids(networkAgent, null, networkAgent.networkCapabilities);
updateProfileAllowedNetworks();
@@ -9634,8 +9886,19 @@
networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);
handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
- updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
- null);
+ if (!shouldCreateNetworksImmediately()) {
+ applyInitialLinkProperties(networkAgent);
+ } else {
+ // The network was created when the agent registered, and the LinkProperties are
+ // already up-to-date. However, updateLinkProperties also makes some changes only
+ // when the network connects. Apply those changes here. On T and below these are
+ // handled by the applyInitialLinkProperties call just above.
+ // TODO: stop relying on updateLinkProperties(..., null) to do this.
+ // If something depends on both LinkProperties and connected state, it should be in
+ // this method as well.
+ networkAgent.clatd.update();
+ updateProxy(networkAgent.linkProperties, null);
+ }
// 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
@@ -9666,13 +9929,13 @@
// in the absence of a satisfactory, scalable solution which follows an easy/standard
// process to check the interface version, just use an SDK check. NetworkStack will
// always be new enough when running on T+.
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
networkAgent.networkMonitor().notifyNetworkConnected(params);
} else {
networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
params.networkCapabilities);
}
- final long delay = activelyPreferBadWifi()
+ final long delay = !avoidBadWifi() && activelyPreferBadWifi()
? ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS
: DONT_ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS;
scheduleEvaluationTimeout(networkAgent.network, delay);
@@ -10021,6 +10284,16 @@
}
@Override
+ public int[] getSupportedKeepalives() {
+ enforceAnyPermissionOf(mContext, android.Manifest.permission.NETWORK_SETTINGS,
+ // Backwards compatibility with CTS 13
+ android.Manifest.permission.QUERY_ALL_PACKAGES);
+
+ return BinderUtils.withCleanCallingIdentity(() ->
+ KeepaliveResourceUtil.getSupportedKeepalives(mContext));
+ }
+
+ @Override
public void factoryReset() {
enforceSettingsPermission();
@@ -11132,7 +11405,7 @@
if (um.isManagedProfile(profile.getIdentifier())) {
return true;
}
- if (SdkLevel.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
+ if (mDeps.isAtLeastT() && dpm.getDeviceOwner() != null) return true;
return false;
}
@@ -11414,7 +11687,7 @@
private boolean canNetworkBeRateLimited(@NonNull final NetworkAgentInfo networkAgent) {
// Rate-limiting cannot run correctly before T because the BPF program is not loaded.
- if (!SdkLevel.isAtLeastT()) return false;
+ if (!mDeps.isAtLeastT()) return false;
final NetworkCapabilities agentCaps = networkAgent.networkCapabilities;
// Only test networks (they cannot hold NET_CAPABILITY_INTERNET) and networks that provide
@@ -11894,6 +12167,23 @@
return rule;
}
+ private void closeSocketsForFirewallChainLocked(final int chain)
+ throws ErrnoException, SocketException, InterruptedIOException {
+ if (mBpfNetMaps.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<>();
+ ranges.add(new Range<>(Process.FIRST_APPLICATION_UID, Integer.MAX_VALUE));
+ final Set<Integer> exemptUids = mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(chain);
+ mDeps.destroyLiveTcpSockets(ranges, exemptUids);
+ } else {
+ // Denylist means the firewall allows all by default, uids must be explicitly denied
+ // So, close socket owned by uids that are explicitly denied
+ final Set<Integer> ownerUids = mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(chain);
+ mDeps.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+ }
+ }
+
@Override
public void setFirewallChainEnabled(final int chain, final boolean enable) {
enforceNetworkStackOrSettingsPermission();
@@ -11903,6 +12193,14 @@
} catch (ServiceSpecificException e) {
throw new IllegalStateException(e);
}
+
+ if (mDeps.isAtLeastU() && enable) {
+ try {
+ closeSocketsForFirewallChainLocked(chain);
+ } catch (ErrnoException | SocketException | InterruptedIOException e) {
+ Log.e(TAG, "Failed to close sockets after enabling chain (" + chain + "): " + e);
+ }
+ }
}
@Override
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index ee8ab68..e2ef981 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -583,12 +583,8 @@
*/
public void dump(IndentingPrintWriter pw) {
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
- // would like to do the dump()
- final boolean featureEnabled = BinderUtils.withCleanCallingIdentity(
- () -> mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION,
- true /* defaultEnabled */));
+ final boolean featureEnabled = mDependencies.isFeatureEnabled(
+ AUTOMATIC_ON_OFF_KEEPALIVE_VERSION, true /* defaultEnabled */);
pw.println("AutomaticOnOff enabled: " + featureEnabled);
pw.increaseIndent();
for (AutomaticOnOffKeepalive autoKi : mAutomaticOnOffKeepalives) {
@@ -841,8 +837,12 @@
* @return whether the feature is enabled
*/
public boolean isFeatureEnabled(@NonNull final String name, final boolean defaultEnabled) {
- return DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_TETHERING, name,
- defaultEnabled);
+ // 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 would like to do the dump()
+ return BinderUtils.withCleanCallingIdentity(() ->
+ DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_TETHERING, name,
+ DeviceConfigUtils.TETHERING_MODULE_NAME, defaultEnabled));
}
/**
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 122ea1c..9039a14 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -61,6 +61,6 @@
*/
public void loadFlags(ConnectivityService.Dependencies deps, Context ctx) {
mNoRematchAllRequestsOnRegister = deps.isFeatureEnabled(
- ctx, NO_REMATCH_ALL_REQUESTS_ON_REGISTER, false /* defaultEnabled */);
+ ctx, NO_REMATCH_ALL_REQUESTS_ON_REGISTER);
}
}
diff --git a/framework/src/android/net/ConnectivityResources.java b/service/src/com/android/server/connectivity/ConnectivityResources.java
similarity index 67%
rename from framework/src/android/net/ConnectivityResources.java
rename to service/src/com/android/server/connectivity/ConnectivityResources.java
index 18f0de0..9089e4a 100644
--- a/framework/src/android/net/ConnectivityResources.java
+++ b/service/src/com/android/server/connectivity/ConnectivityResources.java
@@ -14,22 +14,16 @@
* limitations under the License.
*/
-package android.net;
-
-import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+package com.android.server.connectivity;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.content.res.Resources;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.List;
+import com.android.net.module.util.DeviceConfigUtils;
/**
* Utility to obtain the {@link com.android.server.ConnectivityService} {@link Resources}, in the
@@ -37,10 +31,6 @@
* @hide
*/
public class ConnectivityResources {
- private static final String RESOURCES_APK_INTENT =
- "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
- private static final String RES_PKG_DIR = "/apex/com.android.tethering/";
-
@NonNull
private final Context mContext;
@@ -76,21 +66,10 @@
return mResourcesContext;
}
- final List<ResolveInfo> pkgs = mContext.getPackageManager()
- .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY);
- pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(RES_PKG_DIR));
- if (pkgs.size() > 1) {
- Log.wtf(ConnectivityResources.class.getSimpleName(),
- "More than one package found: " + pkgs);
- }
- if (pkgs.isEmpty()) {
- throw new IllegalStateException("No connectivity resource package found");
- }
-
+ final String resPkg = DeviceConfigUtils.getConnectivityResourcesPackageName(mContext);
final Context pkgContext;
try {
- pkgContext = mContext.createPackageContext(
- pkgs.get(0).activityInfo.applicationInfo.packageName, 0 /* flags */);
+ pkgContext = mContext.createPackageContext(resPkg, 0 /* flags */);
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException("Resolved package not found", e);
}
diff --git a/service/src/com/android/server/connectivity/KeepaliveResourceUtil.java b/service/src/com/android/server/connectivity/KeepaliveResourceUtil.java
new file mode 100644
index 0000000..92b080d
--- /dev/null
+++ b/service/src/com/android/server/connectivity/KeepaliveResourceUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+
+import com.android.connectivity.resources.R;
+
+/**
+ * Utilities to fetch keepalive configuration from resources.
+ */
+public abstract class KeepaliveResourceUtil {
+
+ /**
+ * Read supported keepalive count for each transport type from overlay resource.
+ *
+ * @param context The context to read resource from.
+ * @return An array of supported keepalive count for each transport type.
+ */
+ @NonNull
+ public static int[] getSupportedKeepalives(@NonNull Context context) {
+ String[] res = null;
+ try {
+ final ConnectivityResources connRes = new ConnectivityResources(context);
+ res = connRes.get().getStringArray(R.array.config_networkSupportedKeepaliveCount);
+ } catch (Resources.NotFoundException unused) {
+ }
+ if (res == null) throw new KeepaliveDeviceConfigurationException("invalid resource");
+
+ final int[] ret = new int[NetworkCapabilities.MAX_TRANSPORT + 1];
+ for (final String row : res) {
+ if (TextUtils.isEmpty(row)) {
+ throw new KeepaliveDeviceConfigurationException("Empty string");
+ }
+ final String[] arr = row.split(",");
+ if (arr.length != 2) {
+ throw new KeepaliveDeviceConfigurationException("Invalid parameter length");
+ }
+
+ int transport;
+ int supported;
+ try {
+ transport = Integer.parseInt(arr[0]);
+ supported = Integer.parseInt(arr[1]);
+ } catch (NumberFormatException e) {
+ throw new KeepaliveDeviceConfigurationException("Invalid number format");
+ }
+
+ if (!NetworkCapabilities.isValidTransport(transport)) {
+ throw new KeepaliveDeviceConfigurationException("Invalid transport " + transport);
+ }
+
+ if (supported < 0) {
+ throw new KeepaliveDeviceConfigurationException(
+ "Invalid supported count " + supported + " for "
+ + NetworkCapabilities.transportNameOf(transport));
+ }
+ ret[transport] = supported;
+ }
+ return ret;
+ }
+
+ /**
+ * An exception thrown when the keepalive resource configuration is invalid.
+ */
+ public static class KeepaliveDeviceConfigurationException extends AndroidRuntimeException {
+ public KeepaliveDeviceConfigurationException(final String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index 60485b3..cc226ce 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -37,7 +37,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.net.ConnectivityResources;
import android.net.ISocketKeepaliveCallback;
import android.net.InetAddresses;
import android.net.InvalidPacketException;
@@ -111,7 +110,7 @@
mTcpController = new TcpKeepaliveController(handler);
mContext = context;
- mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
+ mSupportedKeepalives = KeepaliveResourceUtil.getSupportedKeepalives(context);
final ConnectivityResources res = new ConnectivityResources(mContext);
mReservedPrivilegedSlots = res.get().getInteger(
@@ -570,7 +569,20 @@
} else if (reason == ERROR_STOP_REASON_UNINITIALIZED) {
throw new IllegalStateException("Unexpected stop reason: " + reason);
} else if (reason == ERROR_NO_SUCH_SLOT) {
- throw new IllegalStateException("No such slot: " + reason);
+ // There are multiple independent reasons a keepalive can stop. Some
+ // are software (e.g. the app stops the keepalive) and some are hardware
+ // (e.g. the SIM card gets removed). Therefore, there is a very low
+ // probability that both of these happen at the same time, which would
+ // result in the first stop attempt returning SUCCESS and the second
+ // stop attempt returning NO_SUCH_SLOT. Such a race condition can be
+ // ignored with a log.
+ // This should still be reported because if it happens with any frequency
+ // it probably means there is a bug where the system server is trying
+ // to use a non-existing hardware slot.
+ // TODO : separate the non-existing hardware slot from the case where
+ // there is no keepalive running on this slot.
+ Log.wtf(TAG, "Keepalive on slot " + slot + " can't be stopped : " + reason);
+ notifyErrorCallback(ki.mCallback, reason);
} else {
notifyErrorCallback(ki.mCallback, reason);
}
diff --git a/service/src/com/android/server/connectivity/LingerMonitor.java b/service/src/com/android/server/connectivity/LingerMonitor.java
index df34ce7..8503fcc 100644
--- a/service/src/com/android/server/connectivity/LingerMonitor.java
+++ b/service/src/com/android/server/connectivity/LingerMonitor.java
@@ -25,7 +25,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.net.ConnectivityResources;
import android.net.NetworkCapabilities;
import android.os.SystemClock;
import android.os.UserHandle;
diff --git a/service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java b/service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java
index 58196f7..93018bb 100644
--- a/service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java
@@ -28,7 +28,6 @@
import android.content.IntentFilter;
import android.content.res.Resources;
import android.database.ContentObserver;
-import android.net.ConnectivityResources;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
index 509110d..15d0925 100644
--- a/service/src/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -18,6 +18,11 @@
import static android.system.OsConstants.*;
+import static com.android.net.module.util.NetworkStackConstants.ICMP_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.InetAddresses;
@@ -33,6 +38,7 @@
import android.system.Os;
import android.system.StructTimeval;
import android.text.TextUtils;
+import android.util.Log;
import android.util.Pair;
import com.android.internal.util.IndentingPrintWriter;
@@ -172,7 +178,7 @@
}
}
- private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>();
+ private final Map<Pair<InetAddress, Integer>, Measurement> mIcmpChecks = new HashMap<>();
private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks =
new HashMap<>();
private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>();
@@ -205,17 +211,21 @@
mLinkProperties.addDnsServer(TEST_DNS6);
}
+ final int mtu = mLinkProperties.getMtu();
for (RouteInfo route : mLinkProperties.getRoutes()) {
if (route.getType() == RouteInfo.RTN_UNICAST && route.hasGateway()) {
InetAddress gateway = route.getGateway();
- prepareIcmpMeasurement(gateway);
+ // Use mtu in the route if exists. Otherwise, use the one in the link property.
+ final int routeMtu = route.getMtu();
+ prepareIcmpMeasurements(gateway, (routeMtu > 0) ? routeMtu : mtu);
if (route.isIPv6Default()) {
prepareExplicitSourceIcmpMeasurements(gateway);
}
}
}
+
for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
- prepareIcmpMeasurement(nameserver);
+ prepareIcmpMeasurements(nameserver, mtu);
prepareDnsMeasurement(nameserver);
// Unlike the DnsResolver which doesn't do certificate validation in opportunistic mode,
@@ -261,11 +271,50 @@
localAddr.getHostAddress(), inetSockAddr.getPort());
}
- private void prepareIcmpMeasurement(InetAddress target) {
- if (!mIcmpChecks.containsKey(target)) {
- Measurement measurement = new Measurement();
- measurement.thread = new Thread(new IcmpCheck(target, measurement));
- mIcmpChecks.put(target, measurement);
+ private static int getHeaderLen(@NonNull InetAddress target) {
+ // Convert IPv4 mapped v6 address to v4 if any.
+ try {
+ final InetAddress addr = InetAddress.getByAddress(target.getAddress());
+ // An ICMPv6 header is technically 4 bytes, but the implementation in IcmpCheck#run()
+ // will always fill in another 4 bytes padding in the v6 diagnostic packets, so the size
+ // before icmp data is always 8 bytes in the implementation of ICMP diagnostics for both
+ // v4 and v6 packets. Thus, it's fine to use the v4 header size in the length
+ // calculation.
+ if (addr instanceof Inet6Address) {
+ return IPV6_HEADER_LEN + ICMP_HEADER_LEN;
+ }
+ } catch (UnknownHostException e) {
+ Log.e(TAG, "Create InetAddress fail(" + target + "): " + e);
+ }
+
+ return IPV4_HEADER_MIN_LEN + ICMP_HEADER_LEN;
+ }
+
+ private void prepareIcmpMeasurements(@NonNull InetAddress target, int targetNetworkMtu) {
+ // Test with different size payload ICMP.
+ // 1. Test with 0 payload.
+ addPayloadIcmpMeasurement(target, 0);
+ final int header = getHeaderLen(target);
+ // 2. Test with full size MTU.
+ addPayloadIcmpMeasurement(target, targetNetworkMtu - header);
+ // 3. If v6, make another measurement with the full v6 min MTU, unless that's what
+ // was done above.
+ if ((target instanceof Inet6Address) && (targetNetworkMtu != IPV6_MIN_MTU)) {
+ addPayloadIcmpMeasurement(target, IPV6_MIN_MTU - header);
+ }
+ }
+
+ private void addPayloadIcmpMeasurement(@NonNull InetAddress target, int payloadLen) {
+ // This can happen if the there is no mtu filled(which is 0) in the link property.
+ // The value becomes negative after minus header length.
+ if (payloadLen < 0) return;
+
+ final Pair<InetAddress, Integer> lenTarget =
+ new Pair<>(target, Integer.valueOf(payloadLen));
+ if (!mIcmpChecks.containsKey(lenTarget)) {
+ final Measurement measurement = new Measurement();
+ measurement.thread = new Thread(new IcmpCheck(target, payloadLen, measurement));
+ mIcmpChecks.put(lenTarget, measurement);
}
}
@@ -276,7 +325,7 @@
Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target);
if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) {
Measurement measurement = new Measurement();
- measurement.thread = new Thread(new IcmpCheck(source, target, measurement));
+ measurement.thread = new Thread(new IcmpCheck(source, target, 0, measurement));
mExplicitSourceIcmpChecks.put(srcTarget, measurement);
}
}
@@ -334,8 +383,8 @@
ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount());
// Sort measurements IPv4 first.
- for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
- if (entry.getKey() instanceof Inet4Address) {
+ for (Map.Entry<Pair<InetAddress, Integer>, Measurement> entry : mIcmpChecks.entrySet()) {
+ if (entry.getKey().first instanceof Inet4Address) {
measurements.add(entry.getValue());
}
}
@@ -357,8 +406,8 @@
}
// IPv6 measurements second.
- for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
- if (entry.getKey() instanceof Inet6Address) {
+ for (Map.Entry<Pair<InetAddress, Integer>, Measurement> entry : mIcmpChecks.entrySet()) {
+ if (entry.getKey().first instanceof Inet6Address) {
measurements.add(entry.getValue());
}
}
@@ -489,8 +538,11 @@
private static final int PACKET_BUFSIZE = 512;
private final int mProtocol;
private final int mIcmpType;
+ private final int mPayloadSize;
+ // The length parameter is effectively the -s flag to ping/ping6 to specify the number of
+ // data bytes to be sent.
+ IcmpCheck(InetAddress source, InetAddress target, int length, Measurement measurement) {
- public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) {
super(source, target, measurement);
if (mAddressFamily == AF_INET6) {
@@ -502,12 +554,13 @@
mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE;
mMeasurement.description = "ICMPv4";
}
-
- mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}";
+ mPayloadSize = length;
+ mMeasurement.description += " payloadLength{" + mPayloadSize + "}"
+ + " dst{" + mTarget.getHostAddress() + "}";
}
- public IcmpCheck(InetAddress target, Measurement measurement) {
- this(null, target, measurement);
+ IcmpCheck(InetAddress target, int length, Measurement measurement) {
+ this(null, target, length, measurement);
}
@Override
@@ -523,9 +576,11 @@
mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
// Build a trivial ICMP packet.
- final byte[] icmpPacket = {
- (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header
- };
+ // The v4 ICMP header ICMP_HEADER_LEN (which is 8) and v6 is only 4 bytes (4 bytes
+ // message body followed by header before the payload).
+ // Use 8 bytes for both v4 and v6 for simplicity.
+ final byte[] icmpPacket = new byte[ICMP_HEADER_LEN + mPayloadSize];
+ icmpPacket[0] = (byte) mIcmpType;
int count = 0;
mMeasurement.startTime = now();
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
index cdc0aa9..8b0cb7c 100644
--- a/service/src/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -30,7 +30,6 @@
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
-import android.net.ConnectivityResources;
import android.net.NetworkSpecifier;
import android.net.TelephonyNetworkSpecifier;
import android.net.wifi.WifiInfo;
diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java
index bc0929c..bda4b8f 100644
--- a/service/src/com/android/server/connectivity/ProxyTracker.java
+++ b/service/src/com/android/server/connectivity/ProxyTracker.java
@@ -27,6 +27,7 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.net.LinkProperties;
import android.net.Network;
import android.net.PacProxyManager;
import android.net.Proxy;
@@ -95,6 +96,7 @@
}
public void onPacProxyInstalled(@Nullable Network network, @NonNull ProxyInfo proxy) {
+ Log.i(TAG, "PAC proxy installed on network " + network + " : " + proxy);
mConnectivityServiceHandler
.sendMessage(mConnectivityServiceHandler
.obtainMessage(mEvent, new Pair<>(network, proxy)));
@@ -328,9 +330,15 @@
* @param proxyInfo the proxy spec, or null for no proxy.
*/
public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
+ // The code has been accepting empty proxy objects forever, so for backward
+ // compatibility it should continue doing so.
+ if (proxyInfo != null && TextUtils.isEmpty(proxyInfo.getHost())
+ && Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
+ proxyInfo = null;
+ }
synchronized (mProxyLock) {
if (Objects.equals(mDefaultProxy, proxyInfo)) return;
- if (proxyInfo != null && !proxyInfo.isValid()) {
+ if (proxyInfo != null && !proxyInfo.isValid()) {
if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
return;
}
@@ -355,4 +363,51 @@
}
}
}
+
+ private boolean isPacProxy(@Nullable final ProxyInfo info) {
+ return null != info && info.isPacProxy();
+ }
+
+ /**
+ * Adjust the proxy in the link properties if necessary.
+ *
+ * It is necessary when the proxy in the passed property is for PAC, and the default proxy
+ * is also for PAC. This is because the original LinkProperties from the network agent don't
+ * include the port for the local proxy as it's not known at creation time, but this class
+ * knows it after the proxy service is started.
+ *
+ * This is safe because there can only ever be one proxy service running on the device, so
+ * if the ProxyInfo in the LinkProperties is for PAC, then the port is necessarily the one
+ * ProxyTracker knows about.
+ *
+ * @param lp the LinkProperties to fix up.
+ * @param network the network of the local proxy server.
+ */
+ // TODO: Leave network unused to support local proxy server per network in the future.
+ public void updateDefaultNetworkProxyPortForPAC(@NonNull final LinkProperties lp,
+ @Nullable Network network) {
+ final ProxyInfo defaultProxy = getDefaultProxy();
+ if (isPacProxy(lp.getHttpProxy()) && isPacProxy(defaultProxy)) {
+ synchronized (mProxyLock) {
+ // At this time, this method can only be called for the default network's LP.
+ // Therefore the PAC file URL in the LP must match the one in the default proxy,
+ // and we just update the port.
+ // Note that the global proxy, if any, is set out of band by the DPM and becomes
+ // the default proxy (it overrides it, see {@link getDefaultProxy}). The PAC URL
+ // in the global proxy might not be the one in the LP of the default
+ // network, so discount this case.
+ if (null == mGlobalProxy && !lp.getHttpProxy().getPacFileUrl()
+ .equals(defaultProxy.getPacFileUrl())) {
+ throw new IllegalStateException("Unexpected discrepancy between proxy in LP of "
+ + "default network and default proxy. The former has a PAC URL of "
+ + lp.getHttpProxy().getPacFileUrl() + " while the latter has "
+ + defaultProxy.getPacFileUrl());
+ }
+ }
+ // If this network has a PAC proxy and proxy tracker already knows about
+ // it, now is the right time to patch it in. If proxy tracker does not know
+ // about it yet, then it will be patched in when it learns about it.
+ lp.setHttpProxy(defaultProxy);
+ }
+ }
}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index f24e55f..891c2dd 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-next_app_data = []
+next_app_data = [ ":CtsHostsideNetworkTestsAppNext" ]
// The above line is put in place to prevent any future automerger merge conflict between aosp,
// downstream branches. The CtsHostsideNetworkTestsAppNext target will not exist in
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 12e7d33..1d6828f 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -39,7 +39,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 a281aed..106a49c 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
@@ -54,14 +54,15 @@
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Bundle;
+import android.os.PowerManager;
import android.os.RemoteCallback;
import android.os.SystemClock;
-import android.os.PowerManager;
import android.provider.DeviceConfig;
import android.service.notification.NotificationListenerService;
import android.util.Log;
import android.util.Pair;
+import com.android.compatibility.common.util.AmUtils;
import com.android.compatibility.common.util.BatteryUtils;
import com.android.compatibility.common.util.DeviceConfigStateHelper;
@@ -718,10 +719,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();
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
index 78ae7b8..a558010 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
@@ -64,6 +64,7 @@
"dumpsys usagestats appstandby",
"dumpsys connectivity trafficcontroller",
"dumpsys netd trafficcontroller",
+ "dumpsys platform_compat"
}) {
dumpCommandOutput(out, cmd);
}
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 deca6a2..12b186f 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/hostside/src/com/android/cts/net/ProcNetTest.java b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
index 19e61c6..1a528b1 100644
--- a/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
+++ b/tests/cts/hostside/src/com/android/cts/net/ProcNetTest.java
@@ -166,4 +166,15 @@
assertTrue(interval <= upperBoundSec);
}
}
+
+ /**
+ * Verify that cubic is used as the congestion control algorithm.
+ * (This repeats the VTS test, and is here for good performance of the internet as a whole.)
+ * TODO: revisit this once a better CC algorithm like BBR2 is available.
+ */
+ public void testCongestionControl() throws Exception {
+ String path = "/proc/sys/net/ipv4/tcp_congestion_control";
+ String value = mDevice.executeAdbCommand("shell", "cat", path).trim();
+ assertEquals(value, "cubic");
+ }
}
diff --git a/tests/cts/net/AndroidManifest.xml b/tests/cts/net/AndroidManifest.xml
index 999614c..68e36ff 100644
--- a/tests/cts/net/AndroidManifest.xml
+++ b/tests/cts/net/AndroidManifest.xml
@@ -37,9 +37,6 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
- <!-- TODO (b/186093901): remove after fixing resource querying -->
- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-
<!-- This test also uses signature permissions through adopting the shell identity.
The permissions acquired that way include (probably not exhaustive) :
android.permission.MANAGE_TEST_NETWORKS
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 9d234d3..f0ad3f1 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -225,6 +225,7 @@
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
+import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
@@ -278,6 +279,7 @@
// TODO(b/252972908): reset the original timer when aosp/2188755 is ramped up.
private static final int LISTEN_ACTIVITY_TIMEOUT_MS = 30_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;
private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
@@ -1655,7 +1657,8 @@
final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
// Get number of supported concurrent keepalives for testing network.
- final int[] keepalivesPerTransport = KeepaliveUtils.getSupportedKeepalives(mContext);
+ final int[] keepalivesPerTransport = runAsShell(NETWORK_SETTINGS,
+ () -> mCm.getSupportedKeepalives());
return KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
keepalivesPerTransport, nc);
}
@@ -1670,6 +1673,9 @@
* keepalives is set to 0.
*/
@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ // getSupportedKeepalives is available in updatable ConnectivityManager (S+)
+ // Also, this feature is not mainlined before S, and it's fine to skip on R- devices.
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @ConnectivityModuleTest
@Test
public void testKeepaliveWifiUnsupported() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
@@ -1686,6 +1692,9 @@
}
@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ // getSupportedKeepalives is available in updatable ConnectivityManager (S+)
+ // Also, this feature is not mainlined before S, and it's fine to skip on R- devices.
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @ConnectivityModuleTest
@Test
@RequiresDevice // Keepalive is not supported on virtual hardware
public void testCreateTcpKeepalive() throws Exception {
@@ -1894,6 +1903,9 @@
*/
@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
@Test
+ // getSupportedKeepalives is available in updatable ConnectivityManager (S+)
+ // Also, this feature is not mainlined before S, and it's fine to skip on R- devices.
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @ConnectivityModuleTest
@RequiresDevice // Keepalive is not supported on virtual hardware
public void testSocketKeepaliveLimitWifi() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
@@ -1944,6 +1956,9 @@
*/
@AppModeFull(reason = "Cannot request network in instant app mode")
@Test
+ // getSupportedKeepalives is available in updatable ConnectivityManager (S+)
+ // Also, this feature is not mainlined before S, and it's fine to skip on R- devices.
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @ConnectivityModuleTest
@RequiresDevice // Keepalive is not supported on virtual hardware
public void testSocketKeepaliveLimitTelephony() throws Exception {
if (!mPackageManager.hasSystemFeature(FEATURE_TELEPHONY)) {
@@ -1990,6 +2005,9 @@
*/
@AppModeFull(reason = "Cannot get WifiManager in instant app mode")
@Test
+ // getSupportedKeepalives is available in updatable ConnectivityManager (S+)
+ // Also, this feature is not mainlined before S, and it's fine to skip on R- devices.
+ @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @ConnectivityModuleTest
@RequiresDevice // Keepalive is not supported on virtual hardware
public void testSocketKeepaliveUnprivileged() throws Exception {
assumeTrue(mPackageManager.hasSystemFeature(FEATURE_WIFI));
@@ -2112,7 +2130,12 @@
@AppModeFull(reason = "NETWORK_AIRPLANE_MODE permission can't be granted to instant apps")
@Test
public void testSetAirplaneMode() throws Exception{
- final boolean supportWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI);
+ // Starting from T, wifi supports airplane mode enhancement which may not disconnect wifi
+ // when airplane mode is on. The actual behavior that the device will have could only be
+ // checked with hidden wifi APIs(see Settings.Secure.WIFI_APM_STATE). Thus, stop verifying
+ // wifi on T+ device.
+ final boolean verifyWifi = mPackageManager.hasSystemFeature(FEATURE_WIFI)
+ && !SdkLevel.isAtLeastT();
final boolean supportTelephony = mPackageManager.hasSystemFeature(FEATURE_TELEPHONY);
// store the current state of airplane mode
final boolean isAirplaneModeEnabled = isAirplaneModeEnabled();
@@ -2123,7 +2146,7 @@
// Verify that networks are available as expected if wifi or cell is supported. Continue the
// test if none of them are supported since test should still able to verify the permission
// mechanism.
- if (supportWifi) {
+ if (verifyWifi) {
mCtsNetUtils.ensureWifiConnected();
registerCallbackAndWaitForAvailable(makeWifiNetworkRequest(), wifiCb);
}
@@ -2147,7 +2170,7 @@
// Verify that the enabling airplane mode takes effect as expected to prevent flakiness
// caused by fast airplane mode switches. Ensure network lost before turning off
// airplane mode.
- if (supportWifi) waitForLost(wifiCb);
+ if (verifyWifi) waitForLost(wifiCb);
if (supportTelephony) waitForLost(telephonyCb);
// Verify we can disable Airplane Mode with correct permission:
@@ -2156,7 +2179,7 @@
// Verify that turning airplane mode off takes effect as expected.
// connectToCell only registers a request, it cannot / does not need to be called twice
mCtsNetUtils.ensureWifiConnected();
- if (supportWifi) waitForAvailable(wifiCb);
+ if (verifyWifi) waitForAvailable(wifiCb);
if (supportTelephony) waitForAvailable(telephonyCb);
} finally {
// Restore the previous state of airplane mode and permissions:
@@ -2552,6 +2575,14 @@
tetherUtils.registerTetheringEventCallback();
try {
tetherEventCallback.assumeWifiTetheringSupported(mContext);
+ // To prevent WiFi-to-WiFi interruption while entering APM:
+ // - If WiFi is retained while entering APM, hotspot will also remain enabled.
+ // - If WiFi is off before APM or disabled while entering APM, hotspot will be
+ // disabled.
+ //
+ // To ensure hotspot always be disabled after enabling APM, disable wifi before
+ // enabling the hotspot.
+ mCtsNetUtils.disableWifi();
tetherUtils.startWifiTethering(tetherEventCallback);
// Update setting to verify the behavior.
@@ -2585,6 +2616,7 @@
ConnectivitySettingsManager.setPrivateDnsMode(mContext, curPrivateDnsMode);
tetherUtils.unregisterTetheringEventCallback(tetherEventCallback);
tetherUtils.stopAllTethering();
+ mCtsNetUtils.ensureWifiConnected();
}
}
@@ -3523,6 +3555,103 @@
doTestFirewallBlocking(FIREWALL_CHAIN_OEM_DENY_3, DENYLIST);
}
+ private void assertSocketOpen(final Socket socket) throws Exception {
+ mCtsNetUtils.testHttpRequest(socket);
+ }
+
+ private void assertSocketClosed(final Socket socket) throws Exception {
+ try {
+ mCtsNetUtils.testHttpRequest(socket);
+ fail("Socket is expected to be closed");
+ } catch (SocketException expected) {
+ }
+ }
+
+ private static final boolean EXPECT_OPEN = false;
+ private static final boolean EXPECT_CLOSE = true;
+
+ private void doTestFirewallCloseSocket(final int chain, final int rule, final int targetUid,
+ final boolean expectClose) {
+ runWithShellPermissionIdentity(() -> {
+ // Firewall chain status will be restored after the test.
+ final boolean wasChainEnabled = mCm.getFirewallChainEnabled(chain);
+ final int previousUidFirewallRule = mCm.getUidFirewallRule(chain, targetUid);
+ final Socket socket = new Socket(TEST_HOST, HTTP_PORT);
+ socket.setSoTimeout(NETWORK_REQUEST_TIMEOUT_MS);
+ testAndCleanup(() -> {
+ 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.
+ }
+ mCm.setFirewallChainEnabled(chain, true /* enable */);
+
+ if (expectClose) {
+ assertSocketClosed(socket);
+ } else {
+ assertSocketOpen(socket);
+ }
+ }, /* cleanup */ () -> {
+ // Restore the global chain status
+ 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.
+ }
+ }, /* cleanup */ () -> {
+ socket.close();
+ });
+ }, NETWORK_SETTINGS);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketAllowlistChainAllow() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_ALLOW,
+ Process.myUid(), EXPECT_OPEN);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketAllowlistChainDeny() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY,
+ Process.myUid(), EXPECT_CLOSE);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketAllowlistChainOtherUid() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_ALLOW,
+ Process.myUid() + 1, EXPECT_CLOSE);
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_DOZABLE, FIREWALL_RULE_DENY,
+ Process.myUid() + 1, EXPECT_CLOSE);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketDenylistChainAllow() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW,
+ Process.myUid(), EXPECT_OPEN);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketDenylistChainDeny() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_DENY,
+ Process.myUid(), EXPECT_CLOSE);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) @ConnectivityModuleTest
+ public void testFirewallCloseSocketDenylistChainOtherUid() {
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_ALLOW,
+ Process.myUid() + 1, EXPECT_OPEN);
+ doTestFirewallCloseSocket(FIREWALL_CHAIN_STANDBY, FIREWALL_RULE_DENY,
+ Process.myUid() + 1, EXPECT_OPEN);
+ }
+
private void assumeTestSApis() {
// Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
// shims, and @IgnoreUpTo does not check that.
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 869562b..cf5fc50 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -29,9 +29,9 @@
import android.net.NattKeepalivePacketData
import android.net.Network
import android.net.NetworkAgent
-import android.net.NetworkAgentConfig
import android.net.NetworkAgent.INVALID_NETWORK
import android.net.NetworkAgent.VALID_NETWORK
+import android.net.NetworkAgentConfig
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
@@ -46,21 +46,23 @@
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_TEST
-import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkInfo
import android.net.NetworkProvider
import android.net.NetworkReleasedException
import android.net.NetworkRequest
import android.net.NetworkScore
-import android.net.RouteInfo
import android.net.QosCallback
-import android.net.QosCallbackException
import android.net.QosCallback.QosCallbackRegistrationException
+import android.net.QosCallbackException
import android.net.QosSession
import android.net.QosSessionAttributes
import android.net.QosSocketInfo
+import android.net.RouteInfo
import android.net.SocketKeepalive
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
import android.net.Uri
import android.net.VpnManager
import android.net.VpnTransportInfo
@@ -71,6 +73,7 @@
import android.os.Handler
import android.os.HandlerThread
import android.os.Message
+import android.os.Process
import android.os.SystemClock
import android.platform.test.annotations.AppModeFull
import android.system.OsConstants.IPPROTO_TCP
@@ -89,6 +92,7 @@
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.Losing
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
@@ -168,6 +172,7 @@
// for modules other than Connectivity does not provide much value. Only run them in connectivity
// module MTS, so the tests only need to cover the case of an updated NetworkAgent.
@ConnectivityModuleTest
+@AppModeFull(reason = "Instant apps can't use NetworkAgent because it needs NETWORK_FACTORY'.")
class NetworkAgentTest {
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
@@ -178,6 +183,7 @@
private val agentsToCleanUp = mutableListOf<NetworkAgent>()
private val callbacksToCleanUp = mutableListOf<TestableNetworkCallback>()
private var qosTestSocket: Closeable? = null // either Socket or DatagramSocket
+ private val ifacesToCleanUp = mutableListOf<TestNetworkInterface>()
@Before
fun setUp() {
@@ -189,6 +195,7 @@
fun tearDown() {
agentsToCleanUp.forEach { it.unregister() }
callbacksToCleanUp.forEach { mCM.unregisterNetworkCallback(it) }
+ ifacesToCleanUp.forEach { it.fileDescriptor.close() }
qosTestSocket?.close()
mHandlerThread.quitSafely()
mHandlerThread.join()
@@ -269,7 +276,7 @@
removeCapability(NET_CAPABILITY_INTERNET)
addCapability(NET_CAPABILITY_NOT_SUSPENDED)
addCapability(NET_CAPABILITY_NOT_ROAMING)
- addCapability(NET_CAPABILITY_NOT_VPN)
+ if (!transports.contains(TRANSPORT_VPN)) addCapability(NET_CAPABILITY_NOT_VPN)
if (SdkLevel.isAtLeastS()) {
addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
}
@@ -304,7 +311,7 @@
context: Context = realContext,
specifier: String? = UUID.randomUUID().toString(),
initialConfig: NetworkAgentConfig? = null,
- expectedInitSignalStrengthThresholds: IntArray? = intArrayOf(),
+ expectedInitSignalStrengthThresholds: IntArray = intArrayOf(),
transports: IntArray = intArrayOf()
): Pair<TestableNetworkAgent, TestableNetworkCallback> {
val callback = TestableNetworkCallback()
@@ -317,8 +324,7 @@
agent.register()
agent.markConnected()
agent.expectCallback<OnNetworkCreated>()
- agent.expectSignalStrengths(expectedInitSignalStrengthThresholds)
- agent.expectValidationBypassedStatus()
+ agent.expectPostConnectionCallbacks(expectedInitSignalStrengthThresholds)
callback.expectAvailableThenValidatedCallbacks(agent.network!!)
return agent to callback
}
@@ -336,6 +342,19 @@
mFakeConnectivityService.connect(it.registerForTest(Network(FAKE_NET_ID)))
}
+ private fun TestableNetworkAgent.expectPostConnectionCallbacks(
+ thresholds: IntArray = intArrayOf()
+ ) {
+ expectSignalStrengths(thresholds)
+ expectValidationBypassedStatus()
+ assertNoCallback()
+ }
+
+ private fun createTunInterface(): TestNetworkInterface = realContext.getSystemService(
+ TestNetworkManager::class.java)!!.createTunInterface(emptyList()).also {
+ ifacesToCleanUp.add(it)
+ }
+
fun assertLinkPropertiesEventually(
n: Network,
description: String,
@@ -964,13 +983,11 @@
.also { assertNotNull(agent.network?.bindSocket(it)) }
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackRegisterAndUnregister() {
validateQosCallbackRegisterAndUnregister(IPPROTO_TCP)
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackRegisterAndUnregisterWithDatagramSocket() {
validateQosCallbackRegisterAndUnregister(IPPROTO_UDP)
@@ -1007,13 +1024,11 @@
}
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackOnQosSession() {
validateQosCallbackOnQosSession(IPPROTO_TCP)
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackOnQosSessionWithDatagramSocket() {
validateQosCallbackOnQosSession(IPPROTO_UDP)
@@ -1072,7 +1087,6 @@
}
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackOnError() {
val (agent, qosTestSocket) = setupForQosSocket()
@@ -1111,7 +1125,6 @@
}
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackIdsAreMappedCorrectly() {
val (agent, qosTestSocket) = setupForQosSocket()
@@ -1152,7 +1165,6 @@
}
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testQosCallbackWhenNetworkReleased() {
val (agent, qosTestSocket) = setupForQosSocket()
@@ -1194,7 +1206,6 @@
)
}
- @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
@Test
fun testUnregisterAfterReplacement() {
// Keeps an eye on all test networks.
@@ -1291,8 +1302,12 @@
requestNetwork(makeTestNetworkRequest(specifier = specifier6), callback)
val agent6 = createNetworkAgent(specifier = specifier6)
val network6 = agent6.register()
- // No callbacks are sent, so check the LinkProperties to see if the network has connected.
- assertLinkPropertiesEventuallyNotNull(agent6.network!!)
+ if (SdkLevel.isAtLeastU()) {
+ agent6.expectCallback<OnNetworkCreated>()
+ } else {
+ // No callbacks are sent, so check LinkProperties to wait for the network to be created.
+ assertLinkPropertiesEventuallyNotNull(agent6.network!!)
+ }
// unregisterAfterReplacement tears down the network immediately.
// Approximately check that this is the case by picking an unregister timeout that's longer
@@ -1301,8 +1316,9 @@
val timeoutMs = agent6.DEFAULT_TIMEOUT_MS.toInt() + 1_000
agent6.unregisterAfterReplacement(timeoutMs)
agent6.expectCallback<OnNetworkUnwanted>()
- if (!SdkLevel.isAtLeastT()) {
+ if (!SdkLevel.isAtLeastT() || SdkLevel.isAtLeastU()) {
// Before T, onNetworkDestroyed is called even if the network was never created.
+ // On U+, the network was created by register(). Destroying it sends onNetworkDestroyed.
agent6.expectCallback<OnNetworkDestroyed>()
}
// Poll for LinkProperties becoming null, because when onNetworkUnwanted is called, the
@@ -1375,4 +1391,101 @@
callback.expect<Available>(agent.network!!)
callback.eventuallyExpect<Lost> { it.network == agent.network }
}
+
+ fun doTestNativeNetworkCreation(expectCreatedImmediately: Boolean, transports: IntArray) {
+ val iface = createTunInterface()
+ val ifName = iface.interfaceName
+ val nc = makeTestNetworkCapabilities(ifName, transports).also {
+ if (transports.contains(TRANSPORT_VPN)) {
+ val sessionId = "NetworkAgentTest-${Process.myPid()}"
+ it.transportInfo = VpnTransportInfo(VpnManager.TYPE_VPN_PLATFORM, sessionId,
+ /*bypassable=*/ false, /*longLivedTcpConnectionsExpensive=*/ false)
+ it.underlyingNetworks = listOf()
+ }
+ }
+ val lp = LinkProperties().apply {
+ interfaceName = ifName
+ addLinkAddress(LinkAddress("2001:db8::1/64"))
+ addRoute(RouteInfo(IpPrefix("2001:db8::/64"), null /* nextHop */, ifName))
+ addRoute(RouteInfo(IpPrefix("::/0"),
+ InetAddresses.parseNumericAddress("fe80::abcd"),
+ ifName))
+ }
+
+ // File a request containing the agent's specifier to receive callbacks and to ensure that
+ // the agent is not torn down due to being unneeded.
+ val request = makeTestNetworkRequest(specifier = ifName)
+ val requestCallback = TestableNetworkCallback()
+ requestNetwork(request, requestCallback)
+
+ val listenCallback = TestableNetworkCallback()
+ registerNetworkCallback(request, listenCallback)
+
+ // Register the NetworkAgent...
+ val agent = createNetworkAgent(realContext, initialNc = nc, initialLp = lp)
+ val network = agent.register()
+
+ // ... and then change the NetworkCapabilities and LinkProperties.
+ nc.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ agent.sendNetworkCapabilities(nc)
+ lp.addLinkAddress(LinkAddress("192.0.2.2/25"))
+ lp.addRoute(RouteInfo(IpPrefix("192.0.2.0/25"), null /* nextHop */, ifName))
+ agent.sendLinkProperties(lp)
+
+ requestCallback.assertNoCallback()
+ listenCallback.assertNoCallback()
+ if (!expectCreatedImmediately) {
+ agent.assertNoCallback()
+ agent.markConnected()
+ agent.expectCallback<OnNetworkCreated>()
+ } else {
+ agent.expectCallback<OnNetworkCreated>()
+ agent.markConnected()
+ }
+ agent.expectPostConnectionCallbacks()
+
+ // onAvailable must be called only when the network connects, and no other callbacks may be
+ // called before that happens. The callbacks report the state of the network as it was when
+ // it connected, so they reflect the NC and LP changes made after registration.
+ requestCallback.expect<Available>(network)
+ listenCallback.expect<Available>(network)
+
+ requestCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
+ NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+ listenCallback.expect<CapabilitiesChanged>(network) { it.caps.hasCapability(
+ NET_CAPABILITY_TEMPORARILY_NOT_METERED) }
+
+ requestCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
+ listenCallback.expect<LinkPropertiesChanged>(network) { it.lp.equals(lp) }
+
+ requestCallback.expect<BlockedStatus>()
+ listenCallback.expect<BlockedStatus>()
+
+ // Except for network validation, ensure no more callbacks are sent.
+ requestCallback.expectCaps(network) {
+ it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ listenCallback.expectCaps(network) {
+ it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ unregister(agent)
+ // Lost implicitly checks that no further callbacks happened after connect.
+ requestCallback.expect<Lost>(network)
+ listenCallback.expect<Lost>(network)
+ assertNull(mCM.getLinkProperties(network))
+ }
+
+ @Test
+ fun testNativeNetworkCreation_PhysicalNetwork() {
+ // On T and below, the native network is only created when the agent connects.
+ // Starting in U, the native network is created as soon as the agent is registered.
+ doTestNativeNetworkCreation(expectCreatedImmediately = SdkLevel.isAtLeastU(),
+ intArrayOf(TRANSPORT_CELLULAR))
+ }
+
+ @Test
+ fun testNativeNetworkCreation_Vpn() {
+ // VPN networks are always created as soon as the agent is registered.
+ doTestNativeNetworkCreation(expectCreatedImmediately = true, intArrayOf(TRANSPORT_VPN))
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index db7f38c..9808137 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -73,8 +73,8 @@
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.SOCK_DGRAM
import android.util.Log
+import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
import com.android.compatibility.common.util.PollingCheck
import com.android.compatibility.common.util.PropertyUtil
import com.android.modules.utils.build.SdkLevel.isAtLeastU
@@ -84,6 +84,8 @@
import com.android.networkstack.apishim.common.NsdShim
import com.android.testutils.ConnectivityModuleTest
import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkAgent
@@ -129,12 +131,15 @@
// tried sequentially
private const val REGISTRATION_TIMEOUT_MS = 10_000L
private const val DBG = false
+private const val TEST_PORT = 12345
private val nsdShim = NsdShimImpl.newInstance()
@AppModeFull(reason = "Socket cannot bind in instant app mode")
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
@ConnectivityModuleTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class NsdManagerTest {
// Rule used to filter CtsNetTestCasesMaxTargetSdkXX
@get:Rule
@@ -282,13 +287,17 @@
fun waitForServiceDiscovered(
serviceName: String,
+ serviceType: String,
expectedNetwork: Network? = null
): NsdServiceInfo {
- return expectCallbackEventually<ServiceFound> {
+ val serviceFound = expectCallbackEventually<ServiceFound> {
it.serviceInfo.serviceName == serviceName &&
(expectedNetwork == null ||
expectedNetwork == nsdShim.getNetwork(it.serviceInfo))
}.serviceInfo
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", serviceFound.serviceType)
+ return serviceFound
}
}
@@ -432,6 +441,13 @@
return agent
}
+ private fun makeTestServiceInfo(network: Network? = null) = NsdServiceInfo().also {
+ it.serviceType = serviceType
+ it.serviceName = serviceName
+ it.network = network
+ it.port = TEST_PORT
+ }
+
@After
fun tearDown() {
if (TestUtils.shouldTestTApis()) {
@@ -497,6 +513,10 @@
val registeredInfo = registrationRecord.expectCallback<ServiceRegistered>(
REGISTRATION_TIMEOUT_MS).serviceInfo
+ // Only service name is included in ServiceRegistered callbacks
+ assertNull(registeredInfo.serviceType)
+ assertEquals(si.serviceName, registeredInfo.serviceName)
+
val discoveryRecord = NsdDiscoveryRecord()
// Test discovering without an Executor
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
@@ -505,12 +525,15 @@
discoveryRecord.expectCallback<DiscoveryStarted>()
// Expect a service record to be discovered
- val foundInfo = discoveryRecord.waitForServiceDiscovered(registeredInfo.serviceName)
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(
+ registeredInfo.serviceName, serviceType)
// Test resolving without an Executor
val resolveRecord = NsdResolveRecord()
nsdManager.resolveService(foundInfo, resolveRecord)
val resolvedService = resolveRecord.expectCallback<ServiceResolved>().serviceInfo
+ assertEquals(".$serviceType", resolvedService.serviceType)
+ assertEquals(registeredInfo.serviceName, resolvedService.serviceName)
// Check Txt attributes
assertEquals(8, resolvedService.attributes.size)
@@ -538,9 +561,11 @@
registrationRecord.expectCallback<ServiceUnregistered>()
// Expect a callback for service lost
- discoveryRecord.expectCallbackEventually<ServiceLost> {
+ val lostCb = discoveryRecord.expectCallbackEventually<ServiceLost> {
it.serviceInfo.serviceName == serviceName
}
+ // Lost service types have a dot at the end
+ assertEquals("$serviceType.", lostCb.serviceInfo.serviceType)
// Register service again to see if NsdManager can discover it
val si2 = NsdServiceInfo()
@@ -554,7 +579,8 @@
// Expect a service record to be discovered (and filter the ones
// that are unrelated to this test)
- val foundInfo2 = discoveryRecord.waitForServiceDiscovered(registeredInfo2.serviceName)
+ val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
+ registeredInfo2.serviceName, serviceType)
// Resolve the service
val resolveRecord2 = NsdResolveRecord()
@@ -591,7 +617,7 @@
testNetwork1.network, Executor { it.run() }, discoveryRecord)
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
// Rewind to ensure the service is not found on the other interface
@@ -638,6 +664,8 @@
val serviceDiscovered = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo1.serviceName, serviceDiscovered.serviceInfo.serviceName)
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", serviceDiscovered.serviceInfo.serviceType)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered.serviceInfo))
// Unregister, then register the service back: it should be lost and found again
@@ -650,6 +678,7 @@
val registeredInfo2 = registerService(registrationRecord, si, executor)
val serviceDiscovered2 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered2.serviceInfo.serviceName)
+ assertEquals("$serviceType.", serviceDiscovered2.serviceInfo.serviceType)
assertEquals(testNetwork1.network, nsdShim.getNetwork(serviceDiscovered2.serviceInfo))
// Teardown, then bring back up a network on the test interface: the service should
@@ -665,6 +694,7 @@
val newNetwork = newAgent.network ?: fail("Registered agent should have a network")
val serviceDiscovered3 = discoveryRecord.expectCallback<ServiceFound>()
assertEquals(registeredInfo2.serviceName, serviceDiscovered3.serviceInfo.serviceName)
+ assertEquals("$serviceType.", serviceDiscovered3.serviceInfo.serviceType)
assertEquals(newNetwork, nsdShim.getNetwork(serviceDiscovered3.serviceInfo))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
@@ -740,12 +770,12 @@
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
val foundInfo1 = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo1))
// Rewind as the service could be found on each interface in any order
discoveryRecord.nextEvents.rewind(0)
val foundInfo2 = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork2.network)
+ serviceName, serviceType, testNetwork2.network)
assertEquals(testNetwork2.network, nsdShim.getNetwork(foundInfo2))
nsdShim.resolveService(nsdManager, foundInfo1, Executor { it.run() }, resolveRecord)
@@ -790,7 +820,7 @@
testNetwork1.network, Executor { it.run() }, discoveryRecord)
// Expect that service is found on testNetwork1
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo))
// Discover service on testNetwork2.
@@ -805,7 +835,7 @@
null as Network? /* network */, Executor { it.run() }, discoveryRecord3)
// Expect that service is found on testNetwork1
val foundInfo3 = discoveryRecord3.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
assertEquals(testNetwork1.network, nsdShim.getNetwork(foundInfo3))
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord2)
@@ -835,7 +865,7 @@
nsdManager.discoverServices(
serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryRecord
)
- val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames)
+ val foundInfo = discoveryRecord.waitForServiceDiscovered(serviceNames, serviceType)
// Expect that resolving the service name works properly even service name contains
// non-standard characters.
@@ -985,7 +1015,7 @@
nsdShim.discoverServices(nsdManager, serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, discoveryRecord)
val foundInfo = discoveryRecord.waitForServiceDiscovered(
- serviceName, testNetwork1.network)
+ serviceName, serviceType, testNetwork1.network)
// Register service callback and check the addresses are the same as network addresses
nsdShim.registerServiceInfoCallback(nsdManager, foundInfo, { it.run() }, cbRecord)
@@ -1030,6 +1060,52 @@
assertEquals(NsdManager.FAILURE_OPERATION_NOT_RUNNING, failedCb.errorCode)
}
+ @Test
+ fun testSubtypeAdvertisingAndDiscovery() {
+ val si = makeTestServiceInfo(network = testNetwork1.network)
+ // Test "_type._tcp.local,_subtype" syntax with the registration
+ si.serviceType = si.serviceType + ",_subtype"
+
+ val registrationRecord = NsdRegistrationRecord()
+
+ val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+ val subtypeDiscoveryRecord = NsdDiscoveryRecord()
+ val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si)
+
+ // Test "_subtype._type._tcp.local" syntax with discovery. Note this is not
+ // "_subtype._sub._type._tcp.local".
+ nsdManager.discoverServices(serviceType,
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+ nsdManager.discoverServices("_othersubtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+ nsdManager.discoverServices("_subtype.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, Executor { it.run() }, subtypeDiscoveryRecord)
+
+ subtypeDiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ baseTypeDiscoveryRecord.waitForServiceDiscovered(
+ serviceName, serviceType, testNetwork1.network)
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
+ // The subtype callback was registered later but called, no need for an extra delay
+ otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
+ nsdManager.stopServiceDiscovery(subtypeDiscoveryRecord)
+ nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+ baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ subtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
/**
* Register a service and return its registration record.
*/
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index d817630..9d73946 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -422,7 +422,7 @@
.build();
}
- private void testHttpRequest(Socket s) throws IOException {
+ public void testHttpRequest(Socket s) throws IOException {
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
@@ -430,7 +430,9 @@
byte[] responseBytes = new byte[4096];
out.write(requestBytes);
in.read(responseBytes);
- assertTrue(new String(responseBytes, "UTF-8").startsWith("HTTP/1.0 204 No Content\r\n"));
+ final String response = new String(responseBytes, "UTF-8");
+ assertTrue("Received unexpected response: " + response,
+ response.startsWith("HTTP/1.0 204 No Content\r\n"));
}
private Socket getBoundSocket(Network network, String host, int port) throws IOException {
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 3c1340d..67e1296 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -25,7 +25,6 @@
import android.content.ServiceConnection
import android.content.res.Resources
import android.net.ConnectivityManager
-import android.net.ConnectivityResources
import android.net.IDnsResolver
import android.net.INetd
import android.net.LinkProperties
@@ -50,11 +49,15 @@
import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
+import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.ProxyTracker
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
@@ -73,9 +76,6 @@
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.Spy
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import kotlin.test.fail
const val SERVICE_BIND_TIMEOUT_MS = 5_000L
const val TEST_TIMEOUT_MS = 10_000L
@@ -215,8 +215,8 @@
inv.getArgument(2),
object : MultinetworkPolicyTracker.Dependencies() {
override fun getResourcesForActiveSubId(
- connResources: ConnectivityResources,
- activeSubId: Int
+ connResources: ConnectivityResources,
+ activeSubId: Int
) = resources
})
}.`when`(deps).makeMultinetworkPolicyTracker(any(), any(), any())
diff --git a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
index cca0b66..cb3a315 100644
--- a/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
+++ b/tests/unit/java/android/net/util/KeepaliveUtilsTest.kt
@@ -18,7 +18,6 @@
import android.content.Context
import android.content.res.Resources
-import android.net.ConnectivityResources
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.MAX_TRANSPORT
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
@@ -27,7 +26,9 @@
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.os.Build
import androidx.test.filters.SmallTest
-import com.android.internal.R
+import com.android.connectivity.resources.R
+import com.android.server.connectivity.ConnectivityResources
+import com.android.server.connectivity.KeepaliveResourceUtil
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.After
@@ -37,7 +38,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.any
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
@@ -53,14 +53,9 @@
class KeepaliveUtilsTest {
// Prepare mocked context with given resource strings.
- private fun getMockedContextWithStringArrayRes(
- id: Int,
- name: String,
- res: Array<out String?>?
- ): Context {
+ private fun getMockedContextWithStringArrayRes(id: Int, res: Array<out String?>?): Context {
val mockRes = mock(Resources::class.java)
doReturn(res).`when`(mockRes).getStringArray(eq(id))
- doReturn(id).`when`(mockRes).getIdentifier(eq(name), any(), any())
return mock(Context::class.java).apply {
doReturn(mockRes).`when`(this).getResources()
@@ -79,10 +74,10 @@
try {
val mockContext = getMockedContextWithStringArrayRes(
R.array.config_networkSupportedKeepaliveCount,
- "config_networkSupportedKeepaliveCount", res)
- KeepaliveUtils.getSupportedKeepalives(mockContext)
+ res)
+ KeepaliveResourceUtil.getSupportedKeepalives(mockContext)
fail("Expected KeepaliveDeviceConfigurationException")
- } catch (expected: KeepaliveUtils.KeepaliveDeviceConfigurationException) {
+ } catch (expected: KeepaliveResourceUtil.KeepaliveDeviceConfigurationException) {
}
}
@@ -108,8 +103,8 @@
val mockContext = getMockedContextWithStringArrayRes(
R.array.config_networkSupportedKeepaliveCount,
- "config_networkSupportedKeepaliveCount", validRes)
- val actual = KeepaliveUtils.getSupportedKeepalives(mockContext)
+ validRes)
+ val actual = KeepaliveResourceUtil.getSupportedKeepalives(mockContext)
assertArrayEquals(expectedValidRes, actual)
}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index d189848..19fa41d 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -66,6 +66,7 @@
import android.os.Build;
import android.os.ServiceSpecificException;
import android.system.ErrnoException;
+import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import androidx.test.filters.SmallTest;
@@ -1151,4 +1152,33 @@
mCookieTagMap.updateEntry(new CookieTagMapKey(123), new CookieTagMapValue(456, 0x789));
assertDumpContains(getDump(), "cookie=123 tag=0x789 uid=456");
}
+
+ @Test
+ public void testGetUids() throws ErrnoException {
+ final int uid0 = TEST_UIDS[0];
+ final int uid1 = TEST_UIDS[1];
+ final long match0 = DOZABLE_MATCH | POWERSAVE_MATCH;
+ final long match1 = DOZABLE_MATCH | STANDBY_MATCH;
+ mUidOwnerMap.updateEntry(new S32(uid0), new UidOwnerValue(NULL_IIF, match0));
+ mUidOwnerMap.updateEntry(new S32(uid1), new UidOwnerValue(NULL_IIF, match1));
+
+ assertEquals(new ArraySet<>(List.of(uid0, uid1)),
+ mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_DOZABLE));
+ assertEquals(new ArraySet<>(List.of(uid0)),
+ mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_POWERSAVE));
+
+ assertEquals(new ArraySet<>(List.of(uid1)),
+ mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(FIREWALL_CHAIN_STANDBY));
+ assertEquals(new ArraySet<>(),
+ mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(FIREWALL_CHAIN_OEM_DENY_1));
+ }
+
+ @Test
+ public void testGetUidsIllegalArgument() {
+ final Class<IllegalArgumentException> expected = IllegalArgumentException.class;
+ assertThrows(expected,
+ () -> mBpfNetMaps.getUidsWithDenyRuleOnDenyListChain(FIREWALL_CHAIN_DOZABLE));
+ assertThrows(expected,
+ () -> mBpfNetMaps.getUidsWithAllowRuleOnAllowListChain(FIREWALL_CHAIN_OEM_DENY_1));
+ }
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3188c9c..43c6225 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -30,6 +30,8 @@
import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+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;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
@@ -148,12 +150,14 @@
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.IPPROTO_TCP;
+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;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_OEM;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_PROFILE;
import static com.android.server.ConnectivityService.PREFERENCE_ORDER_VPN;
import static com.android.server.ConnectivityService.createDeliveryGroupKeyForConnectivityAction;
+import static com.android.server.ConnectivityService.makeNflogPrefix;
import static com.android.server.ConnectivityServiceTestUtils.transportToLegacyType;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackRegister;
import static com.android.server.NetworkAgentWrapper.CallbackType.OnQosCallbackUnregister;
@@ -163,6 +167,7 @@
import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.FunctionalUtils.ignoreExceptions;
+import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
import static com.android.testutils.HandlerUtils.waitForIdleSerialExecutor;
import static com.android.testutils.MiscAsserts.assertContainsAll;
import static com.android.testutils.MiscAsserts.assertContainsExactly;
@@ -223,6 +228,8 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.UidFrozenStateChangedCallback;
import android.app.AlarmManager;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
@@ -255,7 +262,6 @@
import android.net.ConnectivityManager.PacketKeepalive;
import android.net.ConnectivityManager.PacketKeepaliveCallback;
import android.net.ConnectivityManager.TooManyRequestsException;
-import android.net.ConnectivityResources;
import android.net.ConnectivitySettingsManager;
import android.net.ConnectivityThread;
import android.net.DataStallReportParcelable;
@@ -371,7 +377,6 @@
import com.android.internal.util.WakeupMessage;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.ArrayTrackRecord;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.LocationPermissionChecker;
@@ -382,12 +387,14 @@
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
import com.android.server.ConnectivityService.NetworkRequestInfo;
+import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.DestroySocketsWrapper;
import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
import com.android.server.connectivity.ApplicationSelfCertifiedNetworkCapabilities;
import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
import com.android.server.connectivity.ClatCoordinator;
import com.android.server.connectivity.ConnectivityFlags;
+import com.android.server.connectivity.ConnectivityResources;
import com.android.server.connectivity.MultinetworkPolicyTracker;
import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies;
import com.android.server.connectivity.Nat464Xlat;
@@ -533,6 +540,9 @@
private static final int TEST_PACKAGE_UID = 123;
private static final int TEST_PACKAGE_UID2 = 321;
private static final int TEST_PACKAGE_UID3 = 456;
+
+ private static final int PACKET_WAKEUP_MARK_MASK = 0x80000000;
+
private static final String ALWAYS_ON_PACKAGE = "com.android.test.alwaysonvpn";
private static final String INTERFACE_NAME = "interface";
@@ -604,6 +614,8 @@
@Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
@Mock TetheringManager mTetheringManager;
@Mock BroadcastOptionsShim mBroadcastOptionsShim;
+ @Mock ActivityManager mActivityManager;
+ @Mock DestroySocketsWrapper mDestroySocketsWrapper;
// BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
// underlying binder calls.
@@ -727,6 +739,7 @@
if (Context.BATTERY_STATS_SERVICE.equals(name)) return mBatteryStatsManager;
if (Context.PAC_PROXY_SERVICE.equals(name)) return mPacProxyManager;
if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
+ if (Context.ACTIVITY_SERVICE.equals(name)) return mActivityManager;
return super.getSystemService(name);
}
@@ -855,7 +868,7 @@
@Override
public void sendStickyBroadcast(Intent intent, Bundle options) {
// Verify that delivery group policy APIs were used on U.
- if (SdkLevel.isAtLeastU() && CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ if (mDeps.isAtLeastU() && CONNECTIVITY_ACTION.equals(intent.getAction())) {
final NetworkInfo ni = intent.getParcelableExtra(EXTRA_NETWORK_INFO,
NetworkInfo.class);
try {
@@ -1071,7 +1084,7 @@
}
private void onValidationRequested() throws Exception {
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
verify(mNetworkMonitor).notifyNetworkConnectedParcel(any());
} else {
verify(mNetworkMonitor).notifyNetworkConnected(any(), any());
@@ -1910,10 +1923,15 @@
doReturn(0).when(mResources).getInteger(R.integer.config_activelyPreferBadWifi);
doReturn(true).when(mResources)
.getBoolean(R.bool.config_cellular_radio_timesharing_capable);
+ doReturn(PACKET_WAKEUP_MARK_MASK).when(mResources).getInteger(
+ R.integer.config_networkWakeupPacketMask);
+ doReturn(PACKET_WAKEUP_MARK_MASK).when(mResources).getInteger(
+ R.integer.config_networkWakeupPacketMark);
}
class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
final ConnectivityResources mConnRes;
+ final ArraySet<Pair<Long, Integer>> mEnabledChangeIds = new ArraySet<>();
ConnectivityServiceDependencies(final Context mockResContext) {
mConnRes = new ConnectivityResources(mockResContext);
@@ -1979,7 +1997,7 @@
@Override
public CarrierPrivilegeAuthenticator makeCarrierPrivilegeAuthenticator(
@NonNull final Context context, @NonNull final TelephonyManager tm) {
- return SdkLevel.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
+ return mDeps.isAtLeastT() ? mCarrierPrivilegeAuthenticator : null;
}
@Override
@@ -2067,15 +2085,72 @@
}
@Override
- public boolean isFeatureEnabled(Context context, String name, boolean defaultEnabled) {
+ public boolean isFeatureEnabled(Context context, String name) {
switch (name) {
case ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER:
return true;
+ case KEY_DESTROY_FROZEN_SOCKETS_VERSION:
+ return true;
default:
- return super.isFeatureEnabled(context, name, defaultEnabled);
+ return super.isFeatureEnabled(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
+ // make sure both threads see it before continuing.
+ visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> {
+ if (enabled) {
+ mEnabledChangeIds.add(data);
+ } else {
+ mEnabledChangeIds.remove(data);
+ }
+ });
+ }
+
+ @Override
+ public boolean isChangeEnabled(final long changeId, final int uid) {
+ return mEnabledChangeIds.contains(new Pair<>(changeId, uid));
+ }
+
+ // In AOSP, build version codes are all over the place (e.g. at the time of this writing
+ // U == V). Define custom ones.
+ private static final int VERSION_UNMOCKED = -1;
+ private static final int VERSION_R = 1;
+ private static final int VERSION_S = 2;
+ private static final int VERSION_T = 3;
+ private static final int VERSION_U = 4;
+ private static final int VERSION_V = 5;
+ private static final int VERSION_MAX = VERSION_V;
+ private int mSdkLevel = VERSION_UNMOCKED;
+
+ private void setBuildSdk(final int sdkLevel) {
+ if (sdkLevel > VERSION_MAX) {
+ throw new IllegalArgumentException("setBuildSdk must not be called with"
+ + " Build.VERSION constants but Dependencies.VERSION_* constants");
+ }
+ visibleOnHandlerThread(mCsHandlerThread.getThreadHandler(), () -> mSdkLevel = sdkLevel);
+ }
+
+ @Override
+ public boolean isAtLeastS() {
+ return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastS()
+ : mSdkLevel >= VERSION_S;
+ }
+
+ @Override
+ public boolean isAtLeastT() {
+ return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastT()
+ : mSdkLevel >= VERSION_T;
+ }
+
+ @Override
+ public boolean isAtLeastU() {
+ return mSdkLevel == VERSION_UNMOCKED ? super.isAtLeastU()
+ : mSdkLevel >= VERSION_U;
+ }
+
@Override
public BpfNetMaps getBpfNetMaps(Context context, INetd netd) {
return mBpfNetMaps;
@@ -2148,6 +2223,35 @@
}
}
}
+
+ // Class to be mocked and used to verify destroy sockets methods call
+ public class DestroySocketsWrapper {
+ public void destroyLiveTcpSockets(final Set<Range<Integer>> ranges,
+ final Set<Integer> exemptUids){}
+ public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids){}
+ }
+
+ @Override @SuppressWarnings("DirectInvocationOnMock")
+ public void destroyLiveTcpSockets(final Set<Range<Integer>> ranges,
+ final Set<Integer> exemptUids) {
+ // Call mocked destroyLiveTcpSockets so that test can verify this method call
+ mDestroySocketsWrapper.destroyLiveTcpSockets(ranges, exemptUids);
+ }
+
+ @Override @SuppressWarnings("DirectInvocationOnMock")
+ public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids) {
+ // Call mocked destroyLiveTcpSocketsByOwnerUids so that test can verify this method call
+ mDestroySocketsWrapper.destroyLiveTcpSocketsByOwnerUids(ownerUids);
+ }
+
+ final ArrayTrackRecord<Long>.ReadHead mScheduledEvaluationTimeouts =
+ new ArrayTrackRecord<Long>().newReadHead();
+ @Override
+ public void scheduleEvaluationTimeout(@NonNull Handler handler,
+ @NonNull final Network network, final long delayMs) {
+ mScheduledEvaluationTimeouts.add(delayMs);
+ super.scheduleEvaluationTimeout(handler, network, delayMs);
+ }
}
private class AutomaticOnOffKeepaliveTrackerDependencies
@@ -2401,10 +2505,14 @@
}
private ExpectedBroadcast expectProxyChangeAction(ProxyInfo proxy) {
+ return expectProxyChangeAction(actualProxy -> proxy.equals(actualProxy));
+ }
+
+ private ExpectedBroadcast expectProxyChangeAction(Predicate<ProxyInfo> tester) {
return registerBroadcastReceiverThat(PROXY_CHANGE_ACTION, 1, intent -> {
final ProxyInfo actualProxy = (ProxyInfo) intent.getExtra(Proxy.EXTRA_PROXY_INFO,
ProxyInfo.buildPacProxy(Uri.EMPTY));
- return proxy.equals(actualProxy);
+ return tester.test(actualProxy);
});
}
@@ -3785,6 +3893,12 @@
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, callbacks);
+ if (mService.shouldCreateNetworksImmediately()) {
+ assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertNull(eventOrder.poll());
+ }
+
// Connect a network, and file a request for it after it has come up, to ensure the nascent
// timer is cleared and the test does not have to wait for it. Filing the request after the
// network has come up is necessary because ConnectivityService does not appear to clear the
@@ -3792,7 +3906,12 @@
// connected.
// TODO: fix this bug, file the request before connecting, and remove the waitForIdle.
mWiFiAgent.connectWithoutInternet();
- waitForIdle();
+ if (!mService.shouldCreateNetworksImmediately()) {
+ assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ } else {
+ waitForIdle();
+ assertNull(eventOrder.poll());
+ }
mCm.requestNetwork(request, callback);
callback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
@@ -3809,7 +3928,6 @@
// Disconnect the network and check that events happened in the right order.
mCm.unregisterNetworkCallback(callback);
- assertEquals("onNetworkCreated", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals("onNetworkUnwanted", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals("timePasses", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
assertEquals("onNetworkDisconnected", eventOrder.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
@@ -5874,7 +5992,7 @@
doReturn(0).when(mResources).getInteger(R.integer.config_activelyPreferBadWifi);
mPolicyTracker.reevaluate();
waitForIdle();
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
// U+ ignore the setting and always actively prefers bad wifi
assertTrue(mService.mNetworkRanker.getConfiguration().activelyPreferBadWifi());
} else {
@@ -6002,10 +6120,13 @@
wifiCallback.assertNoCallback();
}
- public void doTestPreferBadWifi(final boolean preferBadWifi) throws Exception {
+ public void doTestPreferBadWifi(final boolean avoidBadWifi,
+ final boolean preferBadWifi,
+ @NonNull Predicate<Long> checkUnvalidationTimeout) throws Exception {
// Pretend we're on a carrier that restricts switching away from bad wifi, and
// depending on the parameter one that may indeed prefer bad wifi.
- doReturn(0).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
+ doReturn(avoidBadWifi ? 1 : 0).when(mResources)
+ .getInteger(R.integer.config_networkAvoidBadWifi);
doReturn(preferBadWifi ? 1 : 0).when(mResources)
.getInteger(R.integer.config_activelyPreferBadWifi);
mPolicyTracker.reevaluate();
@@ -6027,7 +6148,9 @@
mWiFiAgent.connect(false);
wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
- if (preferBadWifi) {
+ mDeps.mScheduledEvaluationTimeouts.poll(TIMEOUT_MS, t -> checkUnvalidationTimeout.test(t));
+
+ if (!avoidBadWifi && preferBadWifi) {
expectUnvalidationCheckWillNotify(mWiFiAgent, NotificationType.LOST_INTERNET);
mDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
} else {
@@ -6037,15 +6160,31 @@
}
@Test
- public void testPreferBadWifi_doNotPrefer() throws Exception {
+ public void testPreferBadWifi_doNotAvoid_doNotPrefer() throws Exception {
// Starting with U this mode is no longer supported and can't actually be tested
- assumeFalse(SdkLevel.isAtLeastU());
- doTestPreferBadWifi(false /* preferBadWifi */);
+ assumeFalse(mDeps.isAtLeastU());
+ doTestPreferBadWifi(false /* avoidBadWifi */, false /* preferBadWifi */,
+ timeout -> timeout < 14_000);
}
@Test
- public void testPreferBadWifi_doPrefer() throws Exception {
- doTestPreferBadWifi(true /* preferBadWifi */);
+ public void testPreferBadWifi_doNotAvoid_doPrefer() throws Exception {
+ doTestPreferBadWifi(false /* avoidBadWifi */, true /* preferBadWifi */,
+ timeout -> timeout > 14_000);
+ }
+
+ @Test
+ public void testPreferBadWifi_doAvoid_doNotPrefer() throws Exception {
+ // If avoidBadWifi=true, then preferBadWifi should be irrelevant. Test anyway.
+ doTestPreferBadWifi(true /* avoidBadWifi */, false /* preferBadWifi */,
+ timeout -> timeout < 14_000);
+ }
+
+ @Test
+ public void testPreferBadWifi_doAvoid_doPrefer() throws Exception {
+ // If avoidBadWifi=true, then preferBadWifi should be irrelevant. Test anyway.
+ doTestPreferBadWifi(true /* avoidBadWifi */, true /* preferBadWifi */,
+ timeout -> timeout < 14_000);
}
@Test
@@ -7595,7 +7734,9 @@
// Simple connection with initial LP should have updated ifaces.
mCellAgent.connect(false);
waitForIdle();
- expectNotifyNetworkStatus(onlyCell(), onlyCell(), MOBILE_IFNAME);
+ List<Network> allNetworks = mService.shouldCreateNetworksImmediately()
+ ? cellAndWifi() : onlyCell();
+ expectNotifyNetworkStatus(allNetworks, onlyCell(), MOBILE_IFNAME);
reset(mStatsManager);
// Verify change fields other than interfaces does not trigger a notification to NSS.
@@ -7904,9 +8045,13 @@
setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ final int netId = mCellAgent.getNetwork().netId;
waitForIdle();
- verify(mMockDnsResolver, never()).setResolverConfiguration(any());
- verifyNoMoreInteractions(mMockDnsResolver);
+ if (mService.shouldCreateNetworksImmediately()) {
+ verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
+ } else {
+ verify(mMockDnsResolver, never()).setResolverConfiguration(any());
+ }
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
@@ -7922,10 +8067,13 @@
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(false);
waitForIdle();
-
- verify(mMockDnsResolver, times(1)).createNetworkCache(eq(mCellAgent.getNetwork().netId));
- // CS tells dnsresolver about the empty DNS config for this network.
+ if (!mService.shouldCreateNetworksImmediately()) {
+ // CS tells dnsresolver about the empty DNS config for this network.
+ verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
+ }
verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(any());
+
+ verifyNoMoreInteractions(mMockDnsResolver);
reset(mMockDnsResolver);
cellLp.addDnsServer(InetAddress.getByName("2001:db8::1"));
@@ -8040,10 +8188,13 @@
mCm.requestNetwork(cellRequest, cellNetworkCallback);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ final int netId = mCellAgent.getNetwork().netId;
waitForIdle();
- // CS tells netd about the empty DNS config for this network.
- verify(mMockDnsResolver, never()).setResolverConfiguration(any());
- verifyNoMoreInteractions(mMockDnsResolver);
+ if (mService.shouldCreateNetworksImmediately()) {
+ verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
+ } else {
+ verify(mMockDnsResolver, never()).setResolverConfiguration(any());
+ }
final LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
@@ -8062,7 +8213,9 @@
mCellAgent.sendLinkProperties(cellLp);
mCellAgent.connect(false);
waitForIdle();
- verify(mMockDnsResolver, times(1)).createNetworkCache(eq(mCellAgent.getNetwork().netId));
+ if (!mService.shouldCreateNetworksImmediately()) {
+ verify(mMockDnsResolver, times(1)).createNetworkCache(netId);
+ }
verify(mMockDnsResolver, atLeastOnce()).setResolverConfiguration(
mResolverParamsParcelCaptor.capture());
ResolverParamsParcel resolvrParams = mResolverParamsParcelCaptor.getValue();
@@ -8073,6 +8226,7 @@
assertEquals(2, resolvrParams.tlsServers.length);
assertTrue(new ArraySet<>(resolvrParams.tlsServers).containsAll(
asList("2001:db8::1", "192.0.2.1")));
+ verifyNoMoreInteractions(mMockDnsResolver);
reset(mMockDnsResolver);
cellNetworkCallback.expect(AVAILABLE, mCellAgent);
cellNetworkCallback.expect(NETWORK_CAPS_UPDATED, mCellAgent);
@@ -9677,7 +9831,7 @@
final Set<Integer> excludedUids = new ArraySet<Integer>();
excludedUids.add(VPN_UID);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
// On T onwards, the corresponding SDK sandbox UID should also be excluded
excludedUids.add(toSdkSandboxUid(VPN_UID));
}
@@ -9725,7 +9879,7 @@
vpnDefaultCallbackAsUid.assertNoCallback();
excludedUids.add(uid);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
// On T onwards, the corresponding SDK sandbox UID should also be excluded
excludedUids.add(toSdkSandboxUid(uid));
}
@@ -10219,6 +10373,50 @@
}
}
+ private void doTestSetFirewallChainEnabledCloseSocket(final int chain,
+ final boolean isAllowList) throws Exception {
+ reset(mDestroySocketsWrapper);
+
+ mCm.setFirewallChainEnabled(chain, true /* enabled */);
+ final Set<Integer> uids =
+ new ArraySet<>(List.of(TEST_PACKAGE_UID, TEST_PACKAGE_UID2));
+ if (isAllowList) {
+ final Set<Range<Integer>> range = new ArraySet<>(
+ List.of(new Range<>(Process.FIRST_APPLICATION_UID, Integer.MAX_VALUE)));
+ verify(mDestroySocketsWrapper).destroyLiveTcpSockets(range, uids);
+ } else {
+ verify(mDestroySocketsWrapper).destroyLiveTcpSocketsByOwnerUids(uids);
+ }
+
+ mCm.setFirewallChainEnabled(chain, false /* enabled */);
+ verifyNoMoreInteractions(mDestroySocketsWrapper);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testSetFirewallChainEnabledCloseSocket() throws Exception {
+ doReturn(new ArraySet<>(Arrays.asList(TEST_PACKAGE_UID, TEST_PACKAGE_UID2)))
+ .when(mBpfNetMaps)
+ .getUidsWithDenyRuleOnDenyListChain(anyInt());
+ doReturn(new ArraySet<>(Arrays.asList(TEST_PACKAGE_UID, TEST_PACKAGE_UID2)))
+ .when(mBpfNetMaps)
+ .getUidsWithAllowRuleOnAllowListChain(anyInt());
+
+ 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);
+
+ 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);
+ doTestSetFirewallChainEnabledCloseSocket(FIREWALL_CHAIN_OEM_DENY_3, denylist);
+ }
+
private void doTestReplaceFirewallChain(final int chain) {
final int[] uids = new int[] {1001, 1002};
mCm.replaceFirewallChain(chain, uids);
@@ -10386,11 +10584,22 @@
return event;
}
+ private void verifyWakeupModifyInterface(String iface, boolean add) throws RemoteException {
+ if (add) {
+ verify(mMockNetd).wakeupAddInterface(eq(iface), anyString(), anyInt(),
+ anyInt());
+ } else {
+ verify(mMockNetd).wakeupDelInterface(eq(iface), anyString(), anyInt(),
+ anyInt());
+ }
+ }
+
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
if (inOrder != null) {
return inOrder.verify(t);
} else {
- return verify(t);
+ // times(1) for consistency with the above. InOrder#verify always implies times(1).
+ return verify(t, times(1));
}
}
@@ -10404,7 +10613,7 @@
private void verifyClatdStart(@Nullable InOrder inOrder, @NonNull String iface, int netId,
@NonNull String nat64Prefix) throws Exception {
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
verifyWithOrder(inOrder, mClatCoordinator)
.clatStart(eq(iface), eq(netId), eq(new IpPrefix(nat64Prefix)));
} else {
@@ -10414,7 +10623,7 @@
private void verifyNeverClatdStart(@Nullable InOrder inOrder, @NonNull String iface)
throws Exception {
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
verifyNeverWithOrder(inOrder, mClatCoordinator).clatStart(eq(iface), anyInt(), any());
} else {
verifyNeverWithOrder(inOrder, mMockNetd).clatdStart(eq(iface), anyString());
@@ -10423,7 +10632,7 @@
private void verifyClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
throws Exception {
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
verifyWithOrder(inOrder, mClatCoordinator).clatStop();
} else {
verifyWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
@@ -10432,13 +10641,28 @@
private void verifyNeverClatdStop(@Nullable InOrder inOrder, @NonNull String iface)
throws Exception {
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
verifyNeverWithOrder(inOrder, mClatCoordinator).clatStop();
} else {
verifyNeverWithOrder(inOrder, mMockNetd).clatdStop(eq(iface));
}
}
+ private void expectNativeNetworkCreated(int netId, int permission, String iface,
+ InOrder inOrder) throws Exception {
+ verifyWithOrder(inOrder, mMockNetd).networkCreate(nativeNetworkConfigPhysical(netId,
+ permission));
+ verifyWithOrder(inOrder, mMockDnsResolver).createNetworkCache(eq(netId));
+ if (iface != null) {
+ verifyWithOrder(inOrder, mMockNetd).networkAddInterface(netId, iface);
+ }
+ }
+
+ private void expectNativeNetworkCreated(int netId, int permission, String iface)
+ throws Exception {
+ expectNativeNetworkCreated(netId, permission, iface, null /* inOrder */);
+ }
+
@Test
public void testStackedLinkProperties() throws Exception {
final LinkAddress myIpv4 = new LinkAddress("1.2.3.4/24");
@@ -10476,11 +10700,8 @@
int cellNetId = mCellAgent.getNetwork().netId;
waitForIdle();
- verify(mMockNetd, times(1)).networkCreate(nativeNetworkConfigPhysical(cellNetId,
- INetd.PERMISSION_NONE));
+ expectNativeNetworkCreated(cellNetId, INetd.PERMISSION_NONE, MOBILE_IFNAME);
assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default);
- verify(mMockDnsResolver, times(1)).createNetworkCache(eq(cellNetId));
- verify(mMockNetd, times(1)).networkAddInterface(cellNetId, MOBILE_IFNAME);
final ArrayTrackRecord<ReportedInterfaces>.ReadHead readHead =
mDeps.mReportedInterfaceHistory.newReadHead();
assertNotNull(readHead.poll(TIMEOUT_MS, ri -> ri.contentEquals(mServiceContext,
@@ -10612,6 +10833,11 @@
clat.interfaceRemoved(CLAT_MOBILE_IFNAME);
networkCallback.assertNoCallback();
verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
+
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
+ }
+
verifyNoMoreInteractions(mMockNetd);
verifyNoMoreInteractions(mClatCoordinator);
verifyNoMoreInteractions(mMockDnsResolver);
@@ -10648,6 +10874,10 @@
assertRoutesAdded(cellNetId, stackedDefault);
verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, true);
+ }
+
// NAT64 prefix is removed. Expect that clat is stopped.
mService.mResolverUnsolEventCallback.onNat64PrefixEvent(makeNat64PrefixEvent(
cellNetId, PREFIX_OPERATION_REMOVED, kNat64PrefixString, 96));
@@ -10662,6 +10892,11 @@
cb -> cb.getLp().getStackedLinks().size() == 0);
verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
verify(mMockNetd, times(1)).interfaceGetCfg(CLAT_MOBILE_IFNAME);
+
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
+ }
+
// Clean up.
mCellAgent.disconnect();
networkCallback.expect(LOST, mCellAgent);
@@ -10669,11 +10904,16 @@
verify(mMockNetd, times(1)).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
eq(Integer.toString(TRANSPORT_CELLULAR)));
verify(mMockNetd).networkDestroy(cellNetId);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
} else {
verify(mMockNetd, never()).setNetworkAllowlist(any());
}
+
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(MOBILE_IFNAME, false);
+ }
+
verifyNoMoreInteractions(mMockNetd);
verifyNoMoreInteractions(mClatCoordinator);
reset(mMockNetd);
@@ -10703,6 +10943,11 @@
verify(mMockNetd).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
// assertRoutesAdded sees all calls since last mMockNetd reset, so expect IPv6 routes again.
assertRoutesAdded(cellNetId, ipv6Subnet, ipv6Default, stackedDefault);
+
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(MOBILE_IFNAME, true);
+ }
+
reset(mMockNetd);
reset(mClatCoordinator);
@@ -10711,14 +10956,24 @@
networkCallback.expect(LOST, mCellAgent);
networkCallback.assertNoCallback();
verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
+
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
+ }
+
verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
eq(Integer.toString(TRANSPORT_CELLULAR)));
verify(mMockNetd).networkDestroy(cellNetId);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
verify(mMockNetd).setNetworkAllowlist(any());
} else {
verify(mMockNetd, never()).setNetworkAllowlist(any());
}
+
+ if (mDeps.isAtLeastU()) {
+ verifyWakeupModifyInterface(MOBILE_IFNAME, false);
+ }
+
verifyNoMoreInteractions(mMockNetd);
verifyNoMoreInteractions(mClatCoordinator);
@@ -11080,6 +11335,278 @@
assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
}
+ /*
+ * Note for maintainers about how PAC proxies are implemented in Android.
+ *
+ * Generally, a proxy is just a hostname and a port to which requests are sent, instead of
+ * sending them directly. Most HTTP libraries know to use this protocol, and the Java
+ * language has properties to help handling these :
+ * https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
+ * Unfortunately these properties are very old and do not take multi-networking into account.
+ *
+ * A PAC proxy consists of a javascript file stored on a server, and the client is expected to
+ * download the file and interpret the javascript for each HTTP request to know where to direct
+ * it. The device must therefore run code (namely, a javascript interpreter) to interpret the
+ * PAC file correctly. Most HTTP libraries do not know how to do this, since they do not
+ * embark a javascript interpreter (and it would be generally unreasonable for them to do
+ * so). Some apps, notably browsers, do know how to do this, but they are the exception rather
+ * than the rule.
+ * So to support most apps, Android embarks the javascript interpreter. When a network is
+ * configured to have a PAC proxy, Android will first set the ProxyInfo object to an object
+ * that contains the PAC URL (to communicate that to apps that know how to use it), then
+ * download the PAC file and start a native process which will open a server on localhost,
+ * and uses the interpreter inside WebView to interpret the PAC file. This server takes
+ * a little bit of time to start and will listen on a random port. When the port is known,
+ * the framework receives a notification and it updates the ProxyInfo in LinkProperties
+ * as well as send a broadcast to inform apps. This new ProxyInfo still contains the PAC URL,
+ * but it also contains "localhost" as the host and the port that the server listens to as
+ * the port. This will let HTTP libraries that don't have a javascript interpreter work,
+ * because they'll send the requests to this server running on localhost on the correct port,
+ * and this server will do what is appropriate with each request according to the PAC file.
+ *
+ * Note that at the time of this writing, Android does not support starting multiple local
+ * PAC servers, though this would be possible in theory by just starting multiple instances
+ * of this process running their server on different ports. As a stopgap measure, Android
+ * keeps one local server which is always the one for the default network. If a non-default
+ * network has a PAC proxy, it will have a LinkProperties with a port of -1, which means it
+ * could still be used by apps that know how to use the PAC URL directly, but not by HTTP
+ * libs that don't know how to do that. When a network with a PAC proxy becomes the default,
+ * the local server is started. When a network without a PAC proxy becomes the default, the
+ * local server is stopped if it is running (and the LP for the old default network should
+ * be reset to have a port of -1).
+ *
+ * In principle, each network can be configured with a different proxy (typically in the
+ * network settings for a Wi-Fi network). These end up exposed in the LinkProperties of the
+ * relevant network.
+ * Android also exposes ConnectivityManager#getDefaultProxy, which is simply the proxy for
+ * the default network. This was retrofitted from a time where Android did not support multiple
+ * concurrent networks, hence the difficult architecture.
+ * Note that there is also a "global" proxy that can be set by the DPM. When this is set,
+ * it overrides the proxy settings for every single network at the same time – that is, the
+ * system behaves as if the global proxy is the configured proxy for each network.
+ *
+ * Generally there are four ways for apps to look up the proxy.
+ * - Looking up the ProxyInfo in the LinkProperties for a network.
+ * - Listening to the PROXY_CHANGED_ACTION broadcast
+ * - Calling ConnectivityManager#getDefaultProxy, or ConnectivityManager#getProxyForNetwork
+ * which can be used to retrieve the proxy for any given network or the default network by
+ * passing null.
+ * - Reading the standard JVM properties (http{s,}.proxy{Host,Port}). See the Java
+ * distribution documentation for details on how these are supposed to work :
+ * https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html
+ * In Android, these are set by ActivityThread in each process in response to the broadcast.
+ * Many apps actually use these, and it's important they work because it's part of the
+ * Java standard, meaning they need to be set for existing Java libs to work on Android.
+ */
+ @Test
+ public void testPacProxy() throws Exception {
+ final Uri pacUrl = Uri.parse("https://pac-url");
+
+ final TestNetworkCallback cellCallback = new TestNetworkCallback();
+ final NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ // Request cell to make sure it doesn't disconnect at an arbitrary point in the test,
+ // which would make testing the callbacks on it difficult.
+ mCm.requestNetwork(cellRequest, cellCallback);
+ mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ mCellAgent.connect(true);
+ cellCallback.expectAvailableThenValidatedCallbacks(mCellAgent);
+
+ final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+ final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI).build();
+ mCm.registerNetworkCallback(wifiRequest, wifiCallback);
+
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ mWiFiAgent.connect(true);
+ wifiCallback.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+ cellCallback.assertNoCallback();
+
+ final ProxyInfo initialProxyInfo = ProxyInfo.buildPacProxy(pacUrl);
+ final LinkProperties testLinkProperties = new LinkProperties();
+ testLinkProperties.setHttpProxy(initialProxyInfo);
+ mWiFiAgent.sendLinkProperties(testLinkProperties);
+ wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+ cellCallback.assertNoCallback();
+
+ // At first the local PAC proxy server is unstarted (see the description of what the local
+ // server is in the comment at the top of this method). It will contain the PAC URL and a
+ // port of -1 because it is unstarted. Check that all ways of getting that proxy info
+ // returns the same object that was initially created.
+ final ProxyInfo unstartedDefaultProxyInfo = mService.getProxyForNetwork(null);
+ final ProxyInfo unstartedWifiProxyInfo = mService.getProxyForNetwork(
+ mWiFiAgent.getNetwork());
+ final LinkProperties unstartedLp =
+ mService.getLinkProperties(mWiFiAgent.getNetwork());
+
+ assertEquals(initialProxyInfo, unstartedDefaultProxyInfo);
+ assertEquals(initialProxyInfo, unstartedWifiProxyInfo);
+ assertEquals(initialProxyInfo, unstartedLp.getHttpProxy());
+
+ // Make sure the cell network is unaffected. The LP are per-network and each network has
+ // its own configuration. The default proxy and broadcast are system-wide, and the JVM
+ // properties are per-process, and therefore can have only one value at any given time,
+ // so the code sets them to the proxy of the default network (TODO : really, since the
+ // default process is per-network, the JVM properties (http.proxyHost family – see
+ // the comment at the top of the method for details about these) also should be per-network
+ // and even the broadcast contents should be but none of this is implemented). The LP are
+ // still per-network, and a process that wants to use a non-default network is supposed to
+ // look up the proxy in its LP and it has to be correct.
+ assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+
+ // Simulate PacManager sending the notification that the local server has started
+ final ProxyInfo servingProxyInfo = new ProxyInfo(pacUrl, 2097);
+ final ExpectedBroadcast servingProxyBroadcast = expectProxyChangeAction(servingProxyInfo);
+ mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+ wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+ cellCallback.assertNoCallback();
+ servingProxyBroadcast.expectBroadcast();
+
+ final ProxyInfo startedDefaultProxyInfo = mService.getProxyForNetwork(null);
+ final ProxyInfo startedWifiProxyInfo = mService.getProxyForNetwork(
+ mWiFiAgent.getNetwork());
+ final LinkProperties startedLp = mService.getLinkProperties(mWiFiAgent.getNetwork());
+ assertEquals(servingProxyInfo, startedDefaultProxyInfo);
+ assertEquals(servingProxyInfo, startedWifiProxyInfo);
+ assertEquals(servingProxyInfo, startedLp.getHttpProxy());
+ // Make sure the cell network is still unaffected
+ assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+
+ // Start an ethernet network which will become the default, in order to test what happens
+ // to the proxy of wifi while manipulating the proxy of ethernet.
+ final Uri ethPacUrl = Uri.parse("https://ethernet-pac-url");
+ final TestableNetworkCallback ethernetCallback = new TestableNetworkCallback();
+ final NetworkRequest ethernetRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_ETHERNET).build();
+ mEthernetAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
+ mCm.registerNetworkCallback(ethernetRequest, ethernetCallback);
+ mEthernetAgent.connect(true);
+ ethernetCallback.expectAvailableThenValidatedCallbacks(mEthernetAgent);
+
+ // Wifi is no longer the default, so it should get a callback to LP changed with a PAC
+ // proxy but a port of -1 (since the local proxy doesn't serve wifi now)
+ wifiCallback.expect(LINK_PROPERTIES_CHANGED, mWiFiAgent,
+ lp -> lp.getLp().getHttpProxy().getPort() == -1
+ && lp.getLp().getHttpProxy().isPacProxy());
+ // Wifi is lingered as it was the default but is no longer serving any request.
+ wifiCallback.expect(CallbackEntry.LOSING, mWiFiAgent);
+
+ // Now arrange for Ethernet to have a PAC proxy.
+ final ProxyInfo ethProxy = ProxyInfo.buildPacProxy(ethPacUrl);
+ final LinkProperties ethLinkProperties = new LinkProperties();
+ ethLinkProperties.setHttpProxy(ethProxy);
+ mEthernetAgent.sendLinkProperties(ethLinkProperties);
+ ethernetCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mEthernetAgent);
+ // Default network is Ethernet
+ assertEquals(ethProxy, mService.getProxyForNetwork(null));
+ assertEquals(ethProxy, mService.getProxyForNetwork(mEthernetAgent.getNetwork()));
+ // Proxy info for WiFi ideally should be the old one with the old server still running,
+ // but as the PAC code only handles one server at any given time, this can't work. Wifi
+ // having null as a proxy also won't work (apps using WiFi will try to access the network
+ // without proxy) but is not as bad as having the new proxy (that would send requests
+ // over the default network).
+ assertEquals(initialProxyInfo, mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+
+ // Expect the local PAC proxy server starts to serve the proxy on Ethernet. Use
+ // simulateUpdateProxyInfo to simulate this, and check that the callback is called to
+ // inform apps of the port and that the various networks have the expected proxies in
+ // their link properties.
+ final ProxyInfo servingEthProxy = new ProxyInfo(ethPacUrl, 2099);
+ final ExpectedBroadcast servingEthProxyBroadcast = expectProxyChangeAction(servingEthProxy);
+ final ExpectedBroadcast servingProxyBroadcast2 = expectProxyChangeAction(servingProxyInfo);
+ mService.simulateUpdateProxyInfo(mEthernetAgent.getNetwork(), servingEthProxy);
+ ethernetCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mEthernetAgent);
+ assertEquals(servingEthProxy, mService.getProxyForNetwork(null));
+ assertEquals(servingEthProxy, mService.getProxyForNetwork(mEthernetAgent.getNetwork()));
+ assertEquals(initialProxyInfo, mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+ servingEthProxyBroadcast.expectBroadcast();
+
+ // Ethernet disconnects, back to WiFi
+ mEthernetAgent.disconnect();
+ ethernetCallback.expect(CallbackEntry.LOST, mEthernetAgent);
+
+ // WiFi is now the default network again. However, the local proxy server does not serve
+ // WiFi at this time, so at this time a proxy with port -1 is still the correct value.
+ // In time, the local proxy server for ethernet will be downed and the local proxy server
+ // for WiFi will be restarted, and WiFi will see an update to its LP with the new port,
+ // but in this test this won't happen until the test simulates the local proxy starting
+ // up for WiFi (which is done just a few lines below). This is therefore the perfect place
+ // to check that WiFi is unaffected until the new local proxy starts up.
+ wifiCallback.assertNoCallback();
+ assertEquals(initialProxyInfo, mService.getProxyForNetwork(null));
+ assertEquals(initialProxyInfo, mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+ // Note : strictly speaking, for correctness here apps should expect a broadcast with a
+ // port of -1, since that's the current default proxy. The code does not send this
+ // broadcast. In practice though, not sending it is more useful since the new proxy will
+ // start momentarily, so broadcasting and getting all apps to update and retry once now
+ // and again in 250ms is kind of counter-productive, so don't fix this bug.
+
+ // After a while the PAC file for wifi is resolved again and the local proxy server
+ // starts up. This should cause a LP event to inform clients of the port to access the
+ // proxy server for wifi.
+ mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+ wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+ assertEquals(servingProxyInfo, mService.getProxyForNetwork(null));
+ assertEquals(servingProxyInfo, mService.getProxyForNetwork(mWiFiAgent.getNetwork()));
+ assertNull(mService.getProxyForNetwork(mCellAgent.getNetwork()));
+ servingProxyBroadcast2.expectBroadcast();
+
+ // Expect a broadcast for an empty proxy after wifi disconnected, because cell is now
+ // the default network and it doesn't have a proxy. Whether "no proxy" is a null pointer
+ // or a ProxyInfo with an empty host doesn't matter because both are correct, so this test
+ // accepts both.
+ final ExpectedBroadcast emptyProxyBroadcast = expectProxyChangeAction(
+ proxy -> proxy == null || TextUtils.isEmpty(proxy.getHost()));
+ mWiFiAgent.disconnect();
+ emptyProxyBroadcast.expectBroadcast();
+ wifiCallback.expect(CallbackEntry.LOST, mWiFiAgent);
+ assertNull(mService.getProxyForNetwork(null));
+ assertNull(mService.getLinkProperties(mCellAgent.getNetwork()).getHttpProxy());
+ assertNull(mService.getGlobalProxy());
+
+ mCm.unregisterNetworkCallback(cellCallback);
+ }
+
+ @Test
+ public void testPacProxy_NetworkDisconnects_BroadcastSent() throws Exception {
+ // Make a WiFi network with a PAC URI.
+ final Uri pacUrl = Uri.parse("https://pac-url");
+ final ProxyInfo initialProxyInfo = ProxyInfo.buildPacProxy(pacUrl);
+ final LinkProperties testLinkProperties = new LinkProperties();
+ testLinkProperties.setHttpProxy(initialProxyInfo);
+
+ final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+ final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI).build();
+ mCm.registerNetworkCallback(wifiRequest, wifiCallback);
+
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, testLinkProperties);
+ mWiFiAgent.connect(true);
+ // Wifi is up, but the local proxy server hasn't started yet.
+ wifiCallback.expectAvailableThenValidatedCallbacks(mWiFiAgent);
+
+ // Simulate PacManager sending the notification that the local server has started
+ final ProxyInfo servingProxyInfo = new ProxyInfo(pacUrl, 2097);
+ final ExpectedBroadcast servingProxyBroadcast = expectProxyChangeAction(servingProxyInfo);
+ mService.simulateUpdateProxyInfo(mWiFiAgent.getNetwork(), servingProxyInfo);
+ wifiCallback.expect(CallbackEntry.LINK_PROPERTIES_CHANGED, mWiFiAgent);
+ servingProxyBroadcast.expectBroadcast();
+
+ // Now disconnect Wi-Fi and make sure there is a broadcast for some empty proxy. Whether
+ // the "empty" proxy is a null pointer or a ProxyInfo with an empty host doesn't matter
+ // because both are correct, so this test accepts both.
+ final ExpectedBroadcast emptyProxyBroadcast = expectProxyChangeAction(
+ proxy -> proxy == null || TextUtils.isEmpty(proxy.getHost()));
+ mWiFiAgent.disconnect();
+ wifiCallback.expect(CallbackEntry.LOST, mWiFiAgent);
+ emptyProxyBroadcast.expectBroadcast();
+ }
+
@Test
public void testGetProxyForVPN() throws Exception {
final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
@@ -11156,7 +11683,7 @@
mMockVpn.establish(lp, uid, vpnRange);
assertVpnUidRangesUpdated(true, vpnRange, uid);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
// On T and above, VPN should have rules for null interface. Null Interface is a
// wildcard and this accepts traffic from all the interfaces.
// There are two expected invocations, one during the VPN initial
@@ -12145,7 +12672,7 @@
@Test
public void testUnderlyingNetworksWillBeSetInNetworkAgentInfoConstructor() throws Exception {
- assumeTrue(SdkLevel.isAtLeastT());
+ assumeTrue(mDeps.isAtLeastT());
final Network network1 = new Network(100);
final Network network2 = new Network(101);
final List<Network> underlyingNetworks = new ArrayList<>();
@@ -12471,12 +12998,18 @@
private void assertVpnUidRangesUpdated(boolean add, Set<UidRange> vpnRanges, int exemptUid)
throws Exception {
- InOrder inOrder = inOrder(mMockNetd);
+ InOrder inOrder = inOrder(mMockNetd, mDestroySocketsWrapper);
+ final Set<Integer> exemptUidSet = new ArraySet<>(List.of(exemptUid, Process.VPN_UID));
ArgumentCaptor<int[]> exemptUidCaptor = ArgumentCaptor.forClass(int[].class);
- inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
- exemptUidCaptor.capture());
- assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+ if (mDeps.isAtLeastU()) {
+ inOrder.verify(mDestroySocketsWrapper).destroyLiveTcpSockets(
+ UidRange.toIntRanges(vpnRanges), exemptUidSet);
+ } else {
+ inOrder.verify(mMockNetd).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
+ exemptUidCaptor.capture());
+ assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+ }
if (add) {
inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(
@@ -12488,9 +13021,14 @@
toUidRangeStableParcels(vpnRanges), PREFERENCE_ORDER_VPN));
}
- inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
- exemptUidCaptor.capture());
- assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+ if (mDeps.isAtLeastU()) {
+ inOrder.verify(mDestroySocketsWrapper).destroyLiveTcpSockets(
+ UidRange.toIntRanges(vpnRanges), exemptUidSet);
+ } else {
+ inOrder.verify(mMockNetd).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
+ exemptUidCaptor.capture());
+ assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+ }
}
@Test
@@ -14995,7 +15533,7 @@
UserHandle testHandle,
TestNetworkCallback profileDefaultNetworkCallback,
TestNetworkCallback disAllowProfileDefaultNetworkCallback) throws Exception {
- final InOrder inOrder = inOrder(mMockNetd);
+ final InOrder inOrder = inOrder(mMockNetd, mMockDnsResolver);
mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
mCellAgent.connect(true);
@@ -15011,8 +15549,16 @@
final TestNetworkAgentWrapper workAgent =
makeEnterpriseNetworkAgent(profileNetworkPreference.getPreferenceEnterpriseId());
+ if (mService.shouldCreateNetworksImmediately()) {
+ expectNativeNetworkCreated(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM,
+ null /* iface */, inOrder);
+ }
if (connectWorkProfileAgentAhead) {
workAgent.connect(false);
+ if (!mService.shouldCreateNetworksImmediately()) {
+ expectNativeNetworkCreated(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM,
+ null /* iface */, inOrder);
+ }
}
final TestOnCompleteListener listener = new TestOnCompleteListener();
@@ -15052,6 +15598,11 @@
if (!connectWorkProfileAgentAhead) {
workAgent.connect(false);
+ if (!mService.shouldCreateNetworksImmediately()) {
+ inOrder.verify(mMockNetd).networkCreate(
+ nativeNetworkConfigPhysical(workAgent.getNetwork().netId,
+ INetd.PERMISSION_SYSTEM));
+ }
}
profileDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(workAgent);
@@ -15060,8 +15611,6 @@
}
mSystemDefaultNetworkCallback.assertNoCallback();
mDefaultNetworkCallback.assertNoCallback();
- inOrder.verify(mMockNetd).networkCreate(
- nativeNetworkConfigPhysical(workAgent.getNetwork().netId, INetd.PERMISSION_SYSTEM));
inOrder.verify(mMockNetd).networkAddUidRangesParcel(new NativeUidRangeConfig(
workAgent.getNetwork().netId,
uidRangeFor(testHandle, profileNetworkPreference),
@@ -16029,7 +16578,7 @@
mCellAgent.getNetwork().netId,
toUidRangeStableParcels(allowedRanges),
0 /* subPriority */);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{config1User});
} else {
inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16047,7 +16596,7 @@
mCellAgent.getNetwork().netId,
toUidRangeStableParcels(allowedRanges),
0 /* subPriority */);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{config2Users});
} else {
inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16078,7 +16627,7 @@
mCellAgent.getNetwork().netId,
allowAllUidRangesParcel,
0 /* subPriority */);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(
new NativeUidRangeConfig[]{cellAllAllowedConfig});
} else {
@@ -16097,7 +16646,7 @@
// making the order of the list undeterministic. Thus, verify this in order insensitive way.
final ArgumentCaptor<NativeUidRangeConfig[]> configsCaptor = ArgumentCaptor.forClass(
NativeUidRangeConfig[].class);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
assertContainsAll(List.of(configsCaptor.getValue()),
List.of(cellAllAllowedConfig, enterpriseAllAllowedConfig));
@@ -16131,7 +16680,7 @@
mCellAgent.getNetwork().netId,
excludeAppRangesParcel,
0 /* subPriority */);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
assertContainsAll(List.of(configsCaptor.getValue()),
List.of(cellExcludeAppConfig, enterpriseAllAllowedConfig));
@@ -16143,7 +16692,7 @@
mCm.setProfileNetworkPreference(testHandle, PROFILE_NETWORK_PREFERENCE_ENTERPRISE,
r -> r.run(), listener);
listener.expectOnComplete();
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
assertContainsAll(List.of(configsCaptor.getValue()),
List.of(cellAllAllowedConfig, enterpriseAllAllowedConfig));
@@ -16155,7 +16704,7 @@
// disconnects.
enterpriseAgent.disconnect();
waitForIdle();
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(
new NativeUidRangeConfig[]{cellAllAllowedConfig});
} else {
@@ -16180,7 +16729,7 @@
List.of(prefBuilder.build()),
r -> r.run(), listener);
listener.expectOnComplete();
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{});
} else {
inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16208,7 +16757,7 @@
mCellAgent.getNetwork().netId,
excludeAppRangesParcel,
0 /* subPriority */);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(
new NativeUidRangeConfig[]{cellExcludeAppConfig});
} else {
@@ -16232,7 +16781,7 @@
// making the order of the list undeterministic. Thus, verify this in order insensitive way.
final ArgumentCaptor<NativeUidRangeConfig[]> configsCaptor = ArgumentCaptor.forClass(
NativeUidRangeConfig[].class);
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(configsCaptor.capture());
assertContainsAll(List.of(configsCaptor.getValue()),
List.of(enterpriseAllAllowedConfig, cellExcludeAppConfig));
@@ -16243,7 +16792,7 @@
// Verify issuing with cellular set only when enterprise network disconnects.
enterpriseAgent.disconnect();
waitForIdle();
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(
new NativeUidRangeConfig[]{cellExcludeAppConfig});
} else {
@@ -16252,7 +16801,7 @@
mCellAgent.disconnect();
waitForIdle();
- if (SdkLevel.isAtLeastU()) {
+ if (mDeps.isAtLeastU()) {
inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[]{});
} else {
inOrder.verify(mMockNetd, never()).setNetworkAllowlist(any());
@@ -16308,7 +16857,7 @@
profileNetworkPreferenceBuilder.setPreference(PROFILE_NETWORK_PREFERENCE_ENTERPRISE);
profileNetworkPreferenceBuilder.setPreferenceEnterpriseId(NET_ENTERPRISE_ID_1);
final TestOnCompleteListener listener = new TestOnCompleteListener();
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
mCm.setProfileNetworkPreferences(testHandle,
List.of(profileNetworkPreferenceBuilder.build()),
r -> r.run(), listener);
@@ -16416,7 +16965,7 @@
agent.getNetwork().getNetId(),
intToUidRangeStableParcels(uids),
preferenceOrder);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids200Parcel);
}
@@ -16424,7 +16973,7 @@
uids.add(400);
nc.setAllowedUids(uids);
agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
} else {
cb.assertNoCallback();
@@ -16435,13 +16984,13 @@
agent.getNetwork().getNetId(),
intToUidRangeStableParcels(uids),
preferenceOrder);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids300400Parcel);
}
nc.setAllowedUids(uids);
agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids200Parcel);
} else {
@@ -16452,7 +17001,7 @@
uids.add(600);
nc.setAllowedUids(uids);
agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
cb.expectCaps(agent, c -> c.getAllowedUids().equals(uids));
} else {
cb.assertNoCallback();
@@ -16461,7 +17010,7 @@
agent.getNetwork().getNetId(),
intToUidRangeStableParcels(uids),
preferenceOrder);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(uids600Parcel);
inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids300400Parcel);
}
@@ -16469,7 +17018,7 @@
uids.clear();
nc.setAllowedUids(uids);
agent.setNetworkCapabilities(nc, true /* sendToConnectivityService */);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
cb.expectCaps(agent, c -> c.getAllowedUids().isEmpty());
inOrder.verify(mMockNetd, times(1)).networkRemoveUidRangesParcel(uids600Parcel);
} else {
@@ -16522,7 +17071,7 @@
// Cell gets to set the service UID as access UID
ncb.setAllowedUids(serviceUidSet);
mEthernetAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
- if (SdkLevel.isAtLeastT() && hasAutomotiveFeature) {
+ if (mDeps.isAtLeastT() && hasAutomotiveFeature) {
cb.expectCaps(mEthernetAgent, c -> c.getAllowedUids().equals(serviceUidSet));
} else {
// S and no automotive feature must ignore access UIDs.
@@ -16575,7 +17124,7 @@
cb.expectAvailableThenValidatedCallbacks(mCellAgent);
ncb.setAllowedUids(serviceUidSet);
mCellAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
cb.expectCaps(mCellAgent, c -> c.getAllowedUids().equals(serviceUidSet));
} else {
// S must ignore access UIDs.
@@ -16585,7 +17134,7 @@
// ...but not to some other UID. Rejection sets UIDs to the empty set
ncb.setAllowedUids(nonServiceUidSet);
mCellAgent.setNetworkCapabilities(ncb.build(), true /* sendToCS */);
- if (SdkLevel.isAtLeastT()) {
+ if (mDeps.isAtLeastT()) {
cb.expectCaps(mCellAgent, c -> c.getAllowedUids().isEmpty());
} else {
// S must ignore access UIDs.
@@ -17507,14 +18056,14 @@
@Test
public void testIgnoreValidationAfterRoamEnabled() throws Exception {
- final boolean enabled = !SdkLevel.isAtLeastT();
+ final boolean enabled = !mDeps.isAtLeastT();
doTestIgnoreValidationAfterRoam(5_000, enabled);
}
@Test
public void testShouldIgnoreValidationFailureAfterRoam() {
// Always disabled on T+.
- assumeFalse(SdkLevel.isAtLeastT());
+ assumeFalse(mDeps.isAtLeastT());
NetworkAgentInfo nai = fakeWifiNai(new NetworkCapabilities());
@@ -17572,18 +18121,93 @@
});
}
+ private void verifyMtuSetOnWifiInterface(int mtu) throws Exception {
+ verify(mMockNetd, times(1)).interfaceSetMtu(WIFI_IFNAME, mtu);
+ }
+
+ private void verifyMtuNeverSetOnWifiInterface() throws Exception {
+ verify(mMockNetd, never()).interfaceSetMtu(eq(WIFI_IFNAME), anyInt());
+ }
+
+ private void verifyMtuSetOnWifiInterfaceOnlyUpToT(int mtu) throws Exception {
+ if (!mService.shouldCreateNetworksImmediately()) {
+ verify(mMockNetd, times(1)).interfaceSetMtu(WIFI_IFNAME, mtu);
+ } else {
+ verify(mMockNetd, never()).interfaceSetMtu(eq(WIFI_IFNAME), anyInt());
+ }
+ }
+
+ private void verifyMtuSetOnWifiInterfaceOnlyStartingFromU(int mtu) throws Exception {
+ if (mService.shouldCreateNetworksImmediately()) {
+ verify(mMockNetd, times(1)).interfaceSetMtu(WIFI_IFNAME, mtu);
+ } else {
+ verify(mMockNetd, never()).interfaceSetMtu(eq(WIFI_IFNAME), anyInt());
+ }
+ }
+
@Test
- public void testSendLinkPropertiesSetInterfaceMtu() throws Exception {
- final int mtu = 1327;
+ public void testSendLinkPropertiesSetInterfaceMtuBeforeConnect() throws Exception {
+ final int mtu = 1281;
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(WIFI_IFNAME);
lp.setMtu(mtu);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
mWiFiAgent.sendLinkProperties(lp);
-
waitForIdle();
- verify(mMockNetd).interfaceSetMtu(eq(WIFI_IFNAME), eq(mtu));
+ verifyMtuSetOnWifiInterface(mtu);
+ reset(mMockNetd);
+
+ mWiFiAgent.connect(false /* validated */);
+ // Before U, the MTU is always (re-)applied when the network connects.
+ verifyMtuSetOnWifiInterfaceOnlyUpToT(mtu);
+ }
+
+ @Test
+ public void testSendLinkPropertiesUpdateInterfaceMtuBeforeConnect() throws Exception {
+ final int mtu = 1327;
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(WIFI_IFNAME);
+ lp.setMtu(mtu);
+
+ // Registering an agent with an MTU only sets the MTU on U+.
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+ waitForIdle();
+ verifyMtuSetOnWifiInterfaceOnlyStartingFromU(mtu);
+ reset(mMockNetd);
+
+ // Future updates with the same MTU don't set the MTU even on T when it's not set initially.
+ mWiFiAgent.sendLinkProperties(lp);
+ waitForIdle();
+ verifyMtuNeverSetOnWifiInterface();
+
+ // Updating with a different MTU does work.
+ lp.setMtu(mtu + 1);
+ mWiFiAgent.sendLinkProperties(lp);
+ waitForIdle();
+ verifyMtuSetOnWifiInterface(mtu + 1);
+ reset(mMockNetd);
+
+ mWiFiAgent.connect(false /* validated */);
+ // Before U, the MTU is always (re-)applied when the network connects.
+ verifyMtuSetOnWifiInterfaceOnlyUpToT(mtu + 1);
+ }
+
+ @Test
+ public void testSendLinkPropertiesUpdateInterfaceMtuAfterConnect() throws Exception {
+ final int mtu = 1327;
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(WIFI_IFNAME);
+ lp.setMtu(mtu);
+
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+ mWiFiAgent.connect(false /* validated */);
+ verifyMtuNeverSetOnWifiInterface();
+
+ mWiFiAgent.sendLinkProperties(lp);
+ waitForIdle();
+ // The MTU is always (re-)applied when the network connects.
+ verifyMtuSetOnWifiInterface(mtu);
}
@Test
@@ -17594,14 +18218,15 @@
lp.setMtu(mtu);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+ mWiFiAgent.connect(false /* validated */);
+ verifyMtuSetOnWifiInterface(mtu);
+ reset(mMockNetd);
LinkProperties lp2 = new LinkProperties(lp);
lp2.setMtu(mtu2);
-
mWiFiAgent.sendLinkProperties(lp2);
-
waitForIdle();
- verify(mMockNetd).interfaceSetMtu(eq(WIFI_IFNAME), eq(mtu2));
+ verifyMtuSetOnWifiInterface(mtu2);
}
@Test
@@ -17612,10 +18237,13 @@
lp.setMtu(mtu);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
- mWiFiAgent.sendLinkProperties(new LinkProperties(lp));
+ mWiFiAgent.connect(false /* validated */);
+ verifyMtuSetOnWifiInterface(mtu);
+ reset(mMockNetd);
+ mWiFiAgent.sendLinkProperties(new LinkProperties(lp));
waitForIdle();
- verify(mMockNetd, never()).interfaceSetMtu(eq(WIFI_IFNAME), anyInt());
+ verifyMtuNeverSetOnWifiInterface();
}
@Test
@@ -17626,15 +18254,15 @@
lp.setMtu(mtu);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+ mWiFiAgent.connect(false /* validated */);
+ verifyMtuSetOnWifiInterface(mtu);
+ reset(mMockNetd);
- LinkProperties lp2 = new LinkProperties();
- assertNull(lp2.getInterfaceName());
- lp2.setMtu(mtu);
-
+ LinkProperties lp2 = new LinkProperties(lp);
+ lp2.setInterfaceName(null);
mWiFiAgent.sendLinkProperties(new LinkProperties(lp2));
-
waitForIdle();
- verify(mMockNetd, never()).interfaceSetMtu(any(), anyInt());
+ verifyMtuNeverSetOnWifiInterface();
}
@Test
@@ -17645,16 +18273,18 @@
lp.setMtu(mtu);
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+ mWiFiAgent.connect(false /* validated */);
+ verifyMtuSetOnWifiInterface(mtu);
+ reset(mMockNetd);
final String ifaceName2 = WIFI_IFNAME + "_2";
- LinkProperties lp2 = new LinkProperties();
+ LinkProperties lp2 = new LinkProperties(lp);
lp2.setInterfaceName(ifaceName2);
- lp2.setMtu(mtu);
mWiFiAgent.sendLinkProperties(new LinkProperties(lp2));
-
waitForIdle();
- verify(mMockNetd).interfaceSetMtu(eq(ifaceName2), eq(mtu));
+ verify(mMockNetd, times(1)).interfaceSetMtu(eq(ifaceName2), eq(mtu));
+ verifyMtuNeverSetOnWifiInterface();
}
@Test
@@ -17666,4 +18296,108 @@
info.setExtraInfo("test_info");
assertEquals("0;2;test_info", createDeliveryGroupKeyForConnectivityAction(info));
}
+
+ @Test
+ public void testNetdWakeupAddInterfaceForWifiTransport() throws Exception {
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiAgent.connect(false /* validated */);
+
+ final String expectedPrefix = makeNflogPrefix(WIFI_IFNAME,
+ mWiFiAgent.getNetwork().getNetworkHandle());
+ verify(mMockNetd).wakeupAddInterface(WIFI_IFNAME, expectedPrefix, PACKET_WAKEUP_MARK_MASK,
+ PACKET_WAKEUP_MARK_MASK);
+ }
+
+ @Test
+ public void testNetdWakeupAddInterfaceForCellularTransport() throws Exception {
+ final LinkProperties cellLp = new LinkProperties();
+ cellLp.setInterfaceName(MOBILE_IFNAME);
+ mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+ mCellAgent.connect(false /* validated */);
+
+ if (mDeps.isAtLeastU()) {
+ final String expectedPrefix = makeNflogPrefix(MOBILE_IFNAME,
+ mCellAgent.getNetwork().getNetworkHandle());
+ verify(mMockNetd).wakeupAddInterface(MOBILE_IFNAME, expectedPrefix,
+ PACKET_WAKEUP_MARK_MASK, PACKET_WAKEUP_MARK_MASK);
+ } else {
+ verify(mMockNetd, never()).wakeupAddInterface(eq(MOBILE_IFNAME), anyString(), anyInt(),
+ anyInt());
+ }
+ }
+
+ @Test
+ public void testNetdWakeupAddInterfaceForEthernetTransport() throws Exception {
+ final String ethernetIface = "eth42";
+
+ final LinkProperties ethLp = new LinkProperties();
+ ethLp.setInterfaceName(ethernetIface);
+ mEthernetAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET, ethLp);
+ mEthernetAgent.connect(false /* validated */);
+
+ verify(mMockNetd, never()).wakeupAddInterface(eq(ethernetIface), anyString(), anyInt(),
+ anyInt());
+ }
+
+ private static final int TEST_FROZEN_UID = 1000;
+ private static final int TEST_UNFROZEN_UID = 2000;
+
+ /**
+ * Send a UidFrozenStateChanged message to ConnectivityService. Verify that only the frozen UID
+ * gets passed to socketDestroy().
+ */
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testFrozenUidSocketDestroy() throws Exception {
+ ArgumentCaptor<UidFrozenStateChangedCallback> callbackArg =
+ ArgumentCaptor.forClass(UidFrozenStateChangedCallback.class);
+
+ verify(mActivityManager).registerUidFrozenStateChangedCallback(any(),
+ callbackArg.capture());
+
+ final int[] uids = {TEST_FROZEN_UID, TEST_UNFROZEN_UID};
+ final int[] frozenStates = {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN};
+
+ callbackArg.getValue().onUidFrozenStateChanged(uids, frozenStates);
+
+ waitForIdle();
+
+ final Set<Integer> exemptUids = new ArraySet();
+ final UidRange frozenUidRange = new UidRange(TEST_FROZEN_UID, TEST_FROZEN_UID);
+ final Set<UidRange> ranges = Collections.singleton(frozenUidRange);
+
+ verify(mDestroySocketsWrapper).destroyLiveTcpSockets(eq(UidRange.toIntRanges(ranges)),
+ eq(exemptUids));
+ }
+
+ @Test
+ public void testDisconnectSuspendedNetworkStopClatd() throws Exception {
+ final TestNetworkCallback networkCallback = new TestNetworkCallback();
+ final NetworkRequest networkRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_DUN)
+ .build();
+ mCm.requestNetwork(networkRequest, networkCallback);
+
+ final IpPrefix nat64Prefix = new IpPrefix(InetAddress.getByName("64:ff9b::"), 96);
+ NetworkCapabilities nc = new NetworkCapabilities().addCapability(NET_CAPABILITY_DUN);
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(MOBILE_IFNAME);
+ lp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
+ lp.setNat64Prefix(nat64Prefix);
+ mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, lp, nc);
+ mCellAgent.connect(true /* validated */, false /* hasInternet */,
+ false /* privateDnsProbeSent */);
+
+ verifyClatdStart(null /* inOrder */, MOBILE_IFNAME, mCellAgent.getNetwork().netId,
+ nat64Prefix.toString());
+
+ mCellAgent.suspend();
+ mCm.unregisterNetworkCallback(networkCallback);
+ mCellAgent.expectDisconnected();
+ waitForIdle();
+
+ verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
+ }
}
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 2ed989e..1997215 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -23,7 +23,7 @@
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
-import static com.android.server.NsdService.constructServiceType;
+import static com.android.server.NsdService.parseTypeAndSubtype;
import static com.android.testutils.ContextUtils.mockService;
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
@@ -77,6 +77,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
@@ -84,6 +85,7 @@
import com.android.server.NsdService.Dependencies;
import com.android.server.connectivity.mdns.MdnsAdvertiser;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
+import com.android.server.connectivity.mdns.MdnsSearchOptions;
import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
import com.android.server.connectivity.mdns.MdnsServiceInfo;
import com.android.server.connectivity.mdns.MdnsSocketProvider;
@@ -104,6 +106,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
@@ -175,10 +178,10 @@
doReturn(true).when(mMockMDnsM).resolve(
anyInt(), anyString(), anyString(), anyString(), anyInt());
doReturn(false).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
- doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any());
- doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any());
- doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any());
-
+ doReturn(mDiscoveryManager).when(mDeps)
+ .makeMdnsDiscoveryManager(any(), any(), any());
+ doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any(), any());
+ doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any(), any());
mService = makeService();
}
@@ -908,7 +911,8 @@
listener.onServiceNameDiscovered(foundInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(argThat(info ->
info.getServiceName().equals(SERVICE_NAME)
- && info.getServiceType().equals(SERVICE_TYPE)
+ // Service type in discovery callbacks has a dot at the end
+ && info.getServiceType().equals(SERVICE_TYPE + ".")
&& info.getNetwork().equals(network)));
final MdnsServiceInfo removedInfo = new MdnsServiceInfo(
@@ -927,7 +931,8 @@
listener.onServiceNameRemoved(removedInfo);
verify(discListener, timeout(TIMEOUT_MS)).onServiceLost(argThat(info ->
info.getServiceName().equals(SERVICE_NAME)
- && info.getServiceType().equals(SERVICE_TYPE)
+ // Service type in discovery callbacks has a dot at the end
+ && info.getServiceType().equals(SERVICE_TYPE + ".")
&& info.getNetwork().equals(network)));
client.stopServiceDiscovery(discListener);
@@ -967,6 +972,34 @@
}
@Test
+ @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ public void testDiscoveryWithMdnsDiscoveryManager_UsesSubtypes() {
+ final String typeWithSubtype = SERVICE_TYPE + ",_subtype";
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo regInfo = new NsdServiceInfo("Instance", typeWithSubtype);
+ final Network network = new Network(999);
+ regInfo.setHostAddresses(List.of(parseNumericAddress("192.0.2.123")));
+ regInfo.setPort(12345);
+ regInfo.setNetwork(network);
+
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+ waitForIdle();
+ verify(mAdvertiser).addService(anyInt(), argThat(s ->
+ "Instance".equals(s.getServiceName())
+ && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"));
+
+ final DiscoveryListener discListener = mock(DiscoveryListener.class);
+ client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
+ waitForIdle();
+ final ArgumentCaptor<MdnsSearchOptions> optionsCaptor =
+ ArgumentCaptor.forClass(MdnsSearchOptions.class);
+ verify(mDiscoveryManager).registerListener(eq(SERVICE_TYPE + ".local"), any(),
+ optionsCaptor.capture());
+ assertEquals(Collections.singletonList("subtype"), optionsCaptor.getValue().getSubtypes());
+ }
+
+ @Test
public void testResolutionWithMdnsDiscoveryManager() throws UnknownHostException {
setMdnsDiscoveryManagerEnabled();
@@ -974,7 +1007,7 @@
final ResolveListener resolveListener = mock(ResolveListener.class);
final Network network = new Network(999);
final String serviceType = "_nsd._service._tcp";
- final String constructedServiceType = "_nsd._sub._service._tcp.local";
+ final String constructedServiceType = "_service._tcp.local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
@@ -982,10 +1015,14 @@
client.resolveService(request, resolveListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
+ final ArgumentCaptor<MdnsSearchOptions> optionsCaptor =
+ ArgumentCaptor.forClass(MdnsSearchOptions.class);
verify(mDiscoveryManager).registerListener(eq(constructedServiceType),
- listenerCaptor.capture(), argThat(options ->
- network.equals(options.getNetwork())
- && SERVICE_NAME.equals(options.getResolveInstanceName())));
+ listenerCaptor.capture(),
+ optionsCaptor.capture());
+ assertEquals(network, optionsCaptor.getValue().getNetwork());
+ // Subtypes are not used for resolution, only for discovery
+ assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
final MdnsServiceBrowserListener listener = listenerCaptor.getValue();
final MdnsServiceInfo mdnsServiceInfo = new MdnsServiceInfo(
@@ -1009,7 +1046,7 @@
verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(infoCaptor.capture());
final NsdServiceInfo info = infoCaptor.getValue();
assertEquals(SERVICE_NAME, info.getServiceName());
- assertEquals("." + serviceType, info.getServiceType());
+ assertEquals("._service._tcp", info.getServiceType());
assertEquals(PORT, info.getPort());
assertTrue(info.getAttributes().containsKey("key"));
assertEquals(1, info.getAttributes().size());
@@ -1052,7 +1089,7 @@
final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addService(serviceIdCaptor.capture(),
- argThat(info -> matches(info, regInfo)));
+ argThat(info -> matches(info, regInfo)), eq(null) /* subtype */);
client.unregisterService(regListenerWithoutFeature);
waitForIdle();
@@ -1109,8 +1146,10 @@
waitForIdle();
// The advertiser is enabled for _type2 but not _type1
- verify(mAdvertiser, never()).addService(anyInt(), argThat(info -> matches(info, service1)));
- verify(mAdvertiser).addService(anyInt(), argThat(info -> matches(info, service2)));
+ verify(mAdvertiser, never()).addService(
+ anyInt(), argThat(info -> matches(info, service1)), eq(null) /* subtype */);
+ verify(mAdvertiser).addService(
+ anyInt(), argThat(info -> matches(info, service2)), eq(null) /* subtype */);
}
@Test
@@ -1122,7 +1161,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1135,7 +1174,7 @@
verify(mSocketProvider).startMonitoringSockets();
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
- matches(info, regInfo)));
+ matches(info, regInfo)), eq(null) /* subtype */);
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1161,7 +1200,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1171,7 +1210,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
- verify(mAdvertiser, never()).addService(anyInt(), any());
+ verify(mAdvertiser, never()).addService(anyInt(), any(), any());
verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1186,7 +1225,7 @@
// final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
- verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture(), any());
final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
regInfo.setHost(parseNumericAddress("192.0.2.123"));
@@ -1199,7 +1238,8 @@
final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
// Service name is truncated to 63 characters
verify(mAdvertiser).addService(idCaptor.capture(),
- argThat(info -> info.getServiceName().equals("a".repeat(63))));
+ argThat(info -> info.getServiceName().equals("a".repeat(63))),
+ eq(null) /* subtype */);
// Verify onServiceRegistered callback
final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1217,7 +1257,7 @@
final ResolveListener resolveListener = mock(ResolveListener.class);
final Network network = new Network(999);
final String serviceType = "_nsd._service._tcp";
- final String constructedServiceType = "_nsd._sub._service._tcp.local";
+ final String constructedServiceType = "_service._tcp.local";
final ArgumentCaptor<MdnsServiceBrowserListener> listenerCaptor =
ArgumentCaptor.forClass(MdnsServiceBrowserListener.class);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, serviceType);
@@ -1225,8 +1265,14 @@
client.resolveService(request, resolveListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
+ final ArgumentCaptor<MdnsSearchOptions> optionsCaptor =
+ ArgumentCaptor.forClass(MdnsSearchOptions.class);
verify(mDiscoveryManager).registerListener(eq(constructedServiceType),
- listenerCaptor.capture(), argThat(options -> network.equals(options.getNetwork())));
+ listenerCaptor.capture(),
+ optionsCaptor.capture());
+ assertEquals(network, optionsCaptor.getValue().getNetwork());
+ // Subtypes are not used for resolution, only for discovery
+ assertEquals(Collections.emptyList(), optionsCaptor.getValue().getSubtypes());
client.stopServiceResolution(resolveListener);
waitForIdle();
@@ -1241,16 +1287,22 @@
}
@Test
- public void testConstructServiceType() {
+ public void testParseTypeAndSubtype() {
final String serviceType1 = "test._tcp";
final String serviceType2 = "_test._quic";
- final String serviceType3 = "_123._udp.";
- final String serviceType4 = "_TEST._999._tcp.";
+ final String serviceType3 = "_test._quic,_test1,_test2";
+ final String serviceType4 = "_123._udp.";
+ final String serviceType5 = "_TEST._999._tcp.";
+ final String serviceType6 = "_998._tcp.,_TEST";
+ final String serviceType7 = "_997._tcp,_TEST";
- assertEquals(null, constructServiceType(serviceType1));
- assertEquals(null, constructServiceType(serviceType2));
- assertEquals("_123._udp", constructServiceType(serviceType3));
- assertEquals("_TEST._sub._999._tcp", constructServiceType(serviceType4));
+ assertNull(parseTypeAndSubtype(serviceType1));
+ assertNull(parseTypeAndSubtype(serviceType2));
+ assertNull(parseTypeAndSubtype(serviceType3));
+ assertEquals(new Pair<>("_123._udp", null), parseTypeAndSubtype(serviceType4));
+ assertEquals(new Pair<>("_999._tcp", "_TEST"), parseTypeAndSubtype(serviceType5));
+ assertEquals(new Pair<>("_998._tcp", "_TEST"), parseTypeAndSubtype(serviceType6));
+ assertEquals(new Pair<>("_997._tcp", "_TEST"), parseTypeAndSubtype(serviceType7));
}
@Test
@@ -1269,7 +1321,7 @@
client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
waitForIdle();
verify(mSocketProvider).startMonitoringSockets();
- verify(mAdvertiser).addService(anyInt(), any());
+ verify(mAdvertiser).addService(anyInt(), any(), any());
// Verify the discovery uses MdnsDiscoveryManager
final DiscoveryListener discListener = mock(DiscoveryListener.class);
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
index deb56ef..bf23cd1 100644
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -75,12 +75,15 @@
@IgnoreUpTo(R) // VpnManagerService is not available before R
@SmallTest
public class VpnManagerServiceTest extends VpnTestBase {
+ private static final String CONTEXT_ATTRIBUTION_TAG = "VPN_MANAGER";
+
@Rule
public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
private static final int TIMEOUT_MS = 2_000;
@Mock Context mContext;
+ @Mock Context mContextWithoutAttributionTag;
@Mock Context mSystemContext;
@Mock Context mUserAllContext;
private HandlerThread mHandlerThread;
@@ -144,6 +147,13 @@
mHandlerThread = new HandlerThread("TestVpnManagerService");
mDeps = new VpnManagerServiceDependencies();
+
+ // The attribution tag is a dependency for IKE library to collect VPN metrics correctly
+ // and thus should not be changed without updating the IKE code.
+ doReturn(mContext)
+ .when(mContextWithoutAttributionTag)
+ .createAttributionContext(CONTEXT_ATTRIBUTION_TAG);
+
doReturn(mUserAllContext).when(mContext).createContextAsUser(UserHandle.ALL, 0);
doReturn(mSystemContext).when(mContext).createContextAsUser(UserHandle.SYSTEM, 0);
doReturn(mPackageManager).when(mContext).getPackageManager();
@@ -153,7 +163,7 @@
mockService(mContext, UserManager.class, Context.USER_SERVICE, mUserManager);
doReturn(SYSTEM_USER).when(mUserManager).getUserInfo(eq(SYSTEM_USER_ID));
- mService = new VpnManagerService(mContext, mDeps);
+ mService = new VpnManagerService(mContextWithoutAttributionTag, mDeps);
mService.systemReady();
final ArgumentCaptor<BroadcastReceiver> intentReceiverCaptor =
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 3eb1b26..9e0435d 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -38,7 +38,6 @@
import android.app.AlarmManager;
import android.content.Context;
import android.content.res.Resources;
-import android.net.ConnectivityResources;
import android.net.INetd;
import android.net.ISocketKeepaliveCallback;
import android.net.KeepalivePacketData;
@@ -63,6 +62,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.connectivity.resources.R;
import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -235,10 +235,8 @@
anyInt() /* pid */, anyInt() /* uid */);
ConnectivityResources.setResourcesContextForTest(mCtx);
final Resources mockResources = mock(Resources.class);
- doReturn(MOCK_RESOURCE_ID).when(mockResources).getIdentifier(any() /* name */,
- any() /* defType */, any() /* defPackage */);
doReturn(new String[] { "0,3", "3,3" }).when(mockResources)
- .getStringArray(MOCK_RESOURCE_ID);
+ .getStringArray(R.array.config_networkSupportedKeepaliveCount);
doReturn(mockResources).when(mCtx).getResources();
doReturn(mNetd).when(mDependencies).getNetd();
doReturn(mAlarmManager).when(mDependencies).getAlarmManager(any());
diff --git a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
index 0d371fa..e6c0c83 100644
--- a/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -34,7 +34,6 @@
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
-import android.net.ConnectivityResources;
import android.net.IDnsResolver;
import android.net.INetd;
import android.net.LinkProperties;
diff --git a/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt b/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt
index b52e8a8..f19ba4f 100644
--- a/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt
@@ -21,10 +21,8 @@
import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER
import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_PERFORMANCE
import android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY
-import android.net.ConnectivityResources
import android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI
import android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE
-import com.android.server.connectivity.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener
import android.os.Build
import android.os.Handler
import android.os.test.TestLooper
@@ -37,6 +35,7 @@
import com.android.connectivity.resources.R
import com.android.internal.util.test.FakeSettingsProvider
import com.android.modules.utils.build.SdkLevel
+import com.android.server.connectivity.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import org.junit.After
diff --git a/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTestDependencies.kt b/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTestDependencies.kt
index 744c020..4c82c76 100644
--- a/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTestDependencies.kt
+++ b/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTestDependencies.kt
@@ -1,7 +1,6 @@
package com.android.server.connectivity
import android.content.res.Resources
-import android.net.ConnectivityResources
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import android.provider.DeviceConfig.OnPropertiesChangedListener
diff --git a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
index f4b6464..d667662 100644
--- a/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetdEventListenerServiceTest.java
@@ -24,6 +24,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -41,6 +42,8 @@
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import libcore.util.EmptyArray;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -60,7 +63,8 @@
private static final String EXAMPLE_IPV4 = "192.0.2.1";
private static final String EXAMPLE_IPV6 = "2001:db8:1200::2:1";
- private static final long NET_HANDLE = new Network(5391).getNetworkHandle();
+ private static final Network TEST_WIFI_NETWORK = new Network(5391);
+ private static final Network TEST_CELL_NETWORK = new Network(5832);
private static final byte[] MAC_ADDR =
{(byte)0x84, (byte)0xc9, (byte)0xb2, (byte)0x6a, (byte)0xed, (byte)0x4b};
@@ -78,6 +82,8 @@
public void setUp() {
mCm = mock(ConnectivityManager.class);
mService = new NetdEventListenerService(mCm);
+ doReturn(CAPABILITIES_WIFI).when(mCm).getNetworkCapabilities(TEST_WIFI_NETWORK);
+ doReturn(CAPABILITIES_CELL).when(mCm).getNetworkCapabilities(TEST_CELL_NETWORK);
}
@Test
@@ -111,19 +117,25 @@
wakeupEvent(iface, uids[5], v4, tcp, mac, srcIp, dstIp, sport, dport, now);
wakeupEvent(iface, uids[6], v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
wakeupEvent(iface, uids[7], v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
- wakeupEvent(iface, uids[8], v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
+ wakeupEvent("rmnet0", uids[8], v6, udp, EmptyArray.BYTE, srcIp6, dstIp6, sport, dport, now,
+ TEST_CELL_NETWORK);
String[] events2 = remove(listNetdEvent(), baseline);
- int expectedLength2 = uids.length + 1; // +1 for the WakeupStats line
+ int expectedLength2 = uids.length + 2; // +2 for the WakeupStats headers
assertEquals(expectedLength2, events2.length);
+
assertStringContains(events2[0], "WakeupStats");
- assertStringContains(events2[0], "wlan0");
- assertStringContains(events2[0], "0x800");
+ assertStringContains(events2[0], "rmnet0");
assertStringContains(events2[0], "0x86dd");
+
+ assertStringContains(events2[1], "WakeupStats");
+ assertStringContains(events2[1], "wlan0");
+ assertStringContains(events2[1], "0x800");
+ assertStringContains(events2[1], "0x86dd");
for (int i = 0; i < uids.length; i++) {
- String got = events2[i+1];
+ String got = events2[i + 2];
assertStringContains(got, "WakeupEvent");
- assertStringContains(got, "wlan0");
+ assertStringContains(got, ((i == 8) ? "rmnet0" : "wlan0"));
assertStringContains(got, "uid: " + uids[i]);
}
@@ -134,11 +146,13 @@
}
String[] events3 = remove(listNetdEvent(), baseline);
- int expectedLength3 = BUFFER_LENGTH + 1; // +1 for the WakeupStats line
+ int expectedLength3 = BUFFER_LENGTH + 2; // +2 for the WakeupStats headers
assertEquals(expectedLength3, events3.length);
- assertStringContains(events2[0], "WakeupStats");
- assertStringContains(events2[0], "wlan0");
- for (int i = 1; i < expectedLength3; i++) {
+ assertStringContains(events3[0], "WakeupStats");
+ assertStringContains(events3[0], "rmnet0");
+ assertStringContains(events3[1], "WakeupStats");
+ assertStringContains(events3[1], "wlan0");
+ for (int i = 2; i < expectedLength3; i++) {
String got = events3[i];
assertStringContains(got, "WakeupEvent");
assertStringContains(got, "wlan0");
@@ -173,19 +187,24 @@
final int icmp6 = 58;
wakeupEvent("wlan0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
- wakeupEvent("rmnet0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+ wakeupEvent("rmnet0", 10123, v4, tcp, mac, srcIp, dstIp, sport, dport, now,
+ TEST_CELL_NETWORK);
wakeupEvent("wlan0", 1000, v4, udp, mac, srcIp, dstIp, sport, dport, now);
- wakeupEvent("rmnet0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+ wakeupEvent("rmnet0", 10008, v4, tcp, EmptyArray.BYTE, srcIp, dstIp, sport, dport, now,
+ TEST_CELL_NETWORK);
wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now);
wakeupEvent("wlan0", 10008, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
- wakeupEvent("rmnet0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+ wakeupEvent("rmnet0", 1000, v4, tcp, mac, srcIp, dstIp, sport, dport, now,
+ TEST_CELL_NETWORK);
wakeupEvent("wlan0", 10004, v4, udp, mac, srcIp, dstIp, sport, dport, now);
wakeupEvent("wlan0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
wakeupEvent("wlan0", -1, v6, icmp6, mac, srcIp6, dstIp6, sport, dport, now);
- wakeupEvent("rmnet0", 10052, v4, tcp, mac, srcIp, dstIp, sport, dport, now);
+ wakeupEvent("rmnet0", 10052, v4, tcp, mac, srcIp, dstIp, sport, dport, now,
+ TEST_CELL_NETWORK);
wakeupEvent("wlan0", 0, v6, udp, mac, srcIp6, dstIp6, sport, dport, now);
- wakeupEvent("rmnet0", 1000, v6, tcp, mac, srcIp6, dstIp6, sport, dport, now);
+ wakeupEvent("rmnet0", 1000, v6, tcp, null, srcIp6, dstIp6, sport, dport, now,
+ TEST_CELL_NETWORK);
wakeupEvent("wlan0", 1010, v4, udp, mac, srcIp, dstIp, sport, dport, now);
String got = flushStatistics();
@@ -214,7 +233,7 @@
" >",
" l2_broadcast_count: 0",
" l2_multicast_count: 0",
- " l2_unicast_count: 5",
+ " l2_unicast_count: 3",
" no_uid_wakeups: 0",
" non_application_wakeups: 0",
" root_wakeups: 0",
@@ -499,8 +518,13 @@
}
void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp,
- String dstIp, int sport, int dport, long now) throws Exception {
- String prefix = NET_HANDLE + ":" + iface;
+ String dstIp, int sport, int dport, long now) {
+ wakeupEvent(iface, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now, TEST_WIFI_NETWORK);
+ }
+
+ void wakeupEvent(String iface, int uid, int ether, int ip, byte[] mac, String srcIp,
+ String dstIp, int sport, int dport, long now, Network network) {
+ String prefix = network.getNetworkHandle() + ":" + iface;
mService.onWakeupEvent(prefix, uid, ether, ip, mac, srcIp, dstIp, sport, dport, now);
}
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index e038c44..a27a0bf 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -51,7 +51,6 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
-import android.net.ConnectivityResources;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 2926c9a..f7b9fcf 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -25,6 +25,9 @@
import static android.net.ConnectivityManager.NetworkCallback;
import static android.net.INetd.IF_STATE_DOWN;
import static android.net.INetd.IF_STATE_UP;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.RouteInfo.RTN_UNREACHABLE;
@@ -142,6 +145,7 @@
import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
import android.net.ipsec.ike.exceptions.IkeTimeoutException;
+import android.net.vcn.VcnTransportInfo;
import android.net.wifi.WifiInfo;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
@@ -172,6 +176,7 @@
import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.server.DeviceIdleInternal;
import com.android.server.IpSecService;
import com.android.server.VpnTestBase;
@@ -195,6 +200,7 @@
import java.io.FileDescriptor;
import java.io.FileWriter;
import java.io.IOException;
+import java.io.StringWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -212,6 +218,8 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
@@ -1563,6 +1571,11 @@
}
private NetworkCallback triggerOnAvailableAndGetCallback() throws Exception {
+ return triggerOnAvailableAndGetCallback(new NetworkCapabilities.Builder().build());
+ }
+
+ private NetworkCallback triggerOnAvailableAndGetCallback(
+ @NonNull final NetworkCapabilities caps) throws Exception {
final ArgumentCaptor<NetworkCallback> networkCallbackCaptor =
ArgumentCaptor.forClass(NetworkCallback.class);
verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
@@ -1579,7 +1592,7 @@
// if NetworkCapabilities and LinkProperties of underlying network will be sent/cleared or
// not.
// See verifyVpnManagerEvent().
- cb.onCapabilitiesChanged(TEST_NETWORK, new NetworkCapabilities());
+ cb.onCapabilitiesChanged(TEST_NETWORK, caps);
cb.onLinkPropertiesChanged(TEST_NETWORK, new LinkProperties());
return cb;
}
@@ -1903,12 +1916,15 @@
private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6) throws Exception {
- return verifySetupPlatformVpn(vpnProfile, ikeConfig, mtuSupportsIpv6,
- false /* areLongLivedTcpConnectionsExpensive */);
+ return verifySetupPlatformVpn(vpnProfile, ikeConfig,
+ new NetworkCapabilities.Builder().build() /* underlying network caps */,
+ mtuSupportsIpv6, false /* areLongLivedTcpConnectionsExpensive */);
}
private PlatformVpnSnapshot verifySetupPlatformVpn(VpnProfile vpnProfile,
- IkeSessionConfiguration ikeConfig, boolean mtuSupportsIpv6,
+ IkeSessionConfiguration ikeConfig,
+ @NonNull final NetworkCapabilities underlyingNetworkCaps,
+ boolean mtuSupportsIpv6,
boolean areLongLivedTcpConnectionsExpensive) throws Exception {
if (!mtuSupportsIpv6) {
doReturn(IPV6_MIN_MTU - 1).when(mTestDeps).calculateVpnMtu(any(), anyInt(), anyInt(),
@@ -1925,7 +1941,7 @@
.thenReturn(vpnProfile.encode());
vpn.startVpnProfile(TEST_VPN_PKG);
- final NetworkCallback nwCb = triggerOnAvailableAndGetCallback();
+ final NetworkCallback nwCb = triggerOnAvailableAndGetCallback(underlyingNetworkCaps);
verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), anyLong(), any());
reset(mExecutor);
@@ -2079,15 +2095,16 @@
doTestMigrateIkeSession(ikeProfile.toVpnProfile(),
expectedKeepalive,
ESP_IP_VERSION_AUTO /* expectedIpVersion */,
- ESP_ENCAP_TYPE_AUTO /* expectedEncapType */);
+ ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
+ new NetworkCapabilities.Builder().build());
}
- private void doTestMigrateIkeSession_FromIkeTunnConnParams(
+ private Ikev2VpnProfile makeIkeV2VpnProfile(
boolean isAutomaticIpVersionSelectionEnabled,
boolean isAutomaticNattKeepaliveTimerEnabled,
int keepaliveInProfile,
int ipVersionInProfile,
- int encapTypeInProfile) throws Exception {
+ int encapTypeInProfile) {
// TODO: Update helper function in IkeSessionTestUtils to support building IkeSessionParams
// with IP version and encap type when mainline-prod branch support these two APIs.
final IkeSessionParams params = getTestIkeSessionParams(true /* testIpv6 */,
@@ -2099,12 +2116,40 @@
final IkeTunnelConnectionParams tunnelParams =
new IkeTunnelConnectionParams(ikeSessionParams, CHILD_PARAMS);
- final Ikev2VpnProfile ikeProfile = new Ikev2VpnProfile.Builder(tunnelParams)
+ return new Ikev2VpnProfile.Builder(tunnelParams)
.setBypassable(true)
.setAutomaticNattKeepaliveTimerEnabled(isAutomaticNattKeepaliveTimerEnabled)
.setAutomaticIpVersionSelectionEnabled(isAutomaticIpVersionSelectionEnabled)
.build();
+ }
+ private void doTestMigrateIkeSession_FromIkeTunnConnParams(
+ boolean isAutomaticIpVersionSelectionEnabled,
+ boolean isAutomaticNattKeepaliveTimerEnabled,
+ int keepaliveInProfile,
+ int ipVersionInProfile,
+ int encapTypeInProfile) throws Exception {
+ doTestMigrateIkeSession_FromIkeTunnConnParams(isAutomaticIpVersionSelectionEnabled,
+ isAutomaticNattKeepaliveTimerEnabled, keepaliveInProfile, ipVersionInProfile,
+ encapTypeInProfile, new NetworkCapabilities.Builder().build());
+ }
+
+ private void doTestMigrateIkeSession_FromIkeTunnConnParams(
+ boolean isAutomaticIpVersionSelectionEnabled,
+ boolean isAutomaticNattKeepaliveTimerEnabled,
+ int keepaliveInProfile,
+ int ipVersionInProfile,
+ int encapTypeInProfile,
+ @NonNull final NetworkCapabilities nc) throws Exception {
+ final Ikev2VpnProfile ikeProfile = makeIkeV2VpnProfile(
+ isAutomaticIpVersionSelectionEnabled,
+ isAutomaticNattKeepaliveTimerEnabled,
+ keepaliveInProfile,
+ ipVersionInProfile,
+ encapTypeInProfile);
+
+ final IkeSessionParams ikeSessionParams =
+ ikeProfile.getIkeTunnelConnectionParams().getIkeSessionParams();
final int expectedKeepalive = isAutomaticNattKeepaliveTimerEnabled
? AUTOMATIC_KEEPALIVE_DELAY_SECONDS
: ikeSessionParams.getNattKeepAliveDelaySeconds();
@@ -2115,22 +2160,48 @@
? ESP_ENCAP_TYPE_AUTO
: ikeSessionParams.getEncapType();
doTestMigrateIkeSession(ikeProfile.toVpnProfile(), expectedKeepalive,
- expectedIpVersion, expectedEncapType);
+ expectedIpVersion, expectedEncapType, nc);
}
- private void doTestMigrateIkeSession(VpnProfile profile,
- int expectedKeepalive, int expectedIpVersion, int expectedEncapType) throws Exception {
+ @Test
+ public void doTestMigrateIkeSession_Vcn() throws Exception {
+ final int expectedKeepalive = 2097; // Any unlikely number will do
+ final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive))
+ .build();
+ final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile(
+ true /* isAutomaticIpVersionSelectionEnabled */,
+ true /* isAutomaticNattKeepaliveTimerEnabled */,
+ 234 /* keepaliveInProfile */, // Should be ignored, any value will do
+ ESP_IP_VERSION_IPV4, // Should be ignored
+ ESP_ENCAP_TYPE_UDP // Should be ignored
+ );
+ doTestMigrateIkeSession(
+ ikev2VpnProfile.toVpnProfile(),
+ expectedKeepalive,
+ ESP_IP_VERSION_AUTO /* expectedIpVersion */,
+ ESP_ENCAP_TYPE_AUTO /* expectedEncapType */,
+ vcnNc);
+ }
+
+ private void doTestMigrateIkeSession(
+ @NonNull final VpnProfile profile,
+ final int expectedKeepalive,
+ final int expectedIpVersion,
+ final int expectedEncapType,
+ @NonNull final NetworkCapabilities caps) throws Exception {
final PlatformVpnSnapshot vpnSnapShot =
verifySetupPlatformVpn(profile,
createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
+ caps /* underlying network capabilities */,
false /* mtuSupportsIpv6 */,
expectedKeepalive < DEFAULT_LONG_LIVED_TCP_CONNS_EXPENSIVE_TIMEOUT_SEC);
// Simulate a new network coming up
vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
verify(mIkeSessionWrapper, never()).setNetwork(any(), anyInt(), anyInt(), anyInt());
- vpnSnapShot.nwCb.onCapabilitiesChanged(
- TEST_NETWORK_2, new NetworkCapabilities.Builder().build());
+ vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK_2, caps);
// Verify MOBIKE is triggered
verify(mIkeSessionWrapper, timeout(TEST_TIMEOUT_MS)).setNetwork(TEST_NETWORK_2,
expectedIpVersion, expectedEncapType, expectedKeepalive);
@@ -2156,6 +2227,7 @@
final PlatformVpnSnapshot vpnSnapShot =
verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
+ new NetworkCapabilities.Builder().build() /* underlying network caps */,
hasV6 /* mtuSupportsIpv6 */,
false /* areLongLivedTcpConnectionsExpensive */);
reset(mExecutor);
@@ -2343,6 +2415,7 @@
final PlatformVpnSnapshot vpnSnapShot =
verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
+ new NetworkCapabilities.Builder().build() /* underlying network caps */,
false /* mtuSupportsIpv6 */,
true /* areLongLivedTcpConnectionsExpensive */);
@@ -2542,6 +2615,81 @@
vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
}
+ private String getDump(@NonNull final Vpn vpn) {
+ final StringWriter sw = new StringWriter();
+ final IndentingPrintWriter writer = new IndentingPrintWriter(sw, "");
+ vpn.dump(writer);
+ writer.flush();
+ return sw.toString();
+ }
+
+ private int countMatches(@NonNull final Pattern regexp, @NonNull final String string) {
+ final Matcher m = regexp.matcher(string);
+ int i = 0;
+ while (m.find()) ++i;
+ return i;
+ }
+
+ @Test
+ public void testNCEventChanges() throws Exception {
+ final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ .setLinkDownstreamBandwidthKbps(1000)
+ .setLinkUpstreamBandwidthKbps(500);
+
+ final Ikev2VpnProfile ikeProfile =
+ new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY)
+ .setAuthPsk(TEST_VPN_PSK)
+ .setBypassable(true /* isBypassable */)
+ .setAutomaticNattKeepaliveTimerEnabled(true)
+ .setAutomaticIpVersionSelectionEnabled(true)
+ .build();
+
+ final PlatformVpnSnapshot vpnSnapShot =
+ verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
+ createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
+ ncBuilder.build(), false /* mtuSupportsIpv6 */,
+ true /* areLongLivedTcpConnectionsExpensive */);
+
+ // Calls to onCapabilitiesChanged will be thrown to the executor for execution ; by
+ // default this will incur a 10ms delay before it's executed, messing with the timing
+ // of the log and having the checks for counts in equals() below flake.
+ mExecutor.executeDirect = true;
+
+ // First nc changed triggered by verifySetupPlatformVpn
+ final Pattern pattern = Pattern.compile("Cap changed from", Pattern.MULTILINE);
+ final String stage1 = getDump(vpnSnapShot.vpn);
+ assertEquals(1, countMatches(pattern, stage1));
+
+ vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
+ final String stage2 = getDump(vpnSnapShot.vpn);
+ // Was the same caps, there should still be only 1 match
+ assertEquals(1, countMatches(pattern, stage2));
+
+ ncBuilder.setLinkDownstreamBandwidthKbps(1200)
+ .setLinkUpstreamBandwidthKbps(300);
+ vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
+ final String stage3 = getDump(vpnSnapShot.vpn);
+ // Was not an important change, should not be logged, still only 1 match
+ assertEquals(1, countMatches(pattern, stage3));
+
+ ncBuilder.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+ vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
+ final String stage4 = getDump(vpnSnapShot.vpn);
+ // Change to caps is important, should cause a new match
+ assertEquals(2, countMatches(pattern, stage4));
+
+ ncBuilder.removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
+ ncBuilder.setLinkDownstreamBandwidthKbps(600);
+ vpnSnapShot.nwCb.onCapabilitiesChanged(TEST_NETWORK, ncBuilder.build());
+ final String stage5 = getDump(vpnSnapShot.vpn);
+ // Change to caps is important, should cause a new match even with the unimportant change
+ assertEquals(3, countMatches(pattern, stage5));
+ }
+ // TODO : beef up event logs tests
+
private void verifyHandlingNetworkLoss(PlatformVpnSnapshot vpnSnapShot) throws Exception {
// Forget the #sendLinkProperties during first setup.
reset(mMockNetworkAgent);
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 4b495cd..b539fe0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -23,11 +23,13 @@
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
+import com.android.net.module.util.SharedLog
import com.android.server.connectivity.mdns.MdnsAdvertiser.AdvertiserCallback
import com.android.server.connectivity.mdns.MdnsSocketProvider.SocketCallback
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.waitForIdle
+import java.net.NetworkInterface
import java.util.Objects
import org.junit.After
import org.junit.Before
@@ -55,6 +57,7 @@
private val TEST_NETWORK_1 = mock(Network::class.java)
private val TEST_NETWORK_2 = mock(Network::class.java)
private val TEST_HOSTNAME = arrayOf("Android_test", "local")
+private const val TEST_SUBTYPE = "_subtype"
private val SERVICE_1 = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
port = 12345
@@ -89,6 +92,7 @@
private val handler by lazy { Handler(thread.looper) }
private val socketProvider = mock(MdnsSocketProvider::class.java)
private val cb = mock(AdvertiserCallback::class.java)
+ private val sharedlog = mock(SharedLog::class.java)
private val mockSocket1 = mock(MdnsInterfaceSocket::class.java)
private val mockSocket2 = mock(MdnsInterfaceSocket::class.java)
@@ -101,13 +105,15 @@
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(), eq(TEST_HOSTNAME), any()
)
doReturn(mockInterfaceAdvertiser2).`when`(mockDeps).makeAdvertiser(eq(mockSocket2),
- any(), any(), any(), any(), eq(TEST_HOSTNAME)
+ any(), any(), any(), any(), eq(TEST_HOSTNAME), any()
)
doReturn(true).`when`(mockInterfaceAdvertiser1).isProbing(anyInt())
doReturn(true).`when`(mockInterfaceAdvertiser2).isProbing(anyInt())
+ doReturn(createEmptyNetworkInterface()).`when`(mockSocket1).getInterface()
+ doReturn(createEmptyNetworkInterface()).`when`(mockSocket2).getInterface()
}
@After
@@ -116,10 +122,16 @@
thread.join()
}
+ private fun createEmptyNetworkInterface(): NetworkInterface {
+ val constructor = NetworkInterface::class.java.getDeclaredConstructor()
+ constructor.isAccessible = true
+ return constructor.newInstance()
+ }
+
@Test
fun testAddService_OneNetwork() {
- val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps)
- postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1) }
+ val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
+ postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -134,7 +146,8 @@
eq(thread.looper),
any(),
intAdvCbCaptor.capture(),
- eq(TEST_HOSTNAME)
+ eq(TEST_HOSTNAME),
+ any()
)
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
@@ -148,8 +161,8 @@
@Test
fun testAddService_AllNetworks() {
- val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps)
- postSync { advertiser.addService(SERVICE_ID_1, ALL_NETWORKS_SERVICE) }
+ val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
+ postSync { advertiser.addService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE) }
val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
@@ -162,11 +175,15 @@
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)
+ eq(thread.looper), any(), intAdvCbCaptor1.capture(), eq(TEST_HOSTNAME), any()
)
verify(mockDeps).makeAdvertiser(eq(mockSocket2), eq(listOf(TEST_LINKADDR)),
- eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME)
+ eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any()
)
+ verify(mockInterfaceAdvertiser1).addService(
+ anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+ verify(mockInterfaceAdvertiser2).addService(
+ anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor1.value.onRegisterServiceSucceeded(
@@ -194,21 +211,22 @@
@Test
fun testAddService_Conflicts() {
- val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps)
- postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1) }
+ val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
+ postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
val oneNetSocketCb = oneNetSocketCbCaptor.value
// Register a service with the same name on all networks (name conflict)
- postSync { advertiser.addService(SERVICE_ID_2, ALL_NETWORKS_SERVICE) }
+ postSync { advertiser.addService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, null /* subtype */) }
val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
val allNetSocketCb = allNetSocketCbCaptor.value
- postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1) }
- postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE) }
+ postSync { advertiser.addService(LONG_SERVICE_ID_1, LONG_SERVICE_1, null /* subtype */) }
+ postSync { advertiser.addService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
+ null /* subtype */) }
// Callbacks for matching network and all networks both get the socket
postSync {
@@ -233,16 +251,16 @@
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)
+ eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any()
)
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
- argThat { it.matches(SERVICE_1) })
+ argThat { it.matches(SERVICE_1) }, eq(null))
verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
- argThat { it.matches(expectedRenamed) })
+ argThat { it.matches(expectedRenamed) }, eq(null))
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
- argThat { it.matches(LONG_SERVICE_1) })
+ argThat { it.matches(LONG_SERVICE_1) }, eq(null))
verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
- argThat { it.matches(expectedLongRenamed) })
+ argThat { it.matches(expectedLongRenamed) }, eq(null))
doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
postSync { intAdvCbCaptor.value.onRegisterServiceSucceeded(
@@ -264,9 +282,9 @@
@Test
fun testRemoveService_whenAllServiceRemoved_thenUpdateHostName() {
- val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps)
+ val advertiser = MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog)
verify(mockDeps, times(1)).generateHostname()
- postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1) }
+ postSync { advertiser.addService(SERVICE_ID_1, SERVICE_1, null /* subtype */) }
postSync { advertiser.removeService(SERVICE_ID_1) }
verify(mockDeps, times(2)).generateHostname()
}
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 7e7e6a4..350bad0 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -18,26 +18,36 @@
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-import static org.mockito.Mockito.any;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.Network;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.text.TextUtils;
import android.util.Pair;
+import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
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;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
@@ -50,98 +60,278 @@
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
public class MdnsDiscoveryManagerTests {
-
+ private static final long DEFAULT_TIMEOUT = 2000L;
private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
private static final String SERVICE_TYPE_2 = "_test._tcp.local";
- private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1 =
+ private static final Network NETWORK_1 = Mockito.mock(Network.class);
+ private static final Network NETWORK_2 = Mockito.mock(Network.class);
+ private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_NULL_NETWORK =
Pair.create(SERVICE_TYPE_1, null);
- private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2 =
+ private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_NETWORK_1 =
+ Pair.create(SERVICE_TYPE_1, NETWORK_1);
+ private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NULL_NETWORK =
Pair.create(SERVICE_TYPE_2, null);
+ private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NETWORK_1 =
+ Pair.create(SERVICE_TYPE_2, NETWORK_1);
+ private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_NETWORK_2 =
+ Pair.create(SERVICE_TYPE_2, NETWORK_2);
@Mock private ExecutorProvider executorProvider;
@Mock private MdnsSocketClientBase socketClient;
- @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
- @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientType1NullNetwork;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientType1Network1;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientType2NullNetwork;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientType2Network1;
+ @Mock private MdnsServiceTypeClient mockServiceTypeClientType2Network2;
@Mock MdnsServiceBrowserListener mockListenerOne;
@Mock MdnsServiceBrowserListener mockListenerTwo;
+ @Mock SharedLog sharedLog;
private MdnsDiscoveryManager discoveryManager;
+ private HandlerThread thread;
+ private Handler handler;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- when(mockServiceTypeClientOne.getServiceTypeLabels())
- .thenReturn(TextUtils.split(SERVICE_TYPE_1, "\\."));
- when(mockServiceTypeClientTwo.getServiceTypeLabels())
- .thenReturn(TextUtils.split(SERVICE_TYPE_2, "\\."));
-
- discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient) {
+ thread = new HandlerThread("MdnsDiscoveryManagerTests");
+ thread.start();
+ handler = new Handler(thread.getLooper());
+ doReturn(thread.getLooper()).when(socketClient).getLooper();
+ discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient,
+ sharedLog) {
@Override
MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
@Nullable Network network) {
final Pair<String, Network> perNetworkServiceType =
Pair.create(serviceType, network);
- if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1)) {
- return mockServiceTypeClientOne;
- } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_2)) {
- return mockServiceTypeClientTwo;
+ if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1_NULL_NETWORK)) {
+ return mockServiceTypeClientType1NullNetwork;
+ } else if (perNetworkServiceType.equals(
+ PER_NETWORK_SERVICE_TYPE_1_NETWORK_1)) {
+ return mockServiceTypeClientType1Network1;
+ } else if (perNetworkServiceType.equals(
+ PER_NETWORK_SERVICE_TYPE_2_NULL_NETWORK)) {
+ return mockServiceTypeClientType2NullNetwork;
+ } else if (perNetworkServiceType.equals(
+ PER_NETWORK_SERVICE_TYPE_2_NETWORK_1)) {
+ return mockServiceTypeClientType2Network1;
+ } else if (perNetworkServiceType.equals(
+ PER_NETWORK_SERVICE_TYPE_2_NETWORK_2)) {
+ return mockServiceTypeClientType2Network2;
}
return null;
}
};
}
- private void verifyListenerRegistration(String serviceType, MdnsServiceBrowserListener listener,
- MdnsServiceTypeClient client) throws IOException {
+ @After
+ public void tearDown() {
+ if (thread != null) {
+ thread.quitSafely();
+ }
+ }
+
+ private void runOnHandler(Runnable r) {
+ handler.post(r);
+ HandlerUtils.waitForIdle(handler, DEFAULT_TIMEOUT);
+ }
+
+ private SocketCreationCallback expectSocketCreationCallback(String serviceType,
+ MdnsServiceBrowserListener listener, MdnsSearchOptions options) throws IOException {
final ArgumentCaptor<SocketCreationCallback> callbackCaptor =
ArgumentCaptor.forClass(SocketCreationCallback.class);
- discoveryManager.registerListener(serviceType, listener,
- MdnsSearchOptions.getDefaultOptions());
+ runOnHandler(() -> discoveryManager.registerListener(serviceType, listener, options));
verify(socketClient).startDiscovery();
verify(socketClient).notifyNetworkRequested(
- eq(listener), any(), callbackCaptor.capture());
- final SocketCreationCallback callback = callbackCaptor.getValue();
- callback.onSocketCreated(null /* network */);
- verify(client).startSendAndReceive(listener, MdnsSearchOptions.getDefaultOptions());
+ eq(listener), eq(options.getNetwork()), callbackCaptor.capture());
+ return callbackCaptor.getValue();
}
@Test
public void registerListener_unregisterListener() throws IOException {
- verifyListenerRegistration(SERVICE_TYPE_1, mockListenerOne, mockServiceTypeClientOne);
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
+ verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
- when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
- discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
- verify(mockServiceTypeClientOne).stopSendAndReceive(mockListenerOne);
+ when(mockServiceTypeClientType1NullNetwork.stopSendAndReceive(mockListenerOne))
+ .thenReturn(true);
+ runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
+ verify(mockServiceTypeClientType1NullNetwork).stopSendAndReceive(mockListenerOne);
verify(socketClient).stopDiscovery();
}
@Test
public void registerMultipleListeners() throws IOException {
- verifyListenerRegistration(SERVICE_TYPE_1, mockListenerOne, mockServiceTypeClientOne);
- verifyListenerRegistration(SERVICE_TYPE_2, mockListenerTwo, mockServiceTypeClientTwo);
+ final MdnsSearchOptions options =
+ MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
+ verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(mockListenerOne, options);
+ runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options);
+
+ final SocketCreationCallback callback2 = expectSocketCreationCallback(
+ SERVICE_TYPE_2, mockListenerTwo, options);
+ runOnHandler(() -> callback2.onSocketCreated(null /* network */));
+ verify(mockServiceTypeClientType2NullNetwork).startSendAndReceive(mockListenerTwo, options);
+ runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
+ verify(mockServiceTypeClientType2Network2).startSendAndReceive(mockListenerTwo, options);
}
@Test
public void onResponseReceived() throws IOException {
- verifyListenerRegistration(SERVICE_TYPE_1, mockListenerOne, mockServiceTypeClientOne);
- verifyListenerRegistration(SERVICE_TYPE_2, mockListenerTwo, mockServiceTypeClientTwo);
+ final MdnsSearchOptions options1 =
+ MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, options1);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
+ verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
+ mockListenerOne, options1);
+ runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(mockListenerOne, options1);
- MdnsPacket responseForServiceTypeOne = createMdnsPacket(SERVICE_TYPE_1);
+ final MdnsSearchOptions options2 =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
+ final SocketCreationCallback callback2 = expectSocketCreationCallback(
+ SERVICE_TYPE_2, mockListenerTwo, options2);
+ runOnHandler(() -> callback2.onSocketCreated(NETWORK_2));
+ verify(mockServiceTypeClientType2Network2).startSendAndReceive(mockListenerTwo, options2);
+
+ final MdnsPacket responseForServiceTypeOne = createMdnsPacket(SERVICE_TYPE_1);
final int ifIndex = 1;
- discoveryManager.onResponseReceived(responseForServiceTypeOne, ifIndex, null /* network */);
- verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeOne, ifIndex,
- null /* network */);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ responseForServiceTypeOne, ifIndex, null /* network */));
+ // Packets for network null are only processed by the ServiceTypeClient for network null
+ verify(mockServiceTypeClientType1NullNetwork).processResponse(responseForServiceTypeOne,
+ ifIndex, null /* network */);
+ verify(mockServiceTypeClientType1Network1, never()).processResponse(any(), anyInt(), any());
+ verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), anyInt(), any());
- MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
- discoveryManager.onResponseReceived(responseForServiceTypeTwo, ifIndex, null /* network */);
- verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo, ifIndex,
- null /* network */);
+ final MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ responseForServiceTypeTwo, ifIndex, NETWORK_1));
+ verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(any(), anyInt(),
+ eq(NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).processResponse(responseForServiceTypeTwo,
+ ifIndex, NETWORK_1);
+ verify(mockServiceTypeClientType2Network2, never()).processResponse(any(), anyInt(),
+ eq(NETWORK_1));
- MdnsPacket responseForSubtype = createMdnsPacket("subtype._sub._googlecast._tcp.local");
- discoveryManager.onResponseReceived(responseForSubtype, ifIndex, null /* network */);
- verify(mockServiceTypeClientOne).processResponse(responseForSubtype, ifIndex,
- null /* network */);
+ final MdnsPacket responseForSubtype =
+ createMdnsPacket("subtype._sub._googlecast._tcp.local");
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ responseForSubtype, ifIndex, NETWORK_2));
+ verify(mockServiceTypeClientType1NullNetwork, never()).processResponse(
+ any(), anyInt(), eq(NETWORK_2));
+ verify(mockServiceTypeClientType1Network1, never()).processResponse(
+ any(), anyInt(), eq(NETWORK_2));
+ verify(mockServiceTypeClientType2Network2).processResponse(
+ responseForSubtype, ifIndex, NETWORK_2);
+ }
+
+ @Test
+ public void testSocketCreatedAndDestroyed() throws IOException {
+ // Create a ServiceTypeClient for SERVICE_TYPE_1 and NETWORK_1
+ final MdnsSearchOptions network1Options =
+ MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, network1Options);
+ runOnHandler(() -> callback.onSocketCreated(NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).startSendAndReceive(
+ mockListenerOne, network1Options);
+
+ // Create a ServiceTypeClient for SERVICE_TYPE_2 and NETWORK_1
+ final SocketCreationCallback callback2 = expectSocketCreationCallback(
+ SERVICE_TYPE_2, mockListenerTwo, network1Options);
+ runOnHandler(() -> callback2.onSocketCreated(NETWORK_1));
+ verify(mockServiceTypeClientType2Network1).startSendAndReceive(
+ mockListenerTwo, network1Options);
+
+ // Receive a response, it should be processed on both clients.
+ final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
+ final int ifIndex = 1;
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).processResponse(response, ifIndex, NETWORK_1);
+ verify(mockServiceTypeClientType2Network1).processResponse(response, ifIndex, NETWORK_1);
+
+ // The first callback receives a notification that the network has been destroyed,
+ // mockServiceTypeClientOne1 should send service removed notifications and remove from the
+ // list of clients.
+ runOnHandler(() -> callback.onAllSocketsDestroyed(NETWORK_1));
+ verify(mockServiceTypeClientType1Network1).notifySocketDestroyed();
+
+ // Receive a response again, it should be processed only on
+ // mockServiceTypeClientType2Network1. Because the mockServiceTypeClientType1Network1 is
+ // removed from the list of clients, it is no longer able to process responses.
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, NETWORK_1));
+ // Still times(1) as a response was received once previously
+ verify(mockServiceTypeClientType1Network1, times(1))
+ .processResponse(response, ifIndex, NETWORK_1);
+ verify(mockServiceTypeClientType2Network1, times(2))
+ .processResponse(response, ifIndex, NETWORK_1);
+
+ // The client for NETWORK_1 receives the callback that the NETWORK_2 has been destroyed,
+ // mockServiceTypeClientTwo2 shouldn't send any notifications.
+ runOnHandler(() -> callback2.onAllSocketsDestroyed(NETWORK_2));
+ verify(mockServiceTypeClientType2Network1, never()).notifySocketDestroyed();
+
+ // Receive a response again, mockServiceTypeClientType2Network1 is still in the list of
+ // clients, it's still able to process responses.
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, NETWORK_1));
+ verify(mockServiceTypeClientType1Network1, times(1))
+ .processResponse(response, ifIndex, NETWORK_1);
+ verify(mockServiceTypeClientType2Network1, times(3))
+ .processResponse(response, ifIndex, NETWORK_1);
+ }
+
+ @Test
+ public void testUnregisterListenerAfterSocketDestroyed() throws IOException {
+ // Create a ServiceTypeClient for SERVICE_TYPE_1
+ final MdnsSearchOptions network1Options =
+ MdnsSearchOptions.newBuilder().setNetwork(null /* network */).build();
+ final SocketCreationCallback callback = expectSocketCreationCallback(
+ SERVICE_TYPE_1, mockListenerOne, network1Options);
+ runOnHandler(() -> callback.onSocketCreated(null /* network */));
+ verify(mockServiceTypeClientType1NullNetwork).startSendAndReceive(
+ mockListenerOne, network1Options);
+
+ // Receive a response, it should be processed on the client.
+ final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
+ final int ifIndex = 1;
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, null /* network */));
+ verify(mockServiceTypeClientType1NullNetwork).processResponse(
+ response, ifIndex, null /* network */);
+
+ runOnHandler(() -> callback.onAllSocketsDestroyed(null /* network */));
+ verify(mockServiceTypeClientType1NullNetwork).notifySocketDestroyed();
+
+ // Receive a response again, it should not be processed.
+ runOnHandler(() -> discoveryManager.onResponseReceived(
+ response, ifIndex, null /* network */));
+ // Still times(1) as a response was received once previously
+ verify(mockServiceTypeClientType1NullNetwork, times(1))
+ .processResponse(response, ifIndex, null /* network */);
+
+ // Unregister the listener, notifyNetworkUnrequested should be called but other stop methods
+ // won't be call because the service type client was unregistered and destroyed. But those
+ // cleanups were done in notifySocketDestroyed when the socket was destroyed.
+ runOnHandler(() -> discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne));
+ verify(socketClient).notifyNetworkUnrequested(mockListenerOne);
+ verify(mockServiceTypeClientType1NullNetwork, never()).stopSendAndReceive(any());
+ // The stopDiscovery() is only used by MdnsSocketClient, which doesn't send
+ // onAllSocketsDestroyed(). So the socket clients that send onAllSocketsDestroyed() do not
+ // need to call stopDiscovery().
+ verify(socketClient, never()).stopDiscovery();
}
private MdnsPacket createMdnsPacket(String serviceType) {
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 9c0abfc..dd458b8 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -76,7 +76,7 @@
private val replySender = mock(MdnsReplySender::class.java)
private val announcer = mock(MdnsAnnouncer::class.java)
private val prober = mock(MdnsProber::class.java)
- private val sharedlog = mock(SharedLog::class.java)
+ private val sharedlog = SharedLog("MdnsInterfaceAdvertiserTest")
@Suppress("UNCHECKED_CAST")
private val probeCbCaptor = ArgumentCaptor.forClass(PacketRepeaterCallback::class.java)
as ArgumentCaptor<PacketRepeaterCallback<ProbingInfo>>
@@ -92,7 +92,6 @@
private val advertiser by lazy {
MdnsInterfaceAdvertiser(
- LOG_TAG,
socket,
TEST_ADDRS,
thread.looper,
@@ -116,8 +115,9 @@
val knownServices = mutableSetOf<Int>()
doAnswer { inv ->
knownServices.add(inv.getArgument(0))
+
-1
- }.`when`(repository).addService(anyInt(), any())
+ }.`when`(repository).addService(anyInt(), any(), any())
doAnswer { inv ->
knownServices.remove(inv.getArgument(0))
null
@@ -278,8 +278,8 @@
doReturn(serviceId).`when`(testProbingInfo).serviceId
doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
- advertiser.addService(serviceId, serviceInfo)
- verify(repository).addService(serviceId, serviceInfo)
+ advertiser.addService(serviceId, serviceInfo, null /* subtype */)
+ verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
verify(prober).startProbing(testProbingInfo)
// Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
index 90c43e5..87ba5d7 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClientTest.java
@@ -24,8 +24,12 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.net.InetAddresses;
import android.net.Network;
@@ -34,6 +38,7 @@
import android.os.HandlerThread;
import com.android.net.module.util.HexDump;
+import com.android.server.connectivity.mdns.MdnsSocketClientBase.SocketCreationCallback;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.HandlerUtils;
@@ -62,7 +67,7 @@
@Mock private MdnsInterfaceSocket mSocket;
@Mock private MdnsServiceBrowserListener mListener;
@Mock private MdnsSocketClientBase.Callback mCallback;
- @Mock private MdnsSocketClientBase.SocketCreationCallback mSocketCreationCallback;
+ @Mock private SocketCreationCallback mSocketCreationCallback;
private MdnsMultinetworkSocketClient mSocketClient;
private Handler mHandler;
@@ -77,12 +82,17 @@
}
private SocketCallback expectSocketCallback() {
+ return expectSocketCallback(mListener, mNetwork);
+ }
+
+ private SocketCallback expectSocketCallback(MdnsServiceBrowserListener listener,
+ Network requestedNetwork) {
final ArgumentCaptor<SocketCallback> callbackCaptor =
ArgumentCaptor.forClass(SocketCallback.class);
mHandler.post(() -> mSocketClient.notifyNetworkRequested(
- mListener, mNetwork, mSocketCreationCallback));
+ listener, requestedNetwork, mSocketCreationCallback));
verify(mProvider, timeout(DEFAULT_TIMEOUT))
- .requestSocket(eq(mNetwork), callbackCaptor.capture());
+ .requestSocket(eq(requestedNetwork), callbackCaptor.capture());
return callbackCaptor.getValue();
}
@@ -104,22 +114,36 @@
InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
final DatagramPacket ipv6Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
InetAddresses.parseNumericAddress("2001:db8::"), 0 /* port */);
- doReturn(true).when(mSocket).hasJoinedIpv4();
- doReturn(true).when(mSocket).hasJoinedIpv6();
- doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+
+ final MdnsInterfaceSocket tetherIfaceSock1 = mock(MdnsInterfaceSocket.class);
+ final MdnsInterfaceSocket tetherIfaceSock2 = mock(MdnsInterfaceSocket.class);
+ for (MdnsInterfaceSocket socket : List.of(mSocket, tetherIfaceSock1, tetherIfaceSock2)) {
+ doReturn(true).when(socket).hasJoinedIpv4();
+ doReturn(true).when(socket).hasJoinedIpv6();
+ doReturn(createEmptyNetworkInterface()).when(socket).getInterface();
+ }
+
// Notify socket created
callback.onSocketCreated(mNetwork, mSocket, List.of());
verify(mSocketCreationCallback).onSocketCreated(mNetwork);
+ callback.onSocketCreated(null, tetherIfaceSock1, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(null);
+ callback.onSocketCreated(null, tetherIfaceSock2, List.of());
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
// Send packet to IPv4 with target network and verify sending has been called.
mSocketClient.sendMulticastPacket(ipv4Packet, mNetwork);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mSocket).send(ipv4Packet);
+ verify(tetherIfaceSock1, never()).send(any());
+ verify(tetherIfaceSock2, never()).send(any());
// Send packet to IPv6 without target network and verify sending has been called.
- mSocketClient.sendMulticastPacket(ipv6Packet);
+ mSocketClient.sendMulticastPacket(ipv6Packet, null);
HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
- verify(mSocket).send(ipv6Packet);
+ verify(mSocket, never()).send(ipv6Packet);
+ verify(tetherIfaceSock1).send(ipv6Packet);
+ verify(tetherIfaceSock2).send(ipv6Packet);
}
@Test
@@ -169,4 +193,128 @@
new String[] { "Android", "local" } /* serviceHost */)
), response.answers);
}
+
+ @Test
+ public void testSocketRemovedAfterNetworkUnrequested() throws IOException {
+ // Request sockets on all networks
+ final SocketCallback callback = expectSocketCallback(mListener, null);
+ final DatagramPacket ipv4Packet = new DatagramPacket(BUFFER, 0 /* offset */, BUFFER.length,
+ InetAddresses.parseNumericAddress("192.0.2.1"), 0 /* port */);
+
+ // Notify 3 socket created, including 2 tethered interfaces (null network)
+ final MdnsInterfaceSocket socket2 = mock(MdnsInterfaceSocket.class);
+ final MdnsInterfaceSocket socket3 = mock(MdnsInterfaceSocket.class);
+ doReturn(true).when(mSocket).hasJoinedIpv4();
+ doReturn(true).when(mSocket).hasJoinedIpv6();
+ doReturn(true).when(socket2).hasJoinedIpv4();
+ doReturn(true).when(socket2).hasJoinedIpv6();
+ doReturn(true).when(socket3).hasJoinedIpv4();
+ doReturn(true).when(socket3).hasJoinedIpv6();
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ doReturn(createEmptyNetworkInterface()).when(socket2).getInterface();
+ doReturn(createEmptyNetworkInterface()).when(socket3).getInterface();
+
+ callback.onSocketCreated(mNetwork, mSocket, List.of());
+ callback.onSocketCreated(null, socket2, List.of());
+ callback.onSocketCreated(null, socket3, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(mNetwork);
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+
+ // Send IPv4 packet on the non-null Network and verify sending has been called.
+ mSocketClient.sendMulticastPacket(ipv4Packet, mNetwork);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mSocket).send(ipv4Packet);
+ verify(socket2, never()).send(any());
+ verify(socket3, never()).send(any());
+
+ // Request another socket with null network, get the same interfaces
+ final SocketCreationCallback socketCreationCb2 = mock(SocketCreationCallback.class);
+ final MdnsServiceBrowserListener listener2 = mock(MdnsServiceBrowserListener.class);
+
+ // requestSocket is called a second time
+ final ArgumentCaptor<SocketCallback> callback2Captor =
+ ArgumentCaptor.forClass(SocketCallback.class);
+ mHandler.post(() -> mSocketClient.notifyNetworkRequested(
+ listener2, null, socketCreationCb2));
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mProvider, times(2)).requestSocket(eq(null), callback2Captor.capture());
+ final SocketCallback callback2 = callback2Captor.getAllValues().get(1);
+
+ // Notify socket created for all networks.
+ callback2.onSocketCreated(mNetwork, mSocket, List.of());
+ callback2.onSocketCreated(null, socket2, List.of());
+ callback2.onSocketCreated(null, socket3, List.of());
+ verify(socketCreationCb2).onSocketCreated(mNetwork);
+ verify(socketCreationCb2, times(2)).onSocketCreated(null);
+
+ // Send IPv4 packet to null network and verify sending to the 2 tethered interface sockets.
+ mSocketClient.sendMulticastPacket(ipv4Packet, null);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ // ipv4Packet still sent only once on mSocket: times(1) matches the packet sent earlier on
+ // mNetwork
+ verify(mSocket, times(1)).send(ipv4Packet);
+ verify(socket2).send(ipv4Packet);
+ verify(socket3).send(ipv4Packet);
+
+ // Unregister the second request
+ mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(listener2));
+ verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback2);
+
+ // Send IPv4 packet again and verify it's still sent a second time
+ mSocketClient.sendMulticastPacket(ipv4Packet, null);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(socket2, times(2)).send(ipv4Packet);
+ verify(socket3, times(2)).send(ipv4Packet);
+
+ // Unrequest remaining sockets
+ mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
+ verify(mProvider, timeout(DEFAULT_TIMEOUT)).unrequestSocket(callback);
+
+ // Send IPv4 packet and verify no more sending.
+ mSocketClient.sendMulticastPacket(ipv4Packet, null);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ verify(mSocket, times(1)).send(ipv4Packet);
+ verify(socket2, times(2)).send(ipv4Packet);
+ verify(socket3, times(2)).send(ipv4Packet);
+ }
+
+ @Test
+ public void testNotifyNetworkUnrequested_SocketsOnNullNetwork() {
+ final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
+ final SocketCallback callback = expectSocketCallback(
+ mListener, null /* requestedNetwork */);
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
+
+ callback.onSocketCreated(null /* network */, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(null);
+ callback.onSocketCreated(null /* network */, otherSocket, List.of());
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+
+ verify(mSocketCreationCallback, never()).onAllSocketsDestroyed(null /* network */);
+ mHandler.post(() -> mSocketClient.notifyNetworkUnrequested(mListener));
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+
+ verify(mProvider).unrequestSocket(callback);
+ verify(mSocketCreationCallback).onAllSocketsDestroyed(null /* network */);
+ }
+
+ @Test
+ public void testSocketCreatedAndDestroyed_NullNetwork() throws IOException {
+ final MdnsInterfaceSocket otherSocket = mock(MdnsInterfaceSocket.class);
+ final SocketCallback callback = expectSocketCallback(mListener, null /* network */);
+ doReturn(createEmptyNetworkInterface()).when(mSocket).getInterface();
+ doReturn(createEmptyNetworkInterface()).when(otherSocket).getInterface();
+
+ callback.onSocketCreated(null /* network */, mSocket, List.of());
+ verify(mSocketCreationCallback).onSocketCreated(null);
+ callback.onSocketCreated(null /* network */, otherSocket, List.of());
+ verify(mSocketCreationCallback, times(2)).onSocketCreated(null);
+
+ // Notify socket destroyed
+ callback.onInterfaceDestroyed(null /* network */, mSocket);
+ verifyNoMoreInteractions(mSocketCreationCallback);
+ callback.onInterfaceDestroyed(null /* network */, otherSocket);
+ verify(mSocketCreationCallback).onAllSocketsDestroyed(null /* network */);
+ }
}
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 44e0d08..4a39b93 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -44,6 +44,7 @@
private const val TEST_SERVICE_ID_1 = 42
private const val TEST_SERVICE_ID_2 = 43
private const val TEST_PORT = 12345
+private const val TEST_SUBTYPE = "_subtype"
private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
private val TEST_ADDRESSES = listOf(
LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -86,7 +87,8 @@
fun testAddServiceAndProbe() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
assertEquals(0, repository.servicesCount)
- assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
+ 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)
@@ -118,18 +120,18 @@
@Test
fun testAddAndConflicts() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(NameConflictException::class) {
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
}
}
@Test
fun testInvalidReuseOfServiceId() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertFailsWith(IllegalArgumentException::class) {
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
}
}
@@ -138,7 +140,7 @@
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -180,13 +182,49 @@
}
@Test
+ fun testExitAnnouncements_WithSubtype() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+ repository.onAdvertisementSent(TEST_SERVICE_ID_1)
+
+ val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
+ assertNotNull(exitAnnouncement)
+ assertEquals(1, repository.servicesCount)
+ val packet = exitAnnouncement.getPacket(0)
+
+ assertEquals(0x8400 /* response, authoritative */, packet.flags)
+ assertEquals(0, packet.questions.size)
+ assertEquals(0, packet.authorityRecords.size)
+ assertEquals(0, packet.additionalRecords.size)
+
+ assertContentEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")),
+ MdnsPointerRecord(
+ arrayOf("_subtype", "_sub", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */,
+ true /* cacheFlush */,
+ 0L /* ttlMillis */,
+ arrayOf("MyTestService", "_testservice", "_tcp", "local")),
+ ), packet.answers)
+
+ repository.removeService(TEST_SERVICE_ID_1)
+ assertEquals(0, repository.servicesCount)
+ }
+
+ @Test
fun testExitingServiceReAdded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
repository.onAdvertisementSent(TEST_SERVICE_ID_1)
repository.exitService(TEST_SERVICE_ID_1)
- assertEquals(TEST_SERVICE_ID_1, repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
+ assertEquals(TEST_SERVICE_ID_1,
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
assertEquals(1, repository.servicesCount)
repository.removeService(TEST_SERVICE_ID_2)
@@ -196,7 +234,8 @@
@Test
fun testOnProbingSucceeded() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+ TEST_SUBTYPE)
repository.onAdvertisementSent(TEST_SERVICE_ID_1)
val packet = announcementInfo.getPacket(0)
@@ -205,6 +244,7 @@
assertEquals(0, packet.authorityRecords.size)
val serviceType = arrayOf("_testservice", "_tcp", "local")
+ val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
@@ -250,6 +290,13 @@
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
+ MdnsPointerRecord(
+ serviceSubtype,
+ 0L /* receiptTimeMillis */,
+ // Not a unique name owned by the announcer, so cacheFlush=false
+ false /* cacheFlush */,
+ 4500000L /* ttlMillis */,
+ serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
@@ -319,9 +366,21 @@
@Test
fun testGetReply() {
+ doGetReplyTest(subtype = null)
+ }
+
+ @Test
+ fun testGetReply_WithSubtype() {
+ doGetReplyTest(TEST_SUBTYPE)
+ }
+
+ private fun doGetReplyTest(subtype: String?) {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- val questions = listOf(MdnsPointerRecord(arrayOf("_testservice", "_tcp", "local"),
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
+ val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
+ else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
+
+ val questions = listOf(MdnsPointerRecord(queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
// TTL and data is empty for a question
@@ -344,7 +403,7 @@
assertEquals(listOf(
MdnsPointerRecord(
- arrayOf("_testservice", "_tcp", "local"),
+ queriedName,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
longTtl,
@@ -405,8 +464,8 @@
@Test
fun testGetConflictingServices() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
val packet = MdnsPacket(
0 /* flags */,
@@ -433,8 +492,8 @@
@Test
fun testGetConflictingServices_IdenticalService() {
val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME)
- repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
- repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
val otherTtlMillis = 1234L
val packet = MdnsPacket(
@@ -460,10 +519,13 @@
}
}
-private fun MdnsRecordRepository.initWithService(serviceId: Int, serviceInfo: NsdServiceInfo):
- AnnouncementInfo {
+private fun MdnsRecordRepository.initWithService(
+ serviceId: Int,
+ serviceInfo: NsdServiceInfo,
+ subtype: String? = null
+): AnnouncementInfo {
updateAddresses(TEST_ADDRESSES)
- addService(serviceId, serviceInfo)
+ addService(serviceId, serviceInfo, subtype)
val probingInfo = setServiceProbing(serviceId)
assertNotNull(probingInfo)
return onProbingSucceeded(probingInfo)
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 b0a1f18..0d479b1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseDecoderTests.java
@@ -28,6 +28,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import android.net.Network;
@@ -333,7 +334,7 @@
final Network network = mock(Network.class);
responses = decoder.augmentResponses(parsedPacket,
/* existingResponses= */ Collections.emptyList(),
- /* interfaceIndex= */ 10, network /* expireOnExit= */);
+ /* interfaceIndex= */ 10, network /* expireOnExit= */).first;
assertEquals(responses.size(), 1);
assertEquals(responses.valueAt(0).getInterfaceIndex(), 10);
@@ -407,6 +408,31 @@
}
@Test
+ public void testDecodeWithIpv4AddressRemove() throws IOException {
+ MdnsResponse response = makeMdnsResponse(0, DATAIN_SERVICE_NAME_1, List.of(
+ new PacketAndRecordClass(DATAIN_PTR_1,
+ MdnsPointerRecord.class),
+ new PacketAndRecordClass(DATAIN_SERVICE_1,
+ MdnsServiceRecord.class),
+ new PacketAndRecordClass(DATAIN_IPV4_1,
+ MdnsInet4AddressRecord.class),
+ new PacketAndRecordClass(DATAIN_IPV4_2,
+ MdnsInet4AddressRecord.class)));
+ // Now update the response removing the second v4 address
+ final MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
+
+ final byte[] removedAddrResponse = makeResponsePacket(
+ List.of(DATAIN_PTR_1, DATAIN_SERVICE_1, DATAIN_IPV4_1));
+ final ArraySet<MdnsResponse> changes = decode(
+ decoder, removedAddrResponse, List.of(response));
+
+ assertEquals(1, changes.size());
+ assertEquals(1, changes.valueAt(0).getInet4AddressRecords().size());
+ assertEquals(parseNumericAddress("10.1.2.3"),
+ changes.valueAt(0).getInet4AddressRecords().get(0).getInet4Address());
+ }
+
+ @Test
public void testDecodeWithIpv6AddressChange() throws IOException {
MdnsResponse response = makeMdnsResponse(0, DATAIN_SERVICE_NAME_1, List.of(
new PacketAndRecordClass(DATAIN_PTR_1,
@@ -427,6 +453,29 @@
}
@Test
+ public void testDecodeWithIpv6AddressRemoved() throws IOException {
+ MdnsResponse response = makeMdnsResponse(0, DATAIN_SERVICE_NAME_1, List.of(
+ new PacketAndRecordClass(DATAIN_PTR_1,
+ MdnsPointerRecord.class),
+ new PacketAndRecordClass(DATAIN_SERVICE_1,
+ MdnsServiceRecord.class),
+ new PacketAndRecordClass(DATAIN_IPV6_1,
+ MdnsInet6AddressRecord.class),
+ new PacketAndRecordClass(DATAIN_IPV6_2,
+ MdnsInet6AddressRecord.class)));
+ // Now update the response adding an address
+ final MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
+ final byte[] removedAddrResponse = makeResponsePacket(
+ List.of(DATAIN_PTR_1, DATAIN_SERVICE_1, DATAIN_IPV6_1));
+ final ArraySet<MdnsResponse> updatedResponses = decode(
+ decoder, removedAddrResponse, List.of(response));
+ assertEquals(1, updatedResponses.size());
+ assertEquals(1, updatedResponses.valueAt(0).getInet6AddressRecords().size());
+ assertEquals(parseNumericAddress("aabb:ccdd:1122:3344:a0b0:c0d0:1020:3040"),
+ updatedResponses.valueAt(0).getInet6AddressRecords().get(0).getInet6Address());
+ }
+
+ @Test
public void testDecodeWithChangeOnText() throws IOException {
MdnsResponse response = makeMdnsResponse(0, DATAIN_SERVICE_NAME_1, List.of(
new PacketAndRecordClass(DATAIN_PTR_1,
@@ -493,11 +542,12 @@
new PacketAndRecordClass(DATAIN_IPV4_1, MdnsInet4AddressRecord.class),
new PacketAndRecordClass(DATAIN_IPV6_1, MdnsInet6AddressRecord.class),
new PacketAndRecordClass(DATAIN_PTR_1, MdnsPointerRecord.class),
- new PacketAndRecordClass(DATAIN_SERVICE_2, MdnsServiceRecord.class),
+ new PacketAndRecordClass(DATAIN_SERVICE_1, MdnsServiceRecord.class),
new PacketAndRecordClass(DATAIN_TEXT_1, MdnsTextRecord.class));
// Create a two identical responses.
- MdnsResponse response = makeMdnsResponse(0, DATAIN_SERVICE_NAME_1, recordList);
+ MdnsResponse response = makeMdnsResponse(12L /* time */, DATAIN_SERVICE_NAME_1, recordList);
+ doReturn(34L).when(mClock).elapsedRealtime();
final MdnsResponseDecoder decoder = new MdnsResponseDecoder(mClock, null);
final byte[] identicalResponse = makeResponsePacket(
recordList.stream().map(p -> p.packetData).collect(Collectors.toList()));
@@ -582,6 +632,6 @@
return decoder.augmentResponses(parsedPacket,
existingResponses,
- MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class));
+ MdnsSocket.INTERFACE_INDEX_UNSPECIFIED, mock(Network.class)).first;
}
}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
index 3f5e7a1..c10d9dd 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsResponseTests.java
@@ -95,24 +95,24 @@
}
}
- private MdnsResponse makeCompleteResponse(int recordsTtlMillis) {
+ private MdnsResponse makeCompleteResponse(int recordsTtlMillis, int receiptTimeMillis) {
final String[] hostname = new String[] { "MyHostname" };
final String[] serviceName = new String[] { "MyService", "_type", "_tcp", "local" };
final String[] serviceType = new String[] { "_type", "_tcp", "local" };
final MdnsResponse response = new MdnsResponse(/* now= */ 0, serviceName, INTERFACE_INDEX,
mNetwork);
- response.addPointerRecord(new MdnsPointerRecord(serviceType, 0L /* receiptTimeMillis */,
+ response.addPointerRecord(new MdnsPointerRecord(serviceType, receiptTimeMillis,
false /* cacheFlush */, recordsTtlMillis, serviceName));
- response.setServiceRecord(new MdnsServiceRecord(serviceName, 0L /* receiptTimeMillis */,
+ response.setServiceRecord(new MdnsServiceRecord(serviceName, receiptTimeMillis,
true /* cacheFlush */, recordsTtlMillis, 0 /* servicePriority */,
0 /* serviceWeight */, 0 /* servicePort */, hostname));
- response.setTextRecord(new MdnsTextRecord(serviceName, 0L /* receiptTimeMillis */,
+ response.setTextRecord(new MdnsTextRecord(serviceName, receiptTimeMillis,
true /* cacheFlush */, recordsTtlMillis, emptyList() /* entries */));
response.addInet4AddressRecord(new MdnsInetAddressRecord(
- hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ hostname, receiptTimeMillis, true /* cacheFlush */,
recordsTtlMillis, parseNumericAddress("192.0.2.123")));
response.addInet6AddressRecord(new MdnsInetAddressRecord(
- hostname, 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ hostname, receiptTimeMillis, true /* cacheFlush */,
recordsTtlMillis, parseNumericAddress("2001:db8::123")));
return response;
}
@@ -210,7 +210,7 @@
@Test
public void copyConstructor() {
- final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS);
+ final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS, 0 /* receiptTimeMillis */);
final MdnsResponse copy = new MdnsResponse(response);
assertEquals(response.getInet6AddressRecord(), copy.getInet6AddressRecord());
@@ -225,7 +225,7 @@
@Test
public void addRecords_noChange() {
- final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS);
+ final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS, 0 /* receiptTimeMillis */);
assertFalse(response.addPointerRecord(response.getPointerRecords().get(0)));
assertFalse(response.addInet6AddressRecord(response.getInet6AddressRecord()));
@@ -236,8 +236,8 @@
@Test
public void addRecords_ttlChange() {
- final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS);
- final MdnsResponse ttlZeroResponse = makeCompleteResponse(0);
+ final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS, 0 /* receiptTimeMillis */);
+ final MdnsResponse ttlZeroResponse = makeCompleteResponse(0, 0 /* receiptTimeMillis */);
assertTrue(response.addPointerRecord(ttlZeroResponse.getPointerRecords().get(0)));
assertEquals(1, response.getPointerRecords().size());
@@ -270,4 +270,44 @@
// All records were replaced, not added
assertEquals(ttlZeroResponse.getRecords().size(), response.getRecords().size());
}
+
+ @Test
+ public void addRecords_receiptTimeChange() {
+ final MdnsResponse response = makeCompleteResponse(TEST_TTL_MS, 0 /* receiptTimeMillis */);
+ final MdnsResponse receiptTimeChangedResponse = makeCompleteResponse(TEST_TTL_MS,
+ 1 /* receiptTimeMillis */);
+
+ assertFalse(
+ response.addPointerRecord(receiptTimeChangedResponse.getPointerRecords().get(0)));
+ assertEquals(1, response.getPointerRecords().get(0).getReceiptTime());
+ assertTrue(response.getRecords().stream().anyMatch(r ->
+ r == response.getPointerRecords().get(0)));
+
+ assertFalse(
+ response.addInet6AddressRecord(receiptTimeChangedResponse.getInet6AddressRecord()));
+ assertEquals(1, response.getInet6AddressRecords().size());
+ assertEquals(1, response.getInet6AddressRecord().getReceiptTime());
+ assertTrue(response.getRecords().stream().anyMatch(r ->
+ r == response.getInet6AddressRecord()));
+
+ assertFalse(
+ response.addInet4AddressRecord(receiptTimeChangedResponse.getInet4AddressRecord()));
+ assertEquals(1, response.getInet4AddressRecords().size());
+ assertEquals(1, response.getInet4AddressRecord().getReceiptTime());
+ assertTrue(response.getRecords().stream().anyMatch(r ->
+ r == response.getInet4AddressRecord()));
+
+ assertFalse(response.setServiceRecord(receiptTimeChangedResponse.getServiceRecord()));
+ assertEquals(1, response.getServiceRecord().getReceiptTime());
+ assertTrue(response.getRecords().stream().anyMatch(r ->
+ r == response.getServiceRecord()));
+
+ assertFalse(response.setTextRecord(receiptTimeChangedResponse.getTextRecord()));
+ assertEquals(1, response.getTextRecord().getReceiptTime());
+ assertTrue(response.getRecords().stream().anyMatch(r ->
+ r == response.getTextRecord()));
+
+ // All records were replaced, not added
+ assertEquals(receiptTimeChangedResponse.getRecords().size(), response.getRecords().size());
+ }
}
\ No newline at end of file
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 34b44fc..a61e8b2 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -21,9 +21,11 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
@@ -41,6 +43,7 @@
import android.net.Network;
import android.text.TextUtils;
+import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.SharedLog;
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
@@ -52,6 +55,7 @@
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;
@@ -87,6 +91,7 @@
private static final long TEST_TTL = 120000L;
private static final long TEST_ELAPSED_REALTIME = 123L;
+ private static final long TEST_TIMEOUT_MS = 10_000L;
@Mock
private MdnsServiceBrowserListener mockListenerOne;
@@ -422,6 +427,34 @@
assertNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
}
+ @Test
+ public void testQueryScheduledWhenAnsweredFromCache() {
+ final MdnsSearchOptions searchOptions = MdnsSearchOptions.getDefaultOptions();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
+ assertNotNull(currentThreadExecutor.getAndClearSubmittedRunnable());
+
+ client.processResponse(createResponse(
+ "service-instance-1", "192.0.2.123", 5353,
+ SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), /* interfaceIndex= */ 20, mockNetwork);
+
+ verify(mockListenerOne).onServiceNameDiscovered(any());
+ verify(mockListenerOne).onServiceFound(any());
+
+ // File another identical query
+ client.startSendAndReceive(mockListenerTwo, searchOptions);
+
+ verify(mockListenerTwo).onServiceNameDiscovered(any());
+ verify(mockListenerTwo).onServiceFound(any());
+
+ // This time no query is submitted, only scheduled
+ assertNull(currentThreadExecutor.getAndClearSubmittedRunnable());
+ assertNotNull(currentThreadExecutor.getAndClearLastScheduledRunnable());
+ // This just skips the first query of the first burst
+ assertEquals(MdnsConfigs.timeBetweenQueriesInBurstMs(),
+ currentThreadExecutor.getAndClearLastScheduledDelayInMs());
+ }
+
private static void verifyServiceInfo(MdnsServiceInfo serviceInfo, String serviceName,
String[] serviceType, List<String> ipv4Addresses, List<String> ipv6Addresses, int port,
List<String> subTypes, Map<String, String> attributes, int interfaceIndex,
@@ -673,34 +706,8 @@
}
@Test
- public void processResponse_notAllowRemoveSearch_shouldNotRemove() throws Exception {
- final String serviceInstanceName = "service-instance-1";
- client.startSendAndReceive(
- mockListenerOne,
- MdnsSearchOptions.newBuilder().build());
- Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
-
- // Process the initial response.
- client.processResponse(createResponse(
- serviceInstanceName, "192.168.1.1", 5353, /* subtype= */ "ABCDE",
- Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
-
- // Clear the scheduled runnable.
- currentThreadExecutor.getAndClearLastScheduledRunnable();
-
- // Simulate the case where the response is after TTL.
- doReturn(TEST_ELAPSED_REALTIME + TEST_TTL + 1L).when(mockDecoderClock).elapsedRealtime();
- firstMdnsTask.run();
-
- // Verify removed callback was not called.
- verifyServiceRemovedNoCallback(mockListenerOne);
- }
-
- @Test
- @Ignore("MdnsConfigs is not configurable currently.")
- public void processResponse_allowSearchOptionsToRemoveExpiredService_shouldRemove()
+ public void processResponse_searchOptionsEnableServiceRemoval_shouldRemove()
throws Exception {
- //MdnsConfigsFlagsImpl.allowSearchOptionsToRemoveExpiredService.override(true);
final String serviceInstanceName = "service-instance-1";
client =
new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
@@ -710,7 +717,9 @@
return mockPacketWriter;
}
};
- client.startSendAndReceive(mockListenerOne, MdnsSearchOptions.getDefaultOptions());
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder().setRemoveExpiredService(
+ true).build();
+ client.startSendAndReceive(mockListenerOne, searchOptions);
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
// Process the initial response.
@@ -920,19 +929,15 @@
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
// Send twice for IPv4 and IPv6
inOrder.verify(mockSocketClient, times(2)).sendUnicastPacket(srvTxtQueryCaptor.capture(),
- eq(null) /* network */);
+ eq(mockNetwork));
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
- final List<MdnsRecord> srvTxtQuestions = srvTxtQueryPacket.questions;
- final String[] serviceName = Stream.concat(Stream.of(instanceName),
- Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
- assertFalse(srvTxtQuestions.stream().anyMatch(q -> q.getType() == MdnsRecord.TYPE_PTR));
- assertTrue(srvTxtQuestions.stream().anyMatch(q ->
- q.getType() == MdnsRecord.TYPE_SRV && Arrays.equals(q.name, serviceName)));
- assertTrue(srvTxtQuestions.stream().anyMatch(q ->
- q.getType() == MdnsRecord.TYPE_TXT && Arrays.equals(q.name, serviceName)));
+ final String[] serviceName = getTestServiceName(instanceName);
+ assertFalse(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_PTR));
+ assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_SRV, serviceName));
+ assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_TXT, serviceName));
// Process a response with SRV+TXT
final MdnsPacket srvTxtResponse = new MdnsPacket(
@@ -955,15 +960,12 @@
ArgumentCaptor.forClass(DatagramPacket.class);
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
inOrder.verify(mockSocketClient, times(2)).sendMulticastPacket(addressQueryCaptor.capture(),
- eq(null) /* network */);
+ eq(mockNetwork));
final MdnsPacket addressQueryPacket = MdnsPacket.parse(
new MdnsPacketReader(addressQueryCaptor.getValue()));
- final List<MdnsRecord> addressQueryQuestions = addressQueryPacket.questions;
- assertTrue(addressQueryQuestions.stream().anyMatch(q ->
- q.getType() == MdnsRecord.TYPE_A && Arrays.equals(q.name, hostname)));
- assertTrue(addressQueryQuestions.stream().anyMatch(q ->
- q.getType() == MdnsRecord.TYPE_AAAA && Arrays.equals(q.name, hostname)));
+ assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_A, hostname));
+ assertTrue(hasQuestion(addressQueryPacket, MdnsRecord.TYPE_AAAA, hostname));
// Process a response with address records
final MdnsPacket addressResponse = new MdnsPacket(
@@ -996,6 +998,110 @@
}
@Test
+ public void testRenewTxtSrvInResolve() throws Exception {
+ client = new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
+ mockDecoderClock, mockNetwork, mockSharedLog);
+
+ final String instanceName = "service-instance";
+ final String[] hostname = new String[] { "testhost "};
+ final String ipV4Address = "192.0.2.0";
+ final String ipV6Address = "2001:db8::";
+
+ final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+ .setResolveInstanceName(instanceName).build();
+
+ client.startSendAndReceive(mockListenerOne, resolveOptions);
+ InOrder inOrder = inOrder(mockListenerOne, mockSocketClient);
+
+ // Get the query for SRV/TXT
+ final ArgumentCaptor<DatagramPacket> srvTxtQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ // Send twice for IPv4 and IPv6
+ inOrder.verify(mockSocketClient, times(2)).sendUnicastPacket(srvTxtQueryCaptor.capture(),
+ eq(mockNetwork));
+
+ final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
+ new MdnsPacketReader(srvTxtQueryCaptor.getValue()));
+
+ final String[] serviceName = getTestServiceName(instanceName);
+ assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_SRV, serviceName));
+ assertTrue(hasQuestion(srvTxtQueryPacket, MdnsRecord.TYPE_TXT, serviceName));
+
+ // Process a response with all records
+ final MdnsPacket srvTxtResponse = new MdnsPacket(
+ 0 /* flags */,
+ Collections.emptyList() /* questions */,
+ List.of(
+ new MdnsServiceRecord(serviceName, TEST_ELAPSED_REALTIME,
+ true /* cacheFlush */, TEST_TTL, 0 /* servicePriority */,
+ 0 /* serviceWeight */, 1234 /* servicePort */, hostname),
+ new MdnsTextRecord(serviceName, TEST_ELAPSED_REALTIME,
+ true /* cacheFlush */, TEST_TTL,
+ Collections.emptyList() /* entries */),
+ new MdnsInetAddressRecord(hostname, TEST_ELAPSED_REALTIME,
+ true /* cacheFlush */, TEST_TTL,
+ InetAddresses.parseNumericAddress(ipV4Address)),
+ new MdnsInetAddressRecord(hostname, TEST_ELAPSED_REALTIME,
+ true /* cacheFlush */, TEST_TTL,
+ InetAddresses.parseNumericAddress(ipV6Address))),
+ Collections.emptyList() /* authorityRecords */,
+ Collections.emptyList() /* additionalRecords */);
+ client.processResponse(srvTxtResponse, INTERFACE_INDEX, mockNetwork);
+ inOrder.verify(mockListenerOne).onServiceNameDiscovered(any());
+ inOrder.verify(mockListenerOne).onServiceFound(any());
+
+ // Expect no query on the next run
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ inOrder.verifyNoMoreInteractions();
+
+ // Advance time so 75% of TTL passes and re-execute
+ doReturn(TEST_ELAPSED_REALTIME + (long) (TEST_TTL * 0.75))
+ .when(mockDecoderClock).elapsedRealtime();
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+
+ // Expect a renewal query
+ final ArgumentCaptor<DatagramPacket> renewalQueryCaptor =
+ ArgumentCaptor.forClass(DatagramPacket.class);
+ // Second and later sends are sent as "expect multicast response" queries
+ inOrder.verify(mockSocketClient, times(2)).sendMulticastPacket(renewalQueryCaptor.capture(),
+ eq(mockNetwork));
+ inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
+ final MdnsPacket renewalPacket = MdnsPacket.parse(
+ new MdnsPacketReader(renewalQueryCaptor.getValue()));
+ assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_SRV, serviceName));
+ assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_TXT, serviceName));
+ inOrder.verifyNoMoreInteractions();
+
+ long updatedReceiptTime = TEST_ELAPSED_REALTIME + TEST_TTL;
+ final MdnsPacket refreshedSrvTxtResponse = new MdnsPacket(
+ 0 /* flags */,
+ Collections.emptyList() /* questions */,
+ List.of(
+ new MdnsServiceRecord(serviceName, updatedReceiptTime,
+ true /* cacheFlush */, TEST_TTL, 0 /* servicePriority */,
+ 0 /* serviceWeight */, 1234 /* servicePort */, hostname),
+ new MdnsTextRecord(serviceName, updatedReceiptTime,
+ true /* cacheFlush */, TEST_TTL,
+ Collections.emptyList() /* entries */),
+ new MdnsInetAddressRecord(hostname, updatedReceiptTime,
+ true /* cacheFlush */, TEST_TTL,
+ InetAddresses.parseNumericAddress(ipV4Address)),
+ new MdnsInetAddressRecord(hostname, updatedReceiptTime,
+ true /* cacheFlush */, TEST_TTL,
+ InetAddresses.parseNumericAddress(ipV6Address))),
+ Collections.emptyList() /* authorityRecords */,
+ Collections.emptyList() /* additionalRecords */);
+ client.processResponse(refreshedSrvTxtResponse, INTERFACE_INDEX, mockNetwork);
+
+ // Advance time to updatedReceiptTime + 1, expected no refresh query because the cache
+ // should contain the record that have update last receipt time.
+ doReturn(updatedReceiptTime + 1).when(mockDecoderClock).elapsedRealtime();
+ currentThreadExecutor.getAndClearLastScheduledRunnable().run();
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
public void testProcessResponse_ResolveExcludesOtherServices() {
client = new MdnsServiceTypeClient(
SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
@@ -1056,6 +1162,154 @@
inOrder.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
}
+ @Test
+ public void testProcessResponse_SubtypeDiscoveryLimitedToSubtype() {
+ client = new MdnsServiceTypeClient(
+ SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
+
+ final String matchingInstance = "instance1";
+ final String subtype = "_subtype";
+ final String otherInstance = "instance2";
+ final String ipV4Address = "192.0.2.0";
+ final String ipV6Address = "2001:db8::";
+
+ final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
+ // Search with different case. Note MdnsSearchOptions subtype doesn't start with "_"
+ .addSubtype("Subtype").build();
+
+ client.startSendAndReceive(mockListenerOne, options);
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ // Complete response from instanceName
+ final MdnsPacket packetWithoutSubtype = createResponse(
+ matchingInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL);
+ final MdnsPointerRecord originalPtr = (MdnsPointerRecord) CollectionUtils.findFirst(
+ packetWithoutSubtype.answers, r -> r instanceof MdnsPointerRecord);
+
+ // Add a subtype PTR record
+ final ArrayList<MdnsRecord> newAnswers = new ArrayList<>(packetWithoutSubtype.answers);
+ newAnswers.add(new MdnsPointerRecord(
+ // PTR should be _subtype._sub._type._tcp.local -> instance1._type._tcp.local
+ Stream.concat(Stream.of(subtype, "_sub"), Arrays.stream(SERVICE_TYPE_LABELS))
+ .toArray(String[]::new),
+ originalPtr.getReceiptTime(), originalPtr.getCacheFlush(), originalPtr.getTtl(),
+ originalPtr.getPointer()));
+ final MdnsPacket packetWithSubtype = new MdnsPacket(
+ packetWithoutSubtype.flags,
+ packetWithoutSubtype.questions,
+ newAnswers,
+ packetWithoutSubtype.authorityRecords,
+ packetWithoutSubtype.additionalRecords);
+ client.processResponse(packetWithSubtype, INTERFACE_INDEX, mockNetwork);
+
+ // Complete response from otherInstanceName, without subtype
+ client.processResponse(createResponse(
+ otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL),
+ INTERFACE_INDEX, mockNetwork);
+
+ // Address update from otherInstanceName
+ client.processResponse(createResponse(
+ otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+
+ // Goodbye from otherInstanceName
+ client.processResponse(createResponse(
+ otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap(), 0L /* ttl */), INTERFACE_INDEX, mockNetwork);
+
+ // mockListenerOne gets notified for the requested instance
+ final ArgumentMatcher<MdnsServiceInfo> subtypeInstanceMatcher = info ->
+ info.getServiceInstanceName().equals(matchingInstance)
+ && info.getSubtypes().equals(Collections.singletonList(subtype));
+ verify(mockListenerOne).onServiceNameDiscovered(argThat(subtypeInstanceMatcher));
+ verify(mockListenerOne).onServiceFound(argThat(subtypeInstanceMatcher));
+
+ // ...but does not get any callback for the other instance
+ verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceUpdated(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
+
+ // mockListenerTwo gets notified for both though
+ final InOrder inOrder = inOrder(mockListenerTwo);
+ inOrder.verify(mockListenerTwo).onServiceNameDiscovered(argThat(subtypeInstanceMatcher));
+ inOrder.verify(mockListenerTwo).onServiceFound(argThat(subtypeInstanceMatcher));
+
+ inOrder.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
+ inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
+ inOrder.verify(mockListenerTwo).onServiceUpdated(matchServiceName(otherInstance));
+ inOrder.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
+ }
+
+ @Test
+ public void testNotifySocketDestroyed() throws Exception {
+ client = new MdnsServiceTypeClient(
+ SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork, mockSharedLog);
+
+ final String requestedInstance = "instance1";
+ final String otherInstance = "instance2";
+ final String ipV4Address = "192.0.2.0";
+
+ final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+ // Use different case in the options
+ .setResolveInstanceName("Instance1").build();
+
+ client.startSendAndReceive(mockListenerOne, resolveOptions);
+ // Ensure the first task is executed so it schedules a future task
+ currentThreadExecutor.getAndClearSubmittedFuture().get(
+ TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+ // Filing the second request cancels the first future
+ verify(expectedSendFutures[0]).cancel(true);
+
+ // Ensure it gets executed too
+ currentThreadExecutor.getAndClearSubmittedFuture().get(
+ TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+ // Complete response from instanceName
+ client.processResponse(createResponse(
+ requestedInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL),
+ INTERFACE_INDEX, mockNetwork);
+
+ // Complete response from otherInstanceName
+ client.processResponse(createResponse(
+ otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+ Collections.emptyMap() /* textAttributes */, TEST_TTL),
+ INTERFACE_INDEX, mockNetwork);
+
+ verify(expectedSendFutures[1], never()).cancel(true);
+ client.notifySocketDestroyed();
+ verify(expectedSendFutures[1]).cancel(true);
+
+ // mockListenerOne gets notified for the requested instance
+ final InOrder inOrder1 = inOrder(mockListenerOne);
+ inOrder1.verify(mockListenerOne).onServiceNameDiscovered(
+ matchServiceName(requestedInstance));
+ inOrder1.verify(mockListenerOne).onServiceFound(matchServiceName(requestedInstance));
+ inOrder1.verify(mockListenerOne).onServiceRemoved(matchServiceName(requestedInstance));
+ inOrder1.verify(mockListenerOne).onServiceNameRemoved(matchServiceName(requestedInstance));
+ verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
+ verify(mockListenerOne, never()).onServiceNameRemoved(matchServiceName(otherInstance));
+
+ // mockListenerTwo gets notified for both though
+ final InOrder inOrder2 = inOrder(mockListenerTwo);
+ inOrder2.verify(mockListenerTwo).onServiceNameDiscovered(
+ matchServiceName(requestedInstance));
+ inOrder2.verify(mockListenerTwo).onServiceFound(matchServiceName(requestedInstance));
+ inOrder2.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
+ inOrder2.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
+ inOrder2.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
+ inOrder2.verify(mockListenerTwo).onServiceNameRemoved(matchServiceName(otherInstance));
+ inOrder2.verify(mockListenerTwo).onServiceRemoved(matchServiceName(requestedInstance));
+ inOrder2.verify(mockListenerTwo).onServiceNameRemoved(matchServiceName(requestedInstance));
+ }
+
private static MdnsServiceInfo matchServiceName(String name) {
return argThat(info -> info.getServiceInstanceName().equals(name));
}
@@ -1073,27 +1327,42 @@
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
if (expectsUnicastResponse) {
verify(mockSocketClient).sendUnicastPacket(
- expectedIPv4Packets[index], null /* network */);
+ expectedIPv4Packets[index], mockNetwork);
if (multipleSocketDiscovery) {
verify(mockSocketClient).sendUnicastPacket(
- expectedIPv6Packets[index], null /* network */);
+ expectedIPv6Packets[index], mockNetwork);
}
} else {
verify(mockSocketClient).sendMulticastPacket(
- expectedIPv4Packets[index], null /* network */);
+ expectedIPv4Packets[index], mockNetwork);
if (multipleSocketDiscovery) {
verify(mockSocketClient).sendMulticastPacket(
- expectedIPv6Packets[index], null /* network */);
+ expectedIPv6Packets[index], mockNetwork);
}
}
}
+ private static String[] getTestServiceName(String instanceName) {
+ return Stream.concat(Stream.of(instanceName),
+ Arrays.stream(SERVICE_TYPE_LABELS)).toArray(String[]::new);
+ }
+
+ private static boolean hasQuestion(MdnsPacket packet, int type) {
+ return hasQuestion(packet, type, null);
+ }
+
+ private static boolean hasQuestion(MdnsPacket packet, int type, @Nullable String[] name) {
+ return packet.questions.stream().anyMatch(q -> q.getType() == type
+ && (name == null || Arrays.equals(q.name, name)));
+ }
+
// A fake ScheduledExecutorService that keeps tracking the last scheduled Runnable and its delay
// time.
private class FakeExecutor extends ScheduledThreadPoolExecutor {
private long lastScheduledDelayInMs;
private Runnable lastScheduledRunnable;
private Runnable lastSubmittedRunnable;
+ private Future<?> lastSubmittedFuture;
private int futureIndex;
FakeExecutor() {
@@ -1105,6 +1374,7 @@
public Future<?> submit(Runnable command) {
Future<?> future = super.submit(command);
lastSubmittedRunnable = command;
+ lastSubmittedFuture = future;
return future;
}
@@ -1136,6 +1406,12 @@
lastSubmittedRunnable = null;
return val;
}
+
+ Future<?> getAndClearSubmittedFuture() {
+ Future<?> val = lastSubmittedFuture;
+ lastSubmittedFuture = null;
+ return val;
+ }
}
private MdnsPacket createResponse(
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
index 6f3322b..4f56857 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -31,6 +31,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
@@ -40,7 +41,9 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.LinkAddress;
@@ -49,6 +52,9 @@
import android.net.NetworkCapabilities;
import android.net.TetheringManager;
import android.net.TetheringManager.TetheringEventCallback;
+import android.net.wifi.p2p.WifiP2pGroup;
+import android.net.wifi.p2p.WifiP2pInfo;
+import android.net.wifi.p2p.WifiP2pManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
@@ -88,6 +94,7 @@
private static final String TAG = MdnsSocketProviderTest.class.getSimpleName();
private static final String TEST_IFACE_NAME = "test";
private static final String LOCAL_ONLY_IFACE_NAME = "local_only";
+ private static final String WIFI_P2P_IFACE_NAME = "p2p_wifi";
private static final String TETHERED_IFACE_NAME = "tethered";
private static final int TETHERED_IFACE_IDX = 32;
private static final long DEFAULT_TIMEOUT = 2000L;
@@ -111,6 +118,7 @@
private MdnsSocketProvider mSocketProvider;
private NetworkCallback mNetworkCallback;
private TetheringEventCallback mTetheringEventCallback;
+ private SharedLog mLog = new SharedLog("MdnsSocketProviderTest");
private TestNetlinkMonitor mTestSocketNetLinkMonitor;
@Before
@@ -135,11 +143,15 @@
doReturn(true).when(mTetheredIfaceWrapper).supportsMulticast();
doReturn(mLocalOnlyIfaceWrapper).when(mDeps)
.getNetworkInterfaceByName(LOCAL_ONLY_IFACE_NAME);
+ doReturn(mLocalOnlyIfaceWrapper).when(mDeps)
+ .getNetworkInterfaceByName(WIFI_P2P_IFACE_NAME);
doReturn(mTetheredIfaceWrapper).when(mDeps).getNetworkInterfaceByName(TETHERED_IFACE_NAME);
doReturn(mock(MdnsInterfaceSocket.class))
.when(mDeps).createMdnsInterfaceSocket(any(), anyInt(), any(), any());
doReturn(TETHERED_IFACE_IDX).when(mDeps).getNetworkInterfaceIndexByName(
TETHERED_IFACE_NAME);
+ doReturn(789).when(mDeps).getNetworkInterfaceIndexByName(
+ WIFI_P2P_IFACE_NAME);
final HandlerThread thread = new HandlerThread("MdnsSocketProviderTest");
thread.start();
mHandler = new Handler(thread.getLooper());
@@ -153,7 +165,28 @@
return mTestSocketNetLinkMonitor;
}).when(mDeps).createSocketNetlinkMonitor(any(), any(),
any());
- mSocketProvider = new MdnsSocketProvider(mContext, thread.getLooper(), mDeps);
+ mSocketProvider = new MdnsSocketProvider(mContext, thread.getLooper(), mDeps, mLog);
+ }
+
+ private void runOnHandler(Runnable r) {
+ mHandler.post(r);
+ HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ }
+
+ private BroadcastReceiver expectWifiP2PChangeBroadcastReceiver() {
+ final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ verify(mContext, times(1)).registerReceiver(receiverCaptor.capture(),
+ argThat(filter -> filter.hasAction(
+ WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)),
+ any(), any());
+ final BroadcastReceiver originalReceiver = receiverCaptor.getValue();
+ return new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ runOnHandler(() -> originalReceiver.onReceive(context, intent));
+ }
+ };
}
private void startMonitoringSockets() {
@@ -162,16 +195,14 @@
final ArgumentCaptor<TetheringEventCallback> teCallbackCaptor =
ArgumentCaptor.forClass(TetheringEventCallback.class);
- mHandler.post(mSocketProvider::startMonitoringSockets);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::startMonitoringSockets);
verify(mCm).registerNetworkCallback(any(), nwCallbackCaptor.capture(), any());
verify(mTm).registerTetheringEventCallback(any(), teCallbackCaptor.capture());
mNetworkCallback = nwCallbackCaptor.getValue();
mTetheringEventCallback = teCallbackCaptor.getValue();
- mHandler.post(mSocketProvider::startNetLinkMonitor);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::startNetLinkMonitor);
}
private static class TestNetlinkMonitor extends SocketNetlinkMonitor {
@@ -280,9 +311,8 @@
testLp.setInterfaceName(TEST_IFACE_NAME);
testLp.setLinkAddresses(List.of(LINKADDRV4));
final NetworkCapabilities testNc = makeCapabilities(transports);
- mHandler.post(() -> mNetworkCallback.onCapabilitiesChanged(TEST_NETWORK, testNc));
- mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, testLp));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mNetworkCallback.onCapabilitiesChanged(TEST_NETWORK, testNc));
+ runOnHandler(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, testLp));
}
@Test
@@ -290,66 +320,57 @@
startMonitoringSockets();
final TestSocketCallback testCallback1 = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback1));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback1));
testCallback1.expectedNoCallback();
postNetworkAvailable(TRANSPORT_WIFI);
testCallback1.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
final TestSocketCallback testCallback2 = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback2));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback2));
testCallback1.expectedNoCallback();
testCallback2.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
final TestSocketCallback testCallback3 = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(null /* network */, testCallback3));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallback3));
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
testCallback3.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
- mHandler.post(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(
+ runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(
List.of(LOCAL_ONLY_IFACE_NAME)));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mLocalOnlyIfaceWrapper).getNetworkInterface();
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
testCallback3.expectedSocketCreatedForNetwork(null /* network */, List.of());
- mHandler.post(() -> mTetheringEventCallback.onTetheredInterfacesChanged(
+ runOnHandler(() -> mTetheringEventCallback.onTetheredInterfacesChanged(
List.of(TETHERED_IFACE_NAME)));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mTetheredIfaceWrapper).getNetworkInterface();
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
testCallback3.expectedSocketCreatedForNetwork(null /* network */, List.of());
- mHandler.post(() -> mSocketProvider.unrequestSocket(testCallback1));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.unrequestSocket(testCallback1));
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
testCallback3.expectedNoCallback();
- mHandler.post(() -> mNetworkCallback.onLost(TEST_NETWORK));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mNetworkCallback.onLost(TEST_NETWORK));
testCallback1.expectedNoCallback();
testCallback2.expectedInterfaceDestroyedForNetwork(TEST_NETWORK);
testCallback3.expectedInterfaceDestroyedForNetwork(TEST_NETWORK);
- mHandler.post(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(List.of()));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(List.of()));
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
testCallback3.expectedInterfaceDestroyedForNetwork(null /* network */);
- mHandler.post(() -> mSocketProvider.unrequestSocket(testCallback3));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.unrequestSocket(testCallback3));
testCallback1.expectedNoCallback();
testCallback2.expectedNoCallback();
- // Expect the socket destroy for tethered interface.
- testCallback3.expectedInterfaceDestroyedForNetwork(null /* network */);
+ // There was still a tethered interface, but no callback should be sent once unregistered
+ testCallback3.expectedNoCallback();
}
private RtNetlinkAddressMessage createNetworkAddressUpdateNetLink(
@@ -375,8 +396,7 @@
public void testDownstreamNetworkAddressUpdateFromNetlink() {
startMonitoringSockets();
final TestSocketCallback testCallbackAll = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(null /* network */, testCallbackAll));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallbackAll));
// Address add message arrived before the interface is created.
RtNetlinkAddressMessage addIpv4AddrMsg = createNetworkAddressUpdateNetLink(
@@ -384,15 +404,13 @@
LINKADDRV4,
TETHERED_IFACE_IDX,
0 /* flags */);
- mHandler.post(
+ runOnHandler(
() -> mTestSocketNetLinkMonitor.processNetlinkMessage(addIpv4AddrMsg,
0 /* whenMs */));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
// Interface is created.
- mHandler.post(() -> mTetheringEventCallback.onTetheredInterfacesChanged(
+ runOnHandler(() -> mTetheringEventCallback.onTetheredInterfacesChanged(
List.of(TETHERED_IFACE_NAME)));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
verify(mTetheredIfaceWrapper).getNetworkInterface();
testCallbackAll.expectedSocketCreatedForNetwork(null /* network */, List.of(LINKADDRV4));
@@ -402,10 +420,9 @@
LINKADDRV4,
TETHERED_IFACE_IDX,
0 /* flags */);
- mHandler.post(
+ runOnHandler(
() -> mTestSocketNetLinkMonitor.processNetlinkMessage(removeIpv4AddrMsg,
0 /* whenMs */));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
testCallbackAll.expectedAddressesChangedForNetwork(null /* network */, List.of());
// New address added.
@@ -414,9 +431,8 @@
LINKADDRV6,
TETHERED_IFACE_IDX,
0 /* flags */);
- mHandler.post(() -> mTestSocketNetLinkMonitor.processNetlinkMessage(addIpv6AddrMsg,
+ runOnHandler(() -> mTestSocketNetLinkMonitor.processNetlinkMessage(addIpv6AddrMsg,
0 /* whenMs */));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
testCallbackAll.expectedAddressesChangedForNetwork(null /* network */, List.of(LINKADDRV6));
// Address updated
@@ -425,10 +441,9 @@
LINKADDRV6,
TETHERED_IFACE_IDX,
1 /* flags */);
- mHandler.post(
+ runOnHandler(
() -> mTestSocketNetLinkMonitor.processNetlinkMessage(updateIpv6AddrMsg,
0 /* whenMs */));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
testCallbackAll.expectedAddressesChangedForNetwork(null /* network */,
List.of(LINKADDRV6_FLAG_CHANGE));
}
@@ -438,8 +453,7 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
testCallback.expectedNoCallback();
postNetworkAvailable(TRANSPORT_WIFI);
@@ -448,8 +462,7 @@
final LinkProperties newTestLp = new LinkProperties();
newTestLp.setInterfaceName(TEST_IFACE_NAME);
newTestLp.setLinkAddresses(List.of(LINKADDRV4, LINKADDRV6));
- mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, newTestLp));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, newTestLp));
testCallback.expectedAddressesChangedForNetwork(
TEST_NETWORK, List.of(LINKADDRV4, LINKADDRV6));
}
@@ -457,8 +470,7 @@
@Test
public void testStartAndStopMonitoringSockets() {
// Stop monitoring sockets before start. Should not unregister any network callback.
- mHandler.post(mSocketProvider::requestStopWhenInactive);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::requestStopWhenInactive);
verify(mCm, never()).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, never()).unregisterTetheringEventCallback(any(TetheringEventCallback.class));
@@ -466,39 +478,32 @@
startMonitoringSockets();
// Request a socket then unrequest it. Expect no network callback unregistration.
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
testCallback.expectedNoCallback();
- mHandler.post(()-> mSocketProvider.unrequestSocket(testCallback));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(()-> mSocketProvider.unrequestSocket(testCallback));
verify(mCm, never()).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, never()).unregisterTetheringEventCallback(any(TetheringEventCallback.class));
// Request stop and it should unregister network callback immediately because there is no
// socket request.
- mHandler.post(mSocketProvider::requestStopWhenInactive);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::requestStopWhenInactive);
verify(mCm, times(1)).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, times(1)).unregisterTetheringEventCallback(any(TetheringEventCallback.class));
// Start sockets monitoring and request a socket again.
- mHandler.post(mSocketProvider::startMonitoringSockets);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::startMonitoringSockets);
verify(mCm, times(2)).registerNetworkCallback(any(), any(NetworkCallback.class), any());
verify(mTm, times(2)).registerTetheringEventCallback(
any(), any(TetheringEventCallback.class));
final TestSocketCallback testCallback2 = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback2));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback2));
testCallback2.expectedNoCallback();
// Try to stop monitoring sockets but should be ignored and wait until all socket are
// unrequested.
- mHandler.post(mSocketProvider::requestStopWhenInactive);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::requestStopWhenInactive);
verify(mCm, times(1)).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, times(1)).unregisterTetheringEventCallback(any());
// Unrequest the socket then network callbacks should be unregistered.
- mHandler.post(()-> mSocketProvider.unrequestSocket(testCallback2));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(()-> mSocketProvider.unrequestSocket(testCallback2));
verify(mCm, times(2)).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, times(2)).unregisterTetheringEventCallback(any(TetheringEventCallback.class));
}
@@ -509,37 +514,32 @@
// Request a socket with null network.
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(null, testCallback));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(null, testCallback));
testCallback.expectedNoCallback();
// Notify a LinkPropertiesChanged with TEST_NETWORK.
final LinkProperties testLp = new LinkProperties();
testLp.setInterfaceName(TEST_IFACE_NAME);
testLp.setLinkAddresses(List.of(LINKADDRV4));
- mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, testLp));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mNetworkCallback.onLinkPropertiesChanged(TEST_NETWORK, testLp));
verify(mTestNetworkIfaceWrapper, times(1)).getNetworkInterface();
testCallback.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
// Try to stop monitoring and unrequest the socket.
- mHandler.post(mSocketProvider::requestStopWhenInactive);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
- mHandler.post(()-> mSocketProvider.unrequestSocket(testCallback));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
- testCallback.expectedInterfaceDestroyedForNetwork(TEST_NETWORK);
+ runOnHandler(mSocketProvider::requestStopWhenInactive);
+ runOnHandler(()-> mSocketProvider.unrequestSocket(testCallback));
+ // No callback sent when unregistered
+ testCallback.expectedNoCallback();
verify(mCm, times(1)).unregisterNetworkCallback(any(NetworkCallback.class));
verify(mTm, times(1)).unregisterTetheringEventCallback(any());
// Start sockets monitoring and request a socket again. Expected no socket created callback
// because all saved LinkProperties has been cleared.
- mHandler.post(mSocketProvider::startMonitoringSockets);
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(mSocketProvider::startMonitoringSockets);
verify(mCm, times(2)).registerNetworkCallback(any(), any(NetworkCallback.class), any());
verify(mTm, times(2)).registerTetheringEventCallback(
any(), any(TetheringEventCallback.class));
- mHandler.post(() -> mSocketProvider.requestSocket(null, testCallback));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mSocketProvider.requestSocket(null, testCallback));
testCallback.expectedNoCallback();
// Notify a LinkPropertiesChanged with another network.
@@ -548,8 +548,7 @@
final Network otherNetwork = new Network(456);
otherLp.setInterfaceName("test2");
otherLp.setLinkAddresses(List.of(otherAddress));
- mHandler.post(() -> mNetworkCallback.onLinkPropertiesChanged(otherNetwork, otherLp));
- HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
+ runOnHandler(() -> mNetworkCallback.onLinkPropertiesChanged(otherNetwork, otherLp));
verify(mTestNetworkIfaceWrapper, times(2)).getNetworkInterface();
testCallback.expectedSocketCreatedForNetwork(otherNetwork, List.of(otherAddress));
}
@@ -559,7 +558,7 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
postNetworkAvailable(TRANSPORT_CELLULAR);
testCallback.expectedNoCallback();
@@ -571,7 +570,7 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
postNetworkAvailable(TRANSPORT_BLUETOOTH);
testCallback.expectedNoCallback();
@@ -583,7 +582,7 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
postNetworkAvailable(TRANSPORT_BLUETOOTH);
testCallback.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
@@ -595,7 +594,7 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
postNetworkAvailable(TRANSPORT_BLUETOOTH);
testCallback.expectedNoCallback();
@@ -609,7 +608,7 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
postNetworkAvailable(TRANSPORT_VPN, TRANSPORT_WIFI);
testCallback.expectedNoCallback();
@@ -621,9 +620,146 @@
startMonitoringSockets();
final TestSocketCallback testCallback = new TestSocketCallback();
- mHandler.post(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
+ runOnHandler(() -> mSocketProvider.requestSocket(TEST_NETWORK, testCallback));
postNetworkAvailable(TRANSPORT_WIFI);
testCallback.expectedSocketCreatedForNetwork(TEST_NETWORK, List.of(LINKADDRV4));
}
+
+ private Intent buildWifiP2PConnectionChangedIntent(boolean groupFormed) {
+ final Intent intent = new Intent(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
+ final WifiP2pInfo formedInfo = new WifiP2pInfo();
+ formedInfo.groupFormed = groupFormed;
+ final WifiP2pGroup group;
+ if (groupFormed) {
+ group = mock(WifiP2pGroup.class);
+ doReturn(WIFI_P2P_IFACE_NAME).when(group).getInterface();
+ } else {
+ group = null;
+ }
+ intent.putExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO, formedInfo);
+ intent.putExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP, group);
+ return intent;
+ }
+
+ @Test
+ public void testWifiP2PInterfaceChange() {
+ final BroadcastReceiver receiver = expectWifiP2PChangeBroadcastReceiver();
+ startMonitoringSockets();
+
+ // Request a socket with null network.
+ final TestSocketCallback testCallback = new TestSocketCallback();
+ runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallback));
+
+ // Wifi p2p is connected and the interface is up. Get a wifi p2p change intent then expect
+ // a socket creation.
+ final Intent formedIntent = buildWifiP2PConnectionChangedIntent(true /* groupFormed */);
+ receiver.onReceive(mContext, formedIntent);
+ verify(mLocalOnlyIfaceWrapper).getNetworkInterface();
+ testCallback.expectedSocketCreatedForNetwork(null /* network */, List.of());
+
+ // Wifi p2p is disconnected. Get a wifi p2p change intent then expect the socket destroy.
+ final Intent unformedIntent = buildWifiP2PConnectionChangedIntent(false /* groupFormed */);
+ receiver.onReceive(mContext, unformedIntent);
+ testCallback.expectedInterfaceDestroyedForNetwork(null /* network */);
+ }
+
+ @Test
+ public void testWifiP2PInterfaceChangeBeforeStartMonitoringSockets() {
+ final BroadcastReceiver receiver = expectWifiP2PChangeBroadcastReceiver();
+
+ // Get a wifi p2p change intent before start monitoring sockets.
+ final Intent formedIntent = buildWifiP2PConnectionChangedIntent(true /* groupFormed */);
+ receiver.onReceive(mContext, formedIntent);
+
+ // Start monitoring sockets and request a socket with null network.
+ startMonitoringSockets();
+ final TestSocketCallback testCallback = new TestSocketCallback();
+ runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallback));
+ verify(mLocalOnlyIfaceWrapper).getNetworkInterface();
+ testCallback.expectedSocketCreatedForNetwork(null /* network */, List.of());
+ }
+
+ @Test
+ public void testWifiP2PInterfaceChangeBeforeGetAllNetworksRequest() {
+ final BroadcastReceiver receiver = expectWifiP2PChangeBroadcastReceiver();
+ startMonitoringSockets();
+
+ // Get a wifi p2p change intent before request socket for all networks.
+ final Intent formedIntent = buildWifiP2PConnectionChangedIntent(true /* groupFormed */);
+ receiver.onReceive(mContext, formedIntent);
+
+ // Request a socket with null network.
+ final TestSocketCallback testCallback = new TestSocketCallback();
+ runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallback));
+ verify(mLocalOnlyIfaceWrapper).getNetworkInterface();
+ testCallback.expectedSocketCreatedForNetwork(null /* network */, List.of());
+ }
+
+ @Test
+ public void testNoDuplicatedSocketCreation() {
+ final BroadcastReceiver receiver = expectWifiP2PChangeBroadcastReceiver();
+ startMonitoringSockets();
+
+ // Request a socket with null network.
+ final TestSocketCallback testCallback = new TestSocketCallback();
+ runOnHandler(() -> mSocketProvider.requestSocket(null, testCallback));
+ testCallback.expectedNoCallback();
+
+ // Receive an interface added change for the wifi p2p interface. Expect a socket creation
+ // callback.
+ runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(
+ List.of(WIFI_P2P_IFACE_NAME)));
+ verify(mLocalOnlyIfaceWrapper, times(1)).getNetworkInterface();
+ testCallback.expectedSocketCreatedForNetwork(null /* network */, List.of());
+
+ // Receive a wifi p2p connected intent. Expect no callback because the socket is created.
+ final Intent formedIntent = buildWifiP2PConnectionChangedIntent(true /* groupFormed */);
+ receiver.onReceive(mContext, formedIntent);
+ testCallback.expectedNoCallback();
+
+ // Request other socket with null network. Should receive socket created callback once.
+ final TestSocketCallback testCallback2 = new TestSocketCallback();
+ runOnHandler(() -> mSocketProvider.requestSocket(null, testCallback2));
+ testCallback2.expectedSocketCreatedForNetwork(null /* network */, List.of());
+ testCallback2.expectedNoCallback();
+
+ // Receive a wifi p2p disconnected intent. Expect a socket destroy callback.
+ final Intent unformedIntent = buildWifiP2PConnectionChangedIntent(false /* groupFormed */);
+ receiver.onReceive(mContext, unformedIntent);
+ testCallback.expectedInterfaceDestroyedForNetwork(null /* network */);
+
+ // Receive an interface removed change for the wifi p2p interface. Expect no callback
+ // because the socket is destroyed.
+ runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(List.of()));
+ testCallback.expectedNoCallback();
+
+ // Receive a wifi p2p connected intent again. Expect a socket creation callback.
+ receiver.onReceive(mContext, formedIntent);
+ verify(mLocalOnlyIfaceWrapper, times(2)).getNetworkInterface();
+ testCallback.expectedSocketCreatedForNetwork(null /* network */, List.of());
+
+ // Receive an interface added change for the wifi p2p interface again. Expect no callback
+ // because the socket is created.
+ runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(
+ List.of(WIFI_P2P_IFACE_NAME)));
+ testCallback.expectedNoCallback();
+ }
+
+ @Test
+ public void testTetherInterfacesChangedBeforeGetAllNetworksRequest() {
+ startMonitoringSockets();
+
+ // Receive an interface added change for the wifi p2p interface. Expect a socket creation
+ // callback.
+ runOnHandler(() -> mTetheringEventCallback.onLocalOnlyInterfacesChanged(
+ List.of(TETHERED_IFACE_NAME)));
+ verify(mTetheredIfaceWrapper, never()).getNetworkInterface();
+
+ // Request a socket with null network.
+ final TestSocketCallback testCallback = new TestSocketCallback();
+ runOnHandler(() -> mSocketProvider.requestSocket(null /* network */, testCallback));
+ verify(mTetheredIfaceWrapper).getNetworkInterface();
+ testCallback.expectedSocketCreatedForNetwork(null /* network */, List.of());
+ }
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 04163fd..99f6d63 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -96,7 +96,6 @@
import android.content.Intent;
import android.content.res.Resources;
import android.database.ContentObserver;
-import android.net.ConnectivityResources;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsSession;
@@ -145,6 +144,7 @@
import com.android.net.module.util.bpf.CookieTagMapKey;
import com.android.net.module.util.bpf.CookieTagMapValue;
import com.android.server.BpfNetMaps;
+import com.android.server.connectivity.ConnectivityResources;
import com.android.server.net.NetworkStatsService.AlertObserver;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;