[automerger skipped] Merge "LinkProperties: use ordered comparison for P-CSCFs." into tm-mainline-prod am: 11817f2b62 -s ours

am skip reason: Merged-In Ia1884f37739a20d871dd0024cc72cb18c083dc8e with SHA-1 78b8ff8493 is already in history

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/23246157

Change-Id: Ib5703f72281da808d8141e8d6b878b0801539cc7
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index f668807..22eccf9 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -21,7 +21,7 @@
 // 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"
+cronet_test_java_defaults = "CronetTestJavaDefaultsEnabled"
 // This is a placeholder comment to avoid merge conflicts
 // as cronet_test_java_defaults may have different values
 // depending on the branch
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..4506e5a 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,23 +29,23 @@
 // 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
@@ -61,7 +54,7 @@
 // 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"
+cronet_in_tethering_apex_defaults = "CronetInTetheringApexDefaultsEnabled"
 // This is a placeholder comment to avoid merge conflicts
 // as cronet_apex_defaults may have different values
 // depending on the branch
@@ -254,27 +247,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..6b62da9 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -21,8 +21,8 @@
 // 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"
+cronet_java_defaults = "CronetJavaDefaultsEnabled"
+cronet_java_prejarjar_defaults = "CronetJavaPrejarjarDefaultsEnabled"
 // This is a placeholder comment to avoid merge conflicts
 // as cronet_defaults may have different values
 // depending on the branch
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/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/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/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/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/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..333ba5c 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -26,8 +26,8 @@
 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;
 
 import com.android.internal.annotations.GuardedBy;
@@ -37,6 +37,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
@@ -270,29 +271,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 +376,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);
                 }
             });
         }
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..c62e3e3 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -33,6 +33,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 +80,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 +98,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 +173,13 @@
     }
 
     /**
+     * Returns if CHRE enabled for scanning.
+     */
+    public boolean isOffloadOnly() {
+        return mOffloadOnly;
+    }
+
+    /**
      * Returns Scan Filters for this request.
      */
     @NonNull
@@ -203,7 +215,8 @@
         stringBuilder.append("Request[")
                 .append("scanType=").append(mScanType);
         stringBuilder.append(", scanMode=").append(scanModeToString(mScanMode));
-        stringBuilder.append(", enableBle=").append(mBleEnabled);
+        stringBuilder.append(", bleEnabled=").append(mBleEnabled);
+        stringBuilder.append(", offloadOnly=").append(mOffloadOnly);
         stringBuilder.append(", workSource=").append(mWorkSource);
         stringBuilder.append(", scanFilters=").append(mScanFilters);
         stringBuilder.append("]");
@@ -215,6 +228,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 +244,7 @@
             return mScanType == otherRequest.mScanType
                     && (mScanMode == otherRequest.mScanMode)
                     && (mBleEnabled == otherRequest.mBleEnabled)
+                    && (mOffloadOnly == otherRequest.mOffloadOnly)
                     && (Objects.equals(mWorkSource, otherRequest.mWorkSource));
         }
         return false;
@@ -237,7 +252,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mScanType, mScanMode, mBleEnabled, mWorkSource);
+        return Objects.hash(mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource);
     }
 
     /** @hide **/
@@ -260,6 +275,7 @@
         private @ScanMode int mScanMode;
 
         private boolean mBleEnabled;
+        private boolean mOffloadOnly;
         private WorkSource mWorkSource;
         private List<ScanFilter> mScanFilters;
 
@@ -267,6 +283,7 @@
         public Builder() {
             mScanType = INVALID_SCAN_TYPE;
             mBleEnabled = true;
+            mOffloadOnly = false;
             mWorkSource = new WorkSource();
             mScanFilters = new ArrayList<>();
         }
@@ -307,6 +324,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 +394,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 7e7ea3c..53d27ab 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í je 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 46c45af..f7ab21f 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 bolo 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 &quot;Tommy's Bose QC35.&quot; [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&#8230;</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 76d6942..0c10af2 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
 
 import java.util.concurrent.Executors;
 
@@ -46,6 +47,12 @@
      */
     public static final String NEARBY_SUPPORT_TEST_APP = "nearby_support_test_app";
 
+    /**
+     * Flag to control which version of DiscoveryProviderManager should be used.
+     */
+    public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
+            "nearby_refactor_discovery_manager";
+
     private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
 
     private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -57,11 +64,27 @@
     private int mNanoAppMinVersion;
     @GuardedBy("mDeviceConfigLock")
     private boolean mSupportTestApp;
+    @GuardedBy("mDeviceConfigLock")
+    private boolean mRefactorDiscoveryManager;
 
     public NearbyConfiguration() {
         mDeviceConfigListener.start();
     }
 
+    private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+        final String value = getDeviceConfigProperty(name);
+        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    private static int getDeviceConfigInt(final String name, final int defaultValue) {
+        final String value = getDeviceConfigProperty(name);
+        return value != null ? Integer.parseInt(value) : defaultValue;
+    }
+
+    private static String getDeviceConfigProperty(String name) {
+        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_NEARBY, name);
+    }
+
     /**
      * Returns whether broadcasting legacy presence spec is enabled.
      */
@@ -86,11 +109,21 @@
         }
     }
 
+    /**
+     * @return {@code true} if use {@link DiscoveryProviderManager} or use
+     * DiscoveryProviderManagerLegacy if {@code false}.
+     */
+    public boolean refactorDiscoveryManager() {
+        synchronized (mDeviceConfigLock) {
+            return mRefactorDiscoveryManager;
+        }
+    }
+
     private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
         public void start() {
-            DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_TETHERING,
+            DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_NEARBY,
                     Executors.newSingleThreadExecutor(), this);
-            onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_TETHERING));
+            onPropertiesChanged(DeviceConfig.getProperties(DeviceConfig.NAMESPACE_NEARBY));
         }
 
         @Override
@@ -102,21 +135,9 @@
                         NEARBY_MAINLINE_NANO_APP_MIN_VERSION, 0 /* defaultValue */);
                 mSupportTestApp = !IS_USER_BUILD && getDeviceConfigBoolean(
                         NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+                mRefactorDiscoveryManager = getDeviceConfigBoolean(
+                        NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
             }
         }
     }
-
-    private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
-        final String value = getDeviceConfigProperty(name);
-        return value != null ? Boolean.parseBoolean(value) : defaultValue;
-    }
-
-    private static int getDeviceConfigInt(final String name, final int defaultValue) {
-        final String value = getDeviceConfigProperty(name);
-        return value != null ? Integer.parseInt(value) : defaultValue;
-    }
-
-    private static String getDeviceConfigProperty(String name) {
-        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
-    }
 }
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..e68d22a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -0,0 +1,504 @@
+/*
+ * 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);
+
+            Boolean started = startProviders(scanRequest);
+            if (started == null) {
+                return NearbyManager.ScanStatus.UNKNOWN;
+            }
+            if (!started) {
+                return NearbyManager.ScanStatus.ERROR;
+            }
+            mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
+            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..b1fdd5d 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,8 @@
 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 android.provider.DeviceConfig.NAMESPACE_NEARBY;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -35,7 +36,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;
@@ -57,6 +60,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 +70,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 +86,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 +101,19 @@
         @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,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY,
                 "nearby_enable_presence_broadcast_legacy",
                 "true", false);
 
@@ -137,7 +149,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 +171,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 +193,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/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
index ca03081..1b13d50 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -18,7 +18,7 @@
 
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.provider.DeviceConfig.NAMESPACE_NEARBY;
 
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
@@ -48,11 +48,11 @@
     public void testDeviceConfigChanged() throws InterruptedException {
         mNearbyConfiguration = new NearbyConfiguration();
 
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP,
                 "false", false);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
                 "false", false);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
                 "1", false);
         Thread.sleep(500);
 
@@ -60,11 +60,11 @@
         assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isFalse();
         assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(1);
 
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP,
                 "true", false);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
                 "true", false);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
                 "3", false);
         Thread.sleep(500);
 
@@ -76,11 +76,11 @@
     @After
     public void tearDown() {
         // Sets DeviceConfig values back to default
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP,
                 "false", true);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
                 "false", true);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
                 "0", true);
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index a5cbbc8..90c85ab 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -19,7 +19,7 @@
 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.provider.DeviceConfig.NAMESPACE_NEARBY;
 
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
 
@@ -36,6 +36,7 @@
 import android.content.Context;
 import android.nearby.IScanListener;
 import android.nearby.ScanRequest;
+import android.os.IBinder;
 import android.provider.DeviceConfig;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -61,10 +62,14 @@
     private IScanListener mScanListener;
     @Mock
     private AppOpsManager mMockAppOpsManager;
+    @Mock
+    private IBinder mIBinder;
 
     @Before
     public void setUp()  {
         initMocks(this);
+        when(mScanListener.asBinder()).thenReturn(mIBinder);
+
         mUiAutomation.adoptShellPermissionIdentity(
                 READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
@@ -86,7 +91,7 @@
 
     @Test
     public void test_register_noPrivilegedPermission_throwsException() {
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP,
                 "false", false);
         mUiAutomation.dropShellPermissionIdentity();
         assertThrows(java.lang.SecurityException.class,
@@ -96,7 +101,7 @@
 
     @Test
     public void test_unregister_noPrivilegedPermission_throwsException() {
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP,
                 "false", false);
         mUiAutomation.dropShellPermissionIdentity();
         assertThrows(java.lang.SecurityException.class,
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/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index d0008f5..b68e247 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -18,7 +18,7 @@
 
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.provider.DeviceConfig.NAMESPACE_NEARBY;
 
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
@@ -82,7 +82,7 @@
     @Before
     public void setUp() {
         mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
                 "true", false);
 
         mContext = ApplicationProvider.getApplicationContext();
@@ -116,9 +116,9 @@
 
     @Test
     public void testStartAdvertising_featureDisabled() throws Exception {
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
                 "false", false);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP,
                 "false", false);
 
         mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
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 58b8584..9801949 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
@@ -18,9 +18,10 @@
 
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.provider.DeviceConfig.NAMESPACE_NEARBY;
 
 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;
 
@@ -36,8 +37,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.injector.Injector;
@@ -54,6 +57,8 @@
 import java.util.concurrent.Executor;
 
 public class ChreCommunicationTest {
+    private static final int APP_VERSION = 1;
+
     @Mock Injector mInjector;
     @Mock Context mContext;
     @Mock ContextHubManager mManager;
@@ -71,7 +76,7 @@
     public void setUp() {
         InstrumentationRegistry.getInstrumentation().getUiAutomation()
                 .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
                 "1", false);
 
         MockitoAnnotations.initMocks(this);
@@ -83,7 +88,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());
     }
@@ -111,7 +119,7 @@
 
     @Test
     public void testNotReachMinVersion() {
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+        DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
                 "3", false);
         mChreCommunication.start(
                 mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
@@ -121,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 25d3f73..5f6edae 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
@@ -18,7 +18,7 @@
 
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.provider.DeviceConfig.NAMESPACE_NEARBY;
 
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
 
@@ -26,11 +26,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;
@@ -57,6 +63,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 =
@@ -130,7 +197,7 @@
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
         if (isSupportedTestApp) {
-            DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP, "false", false);
+            DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP, "false", false);
         }
         assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
 
@@ -215,7 +282,7 @@
         assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
         // Reverts the setting of test app support
         if (isSupportedTestApp) {
-            DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP, "true", false);
+            DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP, "true", false);
             assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
         }
     }
@@ -227,7 +294,7 @@
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
         if (!isSupportedTestApp) {
-            DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP, "true", false);
+            DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP, "true", false);
         }
         assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
 
@@ -316,7 +383,7 @@
         assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
         // Reverts the setting of test app support
         if (!isSupportedTestApp) {
-            DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_SUPPORT_TEST_APP, "false", false);
+            DeviceConfig.setProperty(NAMESPACE_NEARBY, NEARBY_SUPPORT_TEST_APP, "false", false);
             assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
         }
     }
@@ -327,7 +394,7 @@
     }
 
     private String getDeviceConfigProperty(String name) {
-        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
+        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_NEARBY, name);
     }
 
     private static class InLineExecutor implements Executor {
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/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/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 1d99190..4af4c6a 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;
@@ -84,6 +85,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 +145,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 +161,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 +245,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 +267,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 +282,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 +306,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 +600,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 +617,17 @@
                             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())
+                                            .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)) {
@@ -873,7 +889,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);
@@ -1104,9 +1123,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 +1179,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 +1200,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 +1336,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 +1386,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 +1412,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 +1424,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 +1451,9 @@
          * @see MdnsDiscoveryManager
          */
         public MdnsDiscoveryManager makeMdnsDiscoveryManager(
-                ExecutorProvider executorProvider, MdnsSocketClientBase socketClient) {
-            return new MdnsDiscoveryManager(executorProvider, socketClient);
+                @NonNull ExecutorProvider executorProvider,
+                @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
+            return new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog);
         }
 
         /**
@@ -1399,15 +1461,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 +1832,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/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/MdnsDiscoveryManager.java b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
index 64985ad..6455044 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsDiscoveryManager.java
@@ -32,7 +32,6 @@
 import com.android.net.module.util.SharedLog;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -43,10 +42,10 @@
 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;
@@ -82,7 +81,11 @@
             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;
+                // The serviceTypeNetwork would be null if the MdnsSocketClient is being used. This
+                // is also the case if the socket is for a tethering interface. In either of these
+                // cases, the client is expected to process any responses.
+                if (serviceTypeNetwork == null || isNetworkMatched(network, serviceTypeNetwork)) {
                     list.add(clients.valueAt(i));
                 }
             }
@@ -100,9 +103,10 @@
     }
 
     public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
-            @NonNull MdnsSocketClientBase socketClient) {
+            @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog) {
         this.executorProvider = executorProvider;
         this.socketClient = socketClient;
+        this.sharedLog = sharedLog;
         perNetworkServiceTypeClients = new PerNetworkServiceTypeClients();
     }
 
@@ -120,29 +124,47 @@
             @NonNull String serviceType,
             @NonNull MdnsServiceBrowserListener listener,
             @NonNull MdnsSearchOptions searchOptions) {
-        LOGGER.i("Registering listener for serviceType: " + serviceType);
+        sharedLog.i("Registering listener for serviceType: " + serviceType);
         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) {
+                        synchronized (MdnsDiscoveryManager.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);
+                        }
+                    }
+
+                    @Override
+                    public void onSocketDestroyed(@Nullable Network network) {
+                        synchronized (MdnsDiscoveryManager.this) {
+                            final MdnsServiceTypeClient serviceTypeClient =
+                                    perNetworkServiceTypeClients.get(serviceType, network);
+                            if (serviceTypeClient == null) return;
+                            // Notify all listeners that all services are removed from this socket.
+                            serviceTypeClient.notifyAllServicesRemoved();
+                            perNetworkServiceTypeClients.remove(serviceTypeClient);
+                        }
+                    }
+                });
     }
 
     /**
@@ -155,7 +177,7 @@
     @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
     public synchronized void unregisterListener(
             @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
-        LOGGER.i("Unregistering listener for serviceType:" + serviceType);
+        sharedLog.i("Unregistering listener for serviceType:" + serviceType);
         final List<MdnsServiceTypeClient> serviceTypeClients =
                 perNetworkServiceTypeClients.getByServiceType(serviceType);
         if (serviceTypeClients.isEmpty()) {
@@ -167,12 +189,12 @@
                 // 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();
-                }
             }
         }
+        if (perNetworkServiceTypeClients.isEmpty()) {
+            // No discovery request. Stops the socket client.
+            socketClient.stopDiscovery();
+        }
         // Unrequested the network.
         socketClient.notifyNetworkUnrequested(listener);
     }
@@ -195,19 +217,13 @@
         }
     }
 
-    /** Dump info to dumpsys */
-    public void dump(PrintWriter pw) {
-        LOGGER.reverseDump(pw);
-    }
-
     @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..6414453 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsMultinetworkSocketClient.java
@@ -90,6 +90,7 @@
                 @NonNull MdnsInterfaceSocket socket) {
             mSocketPacketHandlers.remove(socket);
             mActiveNetworkSockets.remove(socket);
+            mSocketCreationCallback.onSocketDestroyed(network);
         }
     }
 
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/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 72b931d..4e6571f 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;
@@ -175,6 +178,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 +187,7 @@
                     listener.onServiceNameDiscovered(info);
                     if (existingResponse.isComplete()) {
                         listener.onServiceFound(info);
+                        hadReply = true;
                     }
                 }
             }
@@ -192,22 +197,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,
+                    searchOptions.getNetwork());
+            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;
     }
 
     /**
@@ -263,6 +283,28 @@
         }
     }
 
+    /** Notify all services are removed because the socket is destroyed. */
+    public void notifyAllServicesRemoved() {
+        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);
+                }
+            }
+        }
+    }
+
     private void onResponseModified(@NonNull MdnsResponse response) {
         final String serviceInstanceName = response.getServiceInstanceName();
         final MdnsResponse currentResponse =
@@ -288,16 +330,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 +357,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);
         }
     }
@@ -535,30 +577,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/MdnsSocketClientBase.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
index ebafc49..6bcad01 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketClientBase.java
@@ -86,5 +86,8 @@
     interface SocketCreationCallback {
         /*** Notify requested socket is created */
         void onSocketCreated(@Nullable Network network);
+
+        /*** Notify requested socket is destroyed */
+        void onSocketDestroyed(@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..ca61d34 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSocketProvider.java
@@ -47,7 +47,6 @@
 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;
@@ -69,7 +68,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 +76,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 =
@@ -94,16 +93,18 @@
     private boolean mMonitoringSockets = false;
     private boolean mRequestStop = false;
 
-    public MdnsSocketProvider(@NonNull Context context, @NonNull Looper looper) {
-        this(context, looper, new Dependencies());
+    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 +136,8 @@
             }
         };
 
-        mSocketNetlinkMonitor = mDependencies.createSocketNetlinkMonitor(mHandler, LOGGER,
-                new NetLinkMessageProcessor());
+        mSocketNetlinkMonitor = mDependencies.createSocketNetlinkMonitor(mHandler,
+                mSharedLog.forSubComponent("NetlinkMonitor"), new NetLinkMessageProcessor());
     }
 
     /**
@@ -253,7 +254,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 +283,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);
 
@@ -420,7 +421,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 +442,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 +471,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 +482,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 +490,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 +562,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
@@ -584,6 +586,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).
@@ -598,7 +601,7 @@
             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
@@ -609,7 +612,7 @@
             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 +621,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/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/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="                        () -&gt; 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..8ec6eb9 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;
@@ -91,7 +92,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 +111,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 +137,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 +246,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 +283,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 +315,13 @@
 
 import java.io.FileDescriptor;
 import java.io.IOException;
+import java.io.InterruptedIOException;
 import java.io.PrintWriter;
 import java.io.Writer;
 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;
@@ -783,6 +790,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.
      */
@@ -1389,9 +1401,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 +1497,28 @@
                 @NonNull final UserHandle user) {
             return CompatChanges.isChangeEnabled(changeId, packageName, user);
         }
+
+        /**
+         * 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);
+        }
     }
 
     public ConnectivityService(Context context) {
@@ -1675,6 +1709,32 @@
         } else {
             mCdmps = null;
         }
+
+        if (SdkLevel.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);
+        }
     }
 
     /**
@@ -2843,6 +2903,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,
@@ -5706,6 +5799,10 @@
                     mKeepaliveTracker.handleSetTestLowTcpPollingTimer(time);
                     break;
                 }
+                case EVENT_UID_FROZEN_STATE_CHANGED:
+                    UidFrozenStateChangedArgs args = (UidFrozenStateChangedArgs) msg.obj;
+                    handleFrozenUids(args.mUids, args.mFrozenStates);
+                    break;
             }
         }
     }
@@ -7904,16 +8001,27 @@
         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;
         }
 
@@ -8448,11 +8556,11 @@
         return stableRanges;
     }
 
-    private void maybeCloseSockets(NetworkAgentInfo nai, UidRangeParcel[] ranges,
-            int[] exemptUids) {
+    private void maybeCloseSockets(NetworkAgentInfo nai, Set<UidRange> ranges,
+            Set<Integer> exemptUids) {
         if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
             try {
-                mNetd.socketDestroy(ranges, exemptUids);
+                mDeps.destroyLiveTcpSockets(UidRange.toIntRanges(ranges), exemptUids);
             } catch (Exception e) {
                 loge("Exception in socket destroy: ", e);
             }
@@ -8460,16 +8568,16 @@
     }
 
     private void updateVpnUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) {
-        int[] exemptUids = new int[2];
+        final Set<Integer> exemptUids = new ArraySet<>();
         // TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used
         // by PPTP. Fix this by making Vpn set the owner UID to VPN_UID instead of system when
         // starting a legacy VPN, and remove VPN_UID here. (b/176542831)
-        exemptUids[0] = VPN_UID;
-        exemptUids[1] = nai.networkCapabilities.getOwnerUid();
+        exemptUids.add(VPN_UID);
+        exemptUids.add(nai.networkCapabilities.getOwnerUid());
         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, exemptUids);
         try {
             if (add) {
                 mNetd.networkAddUidRangesParcel(new NativeUidRangeConfig(
@@ -8483,7 +8591,7 @@
                     " on netId " + nai.network.netId + ". " + e);
         }
         // Close sockets that established connection while requesting netd.
-        maybeCloseSockets(nai, ranges, exemptUids);
+        maybeCloseSockets(nai, uidRanges, exemptUids);
     }
 
     private boolean isProxySetOnAnyDefaultNetwork() {
@@ -10021,6 +10129,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();
 
@@ -11894,6 +12012,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 +12038,14 @@
         } catch (ServiceSpecificException e) {
             throw new IllegalStateException(e);
         }
+
+        if (SdkLevel.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..8c170bc 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(
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/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/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..50de714 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));
@@ -2552,6 +2570,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 +2611,7 @@
             ConnectivitySettingsManager.setPrivateDnsMode(mContext, curPrivateDnsMode);
             tetherUtils.unregisterTetheringEventCallback(tetherEventCallback);
             tetherUtils.stopAllTethering();
+            mCtsNetUtils.ensureWifiConnected();
         }
     }
 
@@ -3523,6 +3550,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/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index db7f38c..88b9baf 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -282,13 +282,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
         }
     }
 
@@ -497,6 +501,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 +513,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 +549,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 +567,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 +605,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 +652,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 +666,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 +682,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 +758,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 +808,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 +823,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 +853,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 +1003,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)
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..8396252 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;
@@ -223,6 +227,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 +261,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;
@@ -388,6 +393,7 @@
 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 +539,10 @@
     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_MASK = 0xffff0000;
+    private static final int PACKET_WAKEUP_MARK = 0x88880000;
+
     private static final String ALWAYS_ON_PACKAGE = "com.android.test.alwaysonvpn";
 
     private static final String INTERFACE_NAME = "interface";
@@ -604,6 +614,7 @@
     @Mock CarrierPrivilegeAuthenticator mCarrierPrivilegeAuthenticator;
     @Mock TetheringManager mTetheringManager;
     @Mock BroadcastOptionsShim mBroadcastOptionsShim;
+    @Mock ActivityManager mActivityManager;
 
     // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
     // underlying binder calls.
@@ -727,6 +738,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);
         }
 
@@ -1853,7 +1865,7 @@
         final Context mockResContext = mock(Context.class);
         doReturn(mResources).when(mockResContext).getResources();
         ConnectivityResources.setResourcesContextForTest(mockResContext);
-        mDeps = new ConnectivityServiceDependencies(mockResContext);
+        mDeps = spy(new ConnectivityServiceDependencies(mockResContext));
         mAutoOnOffKeepaliveDependencies =
                 new AutomaticOnOffKeepaliveTrackerDependencies(mServiceContext);
         mService = new ConnectivityService(mServiceContext,
@@ -1910,9 +1922,14 @@
         doReturn(0).when(mResources).getInteger(R.integer.config_activelyPreferBadWifi);
         doReturn(true).when(mResources)
                 .getBoolean(R.bool.config_cellular_radio_timesharing_capable);
+        doReturn(PACKET_WAKEUP_MASK).when(mResources).getInteger(
+                R.integer.config_networkWakeupPacketMask);
+        doReturn(PACKET_WAKEUP_MARK).when(mResources).getInteger(
+                R.integer.config_networkWakeupPacketMark);
     }
 
-    class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
+    // ConnectivityServiceDependencies is public to use Mockito.spy
+    public class ConnectivityServiceDependencies extends ConnectivityService.Dependencies {
         final ConnectivityResources mConnRes;
 
         ConnectivityServiceDependencies(final Context mockResContext) {
@@ -2067,12 +2084,14 @@
         }
 
         @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);
             }
         }
 
@@ -2148,6 +2167,17 @@
                 }
             }
         }
+
+        @Override
+        public void destroyLiveTcpSockets(final Set<Range<Integer>> ranges,
+                final Set<Integer> exemptUids) {
+            // This function is empty since the invocation of this method is verified by mocks
+        }
+
+        @Override
+        public void destroyLiveTcpSocketsByOwnerUids(final Set<Integer> ownerUids) {
+            // This function is empty since the invocation of this method is verified by mocks
+        }
     }
 
     private class AutomaticOnOffKeepaliveTrackerDependencies
@@ -10219,6 +10249,50 @@
         }
     }
 
+    private void doTestSetFirewallChainEnabledCloseSocket(final int chain,
+            final boolean isAllowList) throws Exception {
+        reset(mDeps);
+
+        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(mDeps).destroyLiveTcpSockets(range, uids);
+        } else {
+            verify(mDeps).destroyLiveTcpSocketsByOwnerUids(uids);
+        }
+
+        mCm.setFirewallChainEnabled(chain, false /* enabled */);
+        verifyNoMoreInteractions(mDeps);
+    }
+
+    @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,6 +10460,16 @@
         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);
@@ -10612,6 +10696,11 @@
         clat.interfaceRemoved(CLAT_MOBILE_IFNAME);
         networkCallback.assertNoCallback();
         verify(mMockNetd, times(1)).networkRemoveInterface(cellNetId, CLAT_MOBILE_IFNAME);
+
+        if (SdkLevel.isAtLeastU()) {
+            verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
+        }
+
         verifyNoMoreInteractions(mMockNetd);
         verifyNoMoreInteractions(mClatCoordinator);
         verifyNoMoreInteractions(mMockDnsResolver);
@@ -10648,6 +10737,10 @@
         assertRoutesAdded(cellNetId, stackedDefault);
         verify(mMockNetd, times(1)).networkAddInterface(cellNetId, CLAT_MOBILE_IFNAME);
 
+        if (SdkLevel.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 +10755,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 (SdkLevel.isAtLeastU()) {
+            verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
+        }
+
         // Clean up.
         mCellAgent.disconnect();
         networkCallback.expect(LOST, mCellAgent);
@@ -10674,6 +10772,11 @@
         } else {
             verify(mMockNetd, never()).setNetworkAllowlist(any());
         }
+
+        if (SdkLevel.isAtLeastU()) {
+            verifyWakeupModifyInterface(MOBILE_IFNAME, false);
+        }
+
         verifyNoMoreInteractions(mMockNetd);
         verifyNoMoreInteractions(mClatCoordinator);
         reset(mMockNetd);
@@ -10703,6 +10806,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 (SdkLevel.isAtLeastU()) {
+            verifyWakeupModifyInterface(MOBILE_IFNAME, true);
+        }
+
         reset(mMockNetd);
         reset(mClatCoordinator);
 
@@ -10711,6 +10819,11 @@
         networkCallback.expect(LOST, mCellAgent);
         networkCallback.assertNoCallback();
         verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
+
+        if (SdkLevel.isAtLeastU()) {
+            verifyWakeupModifyInterface(CLAT_MOBILE_IFNAME, false);
+        }
+
         verify(mMockNetd).idletimerRemoveInterface(eq(MOBILE_IFNAME), anyInt(),
                 eq(Integer.toString(TRANSPORT_CELLULAR)));
         verify(mMockNetd).networkDestroy(cellNetId);
@@ -10719,6 +10832,11 @@
         } else {
             verify(mMockNetd, never()).setNetworkAllowlist(any());
         }
+
+        if (SdkLevel.isAtLeastU()) {
+            verifyWakeupModifyInterface(MOBILE_IFNAME, false);
+        }
+
         verifyNoMoreInteractions(mMockNetd);
         verifyNoMoreInteractions(mClatCoordinator);
 
@@ -12471,12 +12589,11 @@
 
     private void assertVpnUidRangesUpdated(boolean add, Set<UidRange> vpnRanges, int exemptUid)
             throws Exception {
-        InOrder inOrder = inOrder(mMockNetd);
-        ArgumentCaptor<int[]> exemptUidCaptor = ArgumentCaptor.forClass(int[].class);
+        InOrder inOrder = inOrder(mMockNetd, mDeps);
+        final Set<Integer> exemptUidSet = new ArraySet<>(List.of(exemptUid, Process.VPN_UID));
 
-        inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
-                exemptUidCaptor.capture());
-        assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+        inOrder.verify(mDeps).destroyLiveTcpSockets(UidRange.toIntRanges(vpnRanges),
+                exemptUidSet);
 
         if (add) {
             inOrder.verify(mMockNetd, times(1)).networkAddUidRangesParcel(
@@ -12488,9 +12605,8 @@
                             toUidRangeStableParcels(vpnRanges), PREFERENCE_ORDER_VPN));
         }
 
-        inOrder.verify(mMockNetd, times(1)).socketDestroy(eq(toUidRangeStableParcels(vpnRanges)),
-                exemptUidCaptor.capture());
-        assertContainsExactly(exemptUidCaptor.getValue(), Process.VPN_UID, exemptUid);
+        inOrder.verify(mDeps).destroyLiveTcpSockets(UidRange.toIntRanges(vpnRanges),
+                exemptUidSet);
     }
 
     @Test
@@ -17572,18 +17688,77 @@
         });
     }
 
+    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());
+    }
+
     @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 */);
+        // The MTU is always (re-)applied when the network connects.
+        verifyMtuSetOnWifiInterface(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 doesn't set the MTU...
+        mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, lp);
+        waitForIdle();
+        verifyMtuNeverSetOnWifiInterface();
+        reset(mMockNetd);
+
+        // ... but prevents future updates with the same MTU from setting the MTU.
+        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 */);
+        // The MTU is always (re-)applied when the network connects.
+        verifyMtuSetOnWifiInterface(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 +17769,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 +17788,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 +17805,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 +17824,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 +17847,79 @@
         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,
+                PACKET_WAKEUP_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 (SdkLevel.isAtLeastU()) {
+            final String expectedPrefix = makeNflogPrefix(MOBILE_IFNAME,
+                    mCellAgent.getNetwork().getNetworkHandle());
+            verify(mMockNetd).wakeupAddInterface(MOBILE_IFNAME, expectedPrefix, PACKET_WAKEUP_MARK,
+                    PACKET_WAKEUP_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(mDeps).destroyLiveTcpSockets(eq(UidRange.toIntRanges(ranges)),
+                eq(exemptUids));
+    }
 }
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 2ed989e..b3e8cc8 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,9 +178,9 @@
         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/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..395e2bb 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -142,6 +142,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;
@@ -1563,6 +1564,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 +1585,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 +1909,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 +1934,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 +2088,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 +2109,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 +2153,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 +2220,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 +2408,7 @@
         final PlatformVpnSnapshot vpnSnapShot =
                 verifySetupPlatformVpn(ikeProfile.toVpnProfile(),
                         createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */),
+                        new NetworkCapabilities.Builder().build() /* underlying network caps */,
                         false /* mtuSupportsIpv6 */,
                         true /* areLongLivedTcpConnectionsExpensive */);
 
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..63357f1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsDiscoveryManagerTests.java
@@ -18,8 +18,9 @@
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
-import static org.mockito.Mockito.any;
 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;
 
@@ -29,6 +30,7 @@
 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;
@@ -38,6 +40,7 @@
 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;
@@ -53,30 +56,34 @@
 
     private static final String SERVICE_TYPE_1 = "_googlecast._tcp.local";
     private static final String SERVICE_TYPE_2 = "_test._tcp.local";
+    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 =
             Pair.create(SERVICE_TYPE_1, null);
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_1_1 =
+            Pair.create(SERVICE_TYPE_1, NETWORK_1);
     private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2 =
             Pair.create(SERVICE_TYPE_2, null);
+    private static final Pair<String, Network> PER_NETWORK_SERVICE_TYPE_2_2 =
+            Pair.create(SERVICE_TYPE_2, NETWORK_2);
 
     @Mock private ExecutorProvider executorProvider;
     @Mock private MdnsSocketClientBase socketClient;
     @Mock private MdnsServiceTypeClient mockServiceTypeClientOne;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientOne1;
     @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo;
+    @Mock private MdnsServiceTypeClient mockServiceTypeClientTwo2;
 
     @Mock MdnsServiceBrowserListener mockListenerOne;
     @Mock MdnsServiceBrowserListener mockListenerTwo;
+    @Mock SharedLog sharedLog;
     private MdnsDiscoveryManager discoveryManager;
 
     @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) {
+        discoveryManager = new MdnsDiscoveryManager(executorProvider, socketClient, sharedLog) {
                     @Override
                     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
                             @Nullable Network network) {
@@ -84,31 +91,37 @@
                                 Pair.create(serviceType, network);
                         if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1)) {
                             return mockServiceTypeClientOne;
+                        } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_1_1)) {
+                            return mockServiceTypeClientOne1;
                         } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_2)) {
                             return mockServiceTypeClientTwo;
+                        } else if (perNetworkServiceType.equals(PER_NETWORK_SERVICE_TYPE_2_2)) {
+                            return mockServiceTypeClientTwo2;
                         }
                         return null;
                     }
                 };
     }
 
-    private void verifyListenerRegistration(String serviceType, MdnsServiceBrowserListener listener,
-            MdnsServiceTypeClient client) throws IOException {
+    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());
+        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);
+        callback.onSocketCreated(null /* network */);
+        verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options);
 
         when(mockServiceTypeClientOne.stopSendAndReceive(mockListenerOne)).thenReturn(true);
         discoveryManager.unregisterListener(SERVICE_TYPE_1, mockListenerOne);
@@ -118,30 +131,121 @@
 
     @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);
+        callback.onSocketCreated(null /* network */);
+        verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options);
+        callback.onSocketCreated(NETWORK_1);
+        verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options);
+
+        final SocketCreationCallback callback2 = expectSocketCreationCallback(
+                SERVICE_TYPE_2, mockListenerTwo, options);
+        callback2.onSocketCreated(null /* network */);
+        verify(mockServiceTypeClientTwo).startSendAndReceive(mockListenerTwo, options);
+        callback2.onSocketCreated(NETWORK_2);
+        verify(mockServiceTypeClientTwo2).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);
+        callback.onSocketCreated(null /* network */);
+        verify(mockServiceTypeClientOne).startSendAndReceive(mockListenerOne, options1);
+        callback.onSocketCreated(NETWORK_1);
+        verify(mockServiceTypeClientOne1).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);
+        callback2.onSocketCreated(NETWORK_2);
+        verify(mockServiceTypeClientTwo2).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 */);
-
-        MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
-        discoveryManager.onResponseReceived(responseForServiceTypeTwo, ifIndex, null /* network */);
-        verify(mockServiceTypeClientTwo).processResponse(responseForServiceTypeTwo, ifIndex,
+        verify(mockServiceTypeClientOne1).processResponse(responseForServiceTypeOne, ifIndex,
+                null /* network */);
+        verify(mockServiceTypeClientTwo2).processResponse(responseForServiceTypeOne, ifIndex,
                 null /* network */);
 
-        MdnsPacket responseForSubtype = createMdnsPacket("subtype._sub._googlecast._tcp.local");
-        discoveryManager.onResponseReceived(responseForSubtype, ifIndex, null /* network */);
-        verify(mockServiceTypeClientOne).processResponse(responseForSubtype, ifIndex,
-                null /* network */);
+        final MdnsPacket responseForServiceTypeTwo = createMdnsPacket(SERVICE_TYPE_2);
+        discoveryManager.onResponseReceived(responseForServiceTypeTwo, ifIndex, NETWORK_1);
+        verify(mockServiceTypeClientOne).processResponse(responseForServiceTypeTwo, ifIndex,
+                NETWORK_1);
+        verify(mockServiceTypeClientOne1).processResponse(responseForServiceTypeTwo, ifIndex,
+                NETWORK_1);
+        verify(mockServiceTypeClientTwo2, never()).processResponse(responseForServiceTypeTwo,
+                ifIndex, NETWORK_1);
+
+        final MdnsPacket responseForSubtype =
+                createMdnsPacket("subtype._sub._googlecast._tcp.local");
+        discoveryManager.onResponseReceived(responseForSubtype, ifIndex, NETWORK_2);
+        verify(mockServiceTypeClientOne).processResponse(responseForSubtype, ifIndex, NETWORK_2);
+        verify(mockServiceTypeClientOne1, never()).processResponse(
+                responseForSubtype, ifIndex, NETWORK_2);
+        verify(mockServiceTypeClientTwo2).processResponse(responseForSubtype, ifIndex, NETWORK_2);
+    }
+
+    @Test
+    public void testSocketCreatedAndDestroyed() throws IOException {
+        // Create a ServiceTypeClient for SERVICE_TYPE_1 and NETWORK_1
+        final MdnsSearchOptions options1 =
+                MdnsSearchOptions.newBuilder().setNetwork(NETWORK_1).build();
+        final SocketCreationCallback callback = expectSocketCreationCallback(
+                SERVICE_TYPE_1, mockListenerOne, options1);
+        callback.onSocketCreated(NETWORK_1);
+        verify(mockServiceTypeClientOne1).startSendAndReceive(mockListenerOne, options1);
+
+        // Create a ServiceTypeClient for SERVICE_TYPE_2 and NETWORK_2
+        final MdnsSearchOptions options2 =
+                MdnsSearchOptions.newBuilder().setNetwork(NETWORK_2).build();
+        final SocketCreationCallback callback2 = expectSocketCreationCallback(
+                SERVICE_TYPE_2, mockListenerTwo, options2);
+        callback2.onSocketCreated(NETWORK_2);
+        verify(mockServiceTypeClientTwo2).startSendAndReceive(mockListenerTwo, options2);
+
+        // Receive a response, it should be processed on both clients.
+        final MdnsPacket response = createMdnsPacket(SERVICE_TYPE_1);
+        final int ifIndex = 1;
+        discoveryManager.onResponseReceived(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientOne1).processResponse(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientTwo2).processResponse(response, ifIndex, null /* network */);
+
+        // The client for NETWORK_1 receives the callback that the NETWORK_1 has been destroyed,
+        // mockServiceTypeClientOne1 should send service removed notifications and remove from the
+        // list of clients.
+        callback.onSocketDestroyed(NETWORK_1);
+        verify(mockServiceTypeClientOne1).notifyAllServicesRemoved();
+
+        // Receive a response again, it should be processed only on mockServiceTypeClientTwo2.
+        // Because the mockServiceTypeClientOne1 is removed from the list of clients, it is no
+        // longer able to process responses.
+        discoveryManager.onResponseReceived(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientOne1, times(1))
+                .processResponse(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientTwo2, times(2))
+                .processResponse(response, ifIndex, null /* network */);
+
+        // The client for NETWORK_2 receives the callback that the NETWORK_1 has been destroyed,
+        // mockServiceTypeClientTwo2 shouldn't send any notifications.
+        callback2.onSocketDestroyed(NETWORK_1);
+        verify(mockServiceTypeClientTwo2, never()).notifyAllServicesRemoved();
+
+        // Receive a response again, mockServiceTypeClientTwo2 is still in the list of clients, it's
+        // still able to process responses.
+        discoveryManager.onResponseReceived(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientOne1, times(1))
+                .processResponse(response, ifIndex, null /* network */);
+        verify(mockServiceTypeClientTwo2, times(3))
+                .processResponse(response, ifIndex, null /* network */);
     }
 
     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/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/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 34b44fc..bd59156 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -21,6 +21,7 @@
 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;
@@ -41,6 +42,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 +54,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;
@@ -422,6 +425,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,
@@ -1056,6 +1087,142 @@
         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 testNotifyAllServicesRemoved() {
+        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);
+        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        // 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);
+
+        client.notifyAllServicesRemoved();
+
+        // 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));
     }
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..744ec84 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsSocketProviderTest.java
@@ -111,6 +111,7 @@
     private MdnsSocketProvider mSocketProvider;
     private NetworkCallback mNetworkCallback;
     private TetheringEventCallback mTetheringEventCallback;
+    private SharedLog mLog = new SharedLog("MdnsSocketProviderTest");
 
     private TestNetlinkMonitor mTestSocketNetLinkMonitor;
     @Before
@@ -153,7 +154,7 @@
             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 startMonitoringSockets() {
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;