Merge "OWNERS: master -> main" into main am: 66f39a5667

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

Change-Id: I5ded059a17bb94d816f4eb9b3d1041a842c78a94
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index ea465aa..53ad834 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/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/nearby/README.md b/nearby/README.md
index 6925dc4..8451882 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -29,6 +29,20 @@
 $ aidegen .
 # This will launch Intellij project for Nearby module.
 ```
+Note, the setup above may fail to index classes defined in proto, such
+that all classes defined in proto shows red in IDE and cannot be auto-completed.
+To fix, you can mannually add jar files generated from proto to the class path
+as below.  First, find the jar file of presence proto with
+```sh
+ls $ANDROID_BUILD_TOP/out/soong/.intermediates/packages/modules/Connectivity/nearby/service/proto/presence-lite-protos/android_common/combined/presence-lite-protos.jar
+```
+Then, add the jar in IDE as below.
+1. Menu: File > Project Structure
+2. Select Modules at the left panel and select the Dependencies tab.
+3. Select the + icon and select 1 JARs or Directories option.
+4. Select the JAR file found above, make sure it is checked in the beginning square.
+5. Click the OK button.
+6. Restart the IDE to re-index.
 
 ## Build and Install
 
@@ -40,3 +54,23 @@
     --output /tmp/tethering.apex
 $ adb install -r /tmp/tethering.apex
 ```
+
+## Build and Install from tm-mainline-prod branch
+When build and flash the APEX from tm-mainline-prod, you may see the error below.
+```
+[INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade of APEX package com.google.android.tethering is not allowed. Active version: 990090000 attempted: 339990000])
+```
+This is because the device is flashed with AOSP built from master or other branches, which has
+prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
+built from tm-mainline-prod as below.
+1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+2. cp tethering.next.apex com.google.android.tethering.apex
+3. adb push  com.google.android.tethering.apex  /system/apex/
+4. adb reboot
+After the steps above, the APEX can be reinstalled with adb install -r.
+(More APEX background can be found in https://source.android.com/docs/core/ota/apex#using-an-apex.)
+
+## Build APEX to support multiple platforms
+If you need to flash the APEX to different devices, Pixel 6, Pixel 7, or even devices from OEM, you
+can share the APEX by ```source build/envsetup.sh && lunch aosp_arm64-userdebug```. This can avoid
+ re-compiling for different targets.
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 6fa5fb5..10c1132 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -16,13 +16,17 @@
 
 package android.nearby;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import com.android.internal.util.Preconditions;
 
+import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * Represents a data element in Nearby Presence.
@@ -35,11 +39,95 @@
     private final int mKey;
     private final byte[] mValue;
 
+    /** @hide */
+    @IntDef({
+            DataType.BLE_SERVICE_DATA,
+            DataType.BLE_ADDRESS,
+            DataType.SALT,
+            DataType.PRIVATE_IDENTITY,
+            DataType.TRUSTED_IDENTITY,
+            DataType.PUBLIC_IDENTITY,
+            DataType.PROVISIONED_IDENTITY,
+            DataType.TX_POWER,
+            DataType.ACTION,
+            DataType.MODEL_ID,
+            DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
+            DataType.ACCOUNT_KEY_DATA,
+            DataType.CONNECTION_STATUS,
+            DataType.BATTERY,
+            DataType.SCAN_MODE,
+            DataType.TEST_DE_BEGIN,
+            DataType.TEST_DE_END
+    })
+    public @interface DataType {
+        int BLE_SERVICE_DATA = 100;
+        int BLE_ADDRESS = 101;
+        // This is to indicate if the scan is offload only
+        int SCAN_MODE = 102;
+        int SALT = 0;
+        int PRIVATE_IDENTITY = 1;
+        int TRUSTED_IDENTITY = 2;
+        int PUBLIC_IDENTITY = 3;
+        int PROVISIONED_IDENTITY = 4;
+        int TX_POWER = 5;
+        int ACTION = 6;
+        int MODEL_ID = 7;
+        int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
+        int ACCOUNT_KEY_DATA = 9;
+        int CONNECTION_STATUS = 10;
+        int BATTERY = 11;
+        // Reserves test DE ranges from {@link DataElement.DataType#TEST_DE_BEGIN}
+        // to {@link DataElement.DataType#TEST_DE_END}, inclusive.
+        // Reserves 128 Test DEs.
+        int TEST_DE_BEGIN = Integer.MAX_VALUE - 127; // 2147483520
+        int TEST_DE_END = Integer.MAX_VALUE; // 2147483647
+    }
+
+    /**
+     * @hide
+     */
+    public static boolean isValidType(int type) {
+        return type == DataType.BLE_SERVICE_DATA
+                || type == DataType.ACCOUNT_KEY_DATA
+                || type == DataType.BLE_ADDRESS
+                || type == DataType.SCAN_MODE
+                || type == DataType.SALT
+                || type == DataType.PRIVATE_IDENTITY
+                || type == DataType.TRUSTED_IDENTITY
+                || type == DataType.PUBLIC_IDENTITY
+                || type == DataType.PROVISIONED_IDENTITY
+                || type == DataType.TX_POWER
+                || type == DataType.ACTION
+                || type == DataType.MODEL_ID
+                || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
+                || type == DataType.CONNECTION_STATUS
+                || type == DataType.BATTERY;
+    }
+
+    /**
+     * @return {@code true} if this is identity type.
+     * @hide
+     */
+    public boolean isIdentityDataType() {
+        return mKey == DataType.PRIVATE_IDENTITY
+                || mKey == DataType.TRUSTED_IDENTITY
+                || mKey == DataType.PUBLIC_IDENTITY
+                || mKey == DataType.PROVISIONED_IDENTITY;
+    }
+
+    /**
+     * @return {@code true} if this is test data element type.
+     * @hide
+     */
+    public static boolean isTestDeType(int type) {
+        return type >= DataType.TEST_DE_BEGIN && type <= DataType.TEST_DE_END;
+    }
+
     /**
      * Constructs a {@link DataElement}.
      */
     public DataElement(int key, @NonNull byte[] value) {
-        Preconditions.checkState(value != null, "value cannot be null");
+        Preconditions.checkArgument(value != null, "value cannot be null");
         mKey = key;
         mValue = value;
     }
@@ -61,6 +149,20 @@
     };
 
     @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof DataElement) {
+            return mKey == ((DataElement) obj).mKey
+                    && Arrays.equals(mValue, ((DataElement) obj).mValue);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mKey, Arrays.hashCode(mValue));
+    }
+
+    @Override
     public int describeContents() {
         return 0;
     }
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 0291fff..7af271e 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
 import android.nearby.IScanListener;
 import android.nearby.BroadcastRequestParcelable;
 import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
 
 /**
  * Interface for communicating with the nearby services.
@@ -37,4 +38,6 @@
             in IBroadcastListener callback, String packageName, @nullable String attributionTag);
 
     void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
+
+    void queryOffloadCapability(in IOffloadCallback callback) ;
 }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
index 3e3b107..80563b7 100644
--- a/nearby/framework/java/android/nearby/IScanListener.aidl
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -34,5 +34,5 @@
         void onLost(in NearbyDeviceParcelable nearbyDeviceParcelable);
 
         /** Reports when there is an error during scanning. */
-        void onError();
+        void onError(in int errorCode);
 }
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index 538940c..e8fcc28 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -21,11 +21,13 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.util.ArraySet;
 
 import com.android.internal.util.Preconditions;
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * A class represents a device that can be discovered by multiple mediums.
@@ -123,13 +125,17 @@
 
     @Override
     public boolean equals(Object other) {
-        if (other instanceof NearbyDevice) {
-            NearbyDevice otherDevice = (NearbyDevice) other;
-            return Objects.equals(mName, otherDevice.mName)
-                    && mMediums == otherDevice.mMediums
-                    && mRssi == otherDevice.mRssi;
+        if (!(other instanceof NearbyDevice)) {
+            return false;
         }
-        return false;
+        NearbyDevice otherDevice = (NearbyDevice) other;
+        Set<Integer> mediumSet = new ArraySet<>(mMediums);
+        Set<Integer> otherMediumSet = new ArraySet<>(otherDevice.mMediums);
+        if (!mediumSet.equals(otherMediumSet)) {
+            return false;
+        }
+
+        return Objects.equals(mName, otherDevice.mName) && mRssi == otherDevice.mRssi;
     }
 
     @Override
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index 8f44091..8fb9650 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -46,6 +46,7 @@
                 @Override
                 public NearbyDeviceParcelable createFromParcel(Parcel in) {
                     Builder builder = new Builder();
+                    builder.setDeviceId(in.readLong());
                     builder.setScanType(in.readInt());
                     if (in.readInt() == 1) {
                         builder.setName(in.readString());
@@ -76,6 +77,17 @@
                         in.readByteArray(salt);
                         builder.setData(salt);
                     }
+                    if (in.readInt() == 1) {
+                        builder.setPresenceDevice(in.readParcelable(
+                                PresenceDevice.class.getClassLoader(),
+                                PresenceDevice.class));
+                    }
+                    if (in.readInt() == 1) {
+                        int encryptionKeyTagLength = in.readInt();
+                        byte[] keyTag = new byte[encryptionKeyTagLength];
+                        in.readByteArray(keyTag);
+                        builder.setData(keyTag);
+                    }
                     return builder.build();
                 }
 
@@ -85,6 +97,7 @@
                 }
             };
 
+    private final long mDeviceId;
     @ScanRequest.ScanType int mScanType;
     @Nullable private final String mName;
     @NearbyDevice.Medium private final int mMedium;
@@ -96,8 +109,11 @@
     @Nullable private final String mFastPairModelId;
     @Nullable private final byte[] mData;
     @Nullable private final byte[] mSalt;
+    @Nullable private final PresenceDevice mPresenceDevice;
+    @Nullable private final byte[] mEncryptionKeyTag;
 
     private NearbyDeviceParcelable(
+            long deviceId,
             @ScanRequest.ScanType int scanType,
             @Nullable String name,
             int medium,
@@ -108,7 +124,10 @@
             @Nullable String fastPairModelId,
             @Nullable String bluetoothAddress,
             @Nullable byte[] data,
-            @Nullable byte[] salt) {
+            @Nullable byte[] salt,
+            @Nullable PresenceDevice presenceDevice,
+            @Nullable byte[] encryptionKeyTag) {
+        mDeviceId = deviceId;
         mScanType = scanType;
         mName = name;
         mMedium = medium;
@@ -120,6 +139,8 @@
         mBluetoothAddress = bluetoothAddress;
         mData = data;
         mSalt = salt;
+        mPresenceDevice = presenceDevice;
+        mEncryptionKeyTag = encryptionKeyTag;
     }
 
     /** No special parcel contents. */
@@ -136,6 +157,7 @@
      */
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mDeviceId);
         dest.writeInt(mScanType);
         dest.writeInt(mName == null ? 0 : 1);
         if (mName != null) {
@@ -164,13 +186,24 @@
             dest.writeInt(mSalt.length);
             dest.writeByteArray(mSalt);
         }
+        dest.writeInt(mPresenceDevice == null ? 0 : 1);
+        if (mPresenceDevice != null) {
+            dest.writeParcelable(mPresenceDevice, /* parcelableFlags= */ 0);
+        }
+        dest.writeInt(mEncryptionKeyTag == null ? 0 : 1);
+        if (mEncryptionKeyTag != null) {
+            dest.writeInt(mEncryptionKeyTag.length);
+            dest.writeByteArray(mEncryptionKeyTag);
+        }
     }
 
     /** Returns a string representation of this ScanRequest. */
     @Override
     public String toString() {
         return "NearbyDeviceParcelable["
-                + "scanType="
+                + "deviceId="
+                + mDeviceId
+                + ", scanType="
                 + mScanType
                 + ", name="
                 + mName
@@ -197,20 +230,25 @@
     public boolean equals(Object other) {
         if (other instanceof NearbyDeviceParcelable) {
             NearbyDeviceParcelable otherNearbyDeviceParcelable = (NearbyDeviceParcelable) other;
-            return mScanType == otherNearbyDeviceParcelable.mScanType
+            return  mDeviceId == otherNearbyDeviceParcelable.mDeviceId
+                    && mScanType == otherNearbyDeviceParcelable.mScanType
                     && (Objects.equals(mName, otherNearbyDeviceParcelable.mName))
                     && (mMedium == otherNearbyDeviceParcelable.mMedium)
                     && (mTxPower == otherNearbyDeviceParcelable.mTxPower)
                     && (mRssi == otherNearbyDeviceParcelable.mRssi)
                     && (mAction == otherNearbyDeviceParcelable.mAction)
                     && (Objects.equals(
-                            mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
+                    mPublicCredential, otherNearbyDeviceParcelable.mPublicCredential))
                     && (Objects.equals(
-                            mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
+                    mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
                     && (Objects.equals(
-                            mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
+                    mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
                     && (Arrays.equals(mData, otherNearbyDeviceParcelable.mData))
-                    && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt));
+                    && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt))
+                    && (Objects.equals(
+                    mPresenceDevice, otherNearbyDeviceParcelable.mPresenceDevice))
+                    && (Arrays.equals(
+                    mEncryptionKeyTag, otherNearbyDeviceParcelable.mEncryptionKeyTag));
         }
         return false;
     }
@@ -218,6 +256,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(
+                mDeviceId,
                 mScanType,
                 mName,
                 mMedium,
@@ -227,7 +266,19 @@
                 mBluetoothAddress,
                 mFastPairModelId,
                 Arrays.hashCode(mData),
-                Arrays.hashCode(mSalt));
+                Arrays.hashCode(mSalt),
+                mPresenceDevice,
+                Arrays.hashCode(mEncryptionKeyTag));
+    }
+
+    /**
+     * The id of the device.
+     * <p>This id is not a hardware id. It may rotate based on the remote device's broadcasts.
+     *
+     * @hide
+     */
+    public long getDeviceId() {
+        return mDeviceId;
     }
 
     /**
@@ -351,8 +402,29 @@
         return mSalt;
     }
 
+    /**
+     * Gets the {@link PresenceDevice} Nearby Presence device. This field is
+     * for Fast Pair client only.
+     */
+    @Nullable
+    public PresenceDevice getPresenceDevice() {
+        return mPresenceDevice;
+    }
+
+    /**
+     * Gets the encryption key tag calculated from advertisement
+     * Returns {@code null} if the data is not encrypted or this is not a Presence device.
+     *
+     * Used in Presence.
+     */
+    @Nullable
+    public byte[] getEncryptionKeyTag() {
+        return mEncryptionKeyTag;
+    }
+
     /** Builder class for {@link NearbyDeviceParcelable}. */
     public static final class Builder {
+        private long mDeviceId = -1;
         @Nullable private String mName;
         @NearbyDevice.Medium private int mMedium;
         private int mTxPower;
@@ -364,6 +436,14 @@
         @Nullable private String mBluetoothAddress;
         @Nullable private byte[] mData;
         @Nullable private byte[] mSalt;
+        @Nullable private PresenceDevice mPresenceDevice;
+        @Nullable private byte[] mEncryptionKeyTag;
+
+        /** Sets the id of the device. */
+        public Builder setDeviceId(long deviceId) {
+            this.mDeviceId = deviceId;
+            return this;
+        }
 
         /**
          * Sets the scan type of the NearbyDeviceParcelable.
@@ -469,7 +549,7 @@
         /**
          * Sets the scanned raw data.
          *
-         * @param data Data the scan. For example, {@link ScanRecord#getServiceData()} if scanned by
+         * @param data raw data scanned, like {@link ScanRecord#getServiceData()} if scanned by
          *             Bluetooth.
          */
         @NonNull
@@ -479,6 +559,17 @@
         }
 
         /**
+         * Sets the encryption key tag calculated from the advertisement.
+         *
+         * @param encryptionKeyTag calculated from identity scanned from the advertisement
+         */
+        @NonNull
+        public Builder setEncryptionKeyTag(@Nullable byte[] encryptionKeyTag) {
+            mEncryptionKeyTag = encryptionKeyTag;
+            return this;
+        }
+
+        /**
          * Sets the slat in the advertisement from the Nearby Presence device.
          *
          * @param salt in the advertisement from the Nearby Presence device.
@@ -489,10 +580,22 @@
             return this;
         }
 
+        /**
+         * Sets the {@link PresenceDevice} if there is any.
+         * The current {@link NearbyDeviceParcelable} can be seen as the wrapper of the
+         * {@link PresenceDevice}.
+         */
+        @Nullable
+        public Builder setPresenceDevice(@Nullable PresenceDevice presenceDevice) {
+            mPresenceDevice = presenceDevice;
+            return this;
+        }
+
         /** Builds a ScanResult. */
         @NonNull
         public NearbyDeviceParcelable build() {
             return new NearbyDeviceParcelable(
+                    mDeviceId,
                     mScanType,
                     mName,
                     mMedium,
@@ -503,7 +606,9 @@
                     mFastPairModelId,
                     mBluetoothAddress,
                     mData,
-                    mSalt);
+                    mSalt,
+                    mPresenceDevice,
+                    mEncryptionKeyTag);
         }
     }
 }
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 106c290..a70b303 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -26,6 +26,7 @@
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.content.Context;
+import android.nearby.aidl.IOffloadCallback;
 import android.os.RemoteException;
 import android.provider.Settings;
 import android.util.Log;
@@ -37,6 +38,7 @@
 import java.util.Objects;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * This class provides a way to perform Nearby related operations such as scanning, broadcasting
@@ -62,7 +64,7 @@
             ScanStatus.ERROR,
     })
     public @interface ScanStatus {
-        // Default, invalid state.
+        // The undetermined status, some modules may be initializing. Retry is suggested.
         int UNKNOWN = 0;
         // The successful state.
         int SUCCESS = 1;
@@ -73,6 +75,7 @@
     private static final String TAG = "NearbyManager";
 
     /**
+     * TODO(b/286137024): Remove this when CTS R5 is rolled out.
      * Whether allows Fast Pair to scan.
      *
      * (0 = disabled, 1 = enabled)
@@ -103,6 +106,9 @@
         mService = service;
     }
 
+    // This can be null when NearbyDeviceParcelable field not set for Presence device
+    // or the scan type is not recognized.
+    @Nullable
     private static NearbyDevice toClientNearbyDevice(
             NearbyDeviceParcelable nearbyDeviceParcelable,
             @ScanRequest.ScanType int scanType) {
@@ -118,23 +124,12 @@
         }
 
         if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) {
-            PublicCredential publicCredential = nearbyDeviceParcelable.getPublicCredential();
-            if (publicCredential == null) {
-                return null;
+            PresenceDevice presenceDevice = nearbyDeviceParcelable.getPresenceDevice();
+            if (presenceDevice == null) {
+                Log.e(TAG,
+                        "Cannot find any Presence device in discovered NearbyDeviceParcelable");
             }
-            byte[] salt = nearbyDeviceParcelable.getSalt();
-            if (salt == null) {
-                salt = new byte[0];
-            }
-            return new PresenceDevice.Builder(
-                    // Use the public credential hash as the device Id.
-                    String.valueOf(publicCredential.hashCode()),
-                    salt,
-                    publicCredential.getSecretId(),
-                    publicCredential.getEncryptedMetadata())
-                    .setRssi(nearbyDeviceParcelable.getRssi())
-                    .addMedium(nearbyDeviceParcelable.getMedium())
-                    .build();
+            return presenceDevice;
         }
         return null;
     }
@@ -278,29 +273,42 @@
     }
 
     /**
-     * Read from {@link Settings} whether Fast Pair scan is enabled.
+     * Query offload capability in a device. The query is asynchronous and result is called back
+     * in {@link Consumer}, which is set to true if offload is supported.
      *
-     * @param context the {@link Context} to query the setting
-     * @return whether the Fast Pair is enabled
-     * @hide
+     * @param executor the callback will take place on this {@link Executor}
+     * @param callback the callback invoked with {@link OffloadCapability}
      */
-    public static boolean getFastPairScanEnabled(@NonNull Context context) {
-        final int enabled = Settings.Secure.getInt(
-                context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
-        return enabled != 0;
+    public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<OffloadCapability> callback) {
+        try {
+            mService.queryOffloadCapability(new OffloadTransport(executor, callback));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
-    /**
-     * Write into {@link Settings} whether Fast Pair scan is enabled
-     *
-     * @param context the {@link Context} to set the setting
-     * @param enable whether the Fast Pair scan should be enabled
-     * @hide
-     */
-    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
-    public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
-        Settings.Secure.putInt(
-                context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+    private static class OffloadTransport extends IOffloadCallback.Stub {
+
+        private final Executor mExecutor;
+        // Null when cancelled
+        volatile @Nullable Consumer<OffloadCapability> mConsumer;
+
+        OffloadTransport(Executor executor, Consumer<OffloadCapability> consumer) {
+            Preconditions.checkArgument(executor != null, "illegal null executor");
+            Preconditions.checkArgument(consumer != null, "illegal null consumer");
+            mExecutor = executor;
+            mConsumer = consumer;
+        }
+
+        @Override
+        public void onQueryComplete(OffloadCapability capability) {
+            mExecutor.execute(() -> {
+                if (mConsumer != null) {
+                    mConsumer.accept(capability);
+                }
+            });
+        }
     }
 
     private static class ScanListenerTransport extends IScanListener.Stub {
@@ -339,9 +347,9 @@
         public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
                 throws RemoteException {
             mExecutor.execute(() -> {
-                if (mScanCallback != null) {
-                    mScanCallback.onDiscovered(
-                            toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+                NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+                if (mScanCallback != null && nearbyDevice != null) {
+                    mScanCallback.onDiscovered(nearbyDevice);
                 }
             });
         }
@@ -350,7 +358,8 @@
         public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
                 throws RemoteException {
             mExecutor.execute(() -> {
-                if (mScanCallback != null) {
+                NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+                if (mScanCallback != null && nearbyDevice != null) {
                     mScanCallback.onUpdated(
                             toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
                 }
@@ -360,7 +369,8 @@
         @Override
         public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
             mExecutor.execute(() -> {
-                if (mScanCallback != null) {
+                NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+                if (mScanCallback != null && nearbyDevice != null) {
                     mScanCallback.onLost(
                             toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
                 }
@@ -368,10 +378,10 @@
         }
 
         @Override
-        public void onError() {
+        public void onError(int errorCode) {
             mExecutor.execute(() -> {
                 if (mScanCallback != null) {
-                    Log.e("NearbyManager", "onError: There is an error in scan.");
+                    mScanCallback.onError(errorCode);
                 }
             });
         }
@@ -410,4 +420,35 @@
             });
         }
     }
+
+    /**
+     * TODO(b/286137024): Remove this when CTS R5 is rolled out.
+     * Read from {@link Settings} whether Fast Pair scan is enabled.
+     *
+     * @param context the {@link Context} to query the setting
+     * @return whether the Fast Pair is enabled
+     * @hide
+     */
+    public static boolean getFastPairScanEnabled(@NonNull Context context) {
+        final int enabled = Settings.Secure.getInt(
+                context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0);
+        return enabled != 0;
+    }
+
+    /**
+     * TODO(b/286137024): Remove this when CTS R5 is rolled out.
+     * Write into {@link Settings} whether Fast Pair scan is enabled
+     *
+     * @param context the {@link Context} to set the setting
+     * @param enable whether the Fast Pair scan should be enabled
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) {
+        Settings.Secure.putInt(
+                context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0);
+        Log.v(TAG, String.format(
+                "successfully %s Fast Pair scan", enable ? "enables" : "disables"));
+    }
+
 }
diff --git a/nearby/framework/java/android/nearby/OffloadCapability.aidl b/nearby/framework/java/android/nearby/OffloadCapability.aidl
new file mode 100644
index 0000000..fe1c45e
--- /dev/null
+++ b/nearby/framework/java/android/nearby/OffloadCapability.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+/**
+ * A class that can describe what offload functions are available.
+ *
+ * {@hide}
+ */
+parcelable OffloadCapability;
+
diff --git a/nearby/framework/java/android/nearby/OffloadCapability.java b/nearby/framework/java/android/nearby/OffloadCapability.java
new file mode 100644
index 0000000..9071c1c
--- /dev/null
+++ b/nearby/framework/java/android/nearby/OffloadCapability.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class that can describe what offload functions are available.
+ *
+ * @hide
+ */
+@SystemApi
+public final class OffloadCapability implements Parcelable {
+    private final boolean mFastPairSupported;
+    private final boolean mNearbyShareSupported;
+    private final long mVersion;
+
+    public boolean isFastPairSupported() {
+        return mFastPairSupported;
+    }
+
+    public boolean isNearbyShareSupported() {
+        return mNearbyShareSupported;
+    }
+
+    public long getVersion() {
+        return mVersion;
+    }
+
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeBoolean(mFastPairSupported);
+        dest.writeBoolean(mNearbyShareSupported);
+        dest.writeLong(mVersion);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<OffloadCapability> CREATOR = new Creator<OffloadCapability>() {
+        @Override
+        public OffloadCapability createFromParcel(Parcel in) {
+            boolean isFastPairSupported = in.readBoolean();
+            boolean isNearbyShareSupported = in.readBoolean();
+            long version = in.readLong();
+            return new Builder()
+                    .setFastPairSupported(isFastPairSupported)
+                    .setNearbyShareSupported(isNearbyShareSupported)
+                    .setVersion(version)
+                    .build();
+        }
+
+        @Override
+        public OffloadCapability[] newArray(int size) {
+            return new OffloadCapability[size];
+        }
+    };
+
+    private OffloadCapability(boolean fastPairSupported, boolean nearbyShareSupported,
+            long version) {
+        mFastPairSupported = fastPairSupported;
+        mNearbyShareSupported = nearbyShareSupported;
+        mVersion = version;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof OffloadCapability)) return false;
+        OffloadCapability that = (OffloadCapability) o;
+        return isFastPairSupported() == that.isFastPairSupported()
+                && isNearbyShareSupported() == that.isNearbyShareSupported()
+                && getVersion() == that.getVersion();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(isFastPairSupported(), isNearbyShareSupported(), getVersion());
+    }
+
+    @Override
+    public String toString() {
+        return "OffloadCapability{"
+                + "fastPairSupported=" + mFastPairSupported
+                + ", nearbyShareSupported=" + mNearbyShareSupported
+                + ", version=" + mVersion
+                + '}';
+    }
+
+    /**
+     * Builder class for {@link OffloadCapability}.
+     */
+    public static final class Builder {
+        private boolean mFastPairSupported;
+        private boolean mNearbyShareSupported;
+        private long mVersion;
+
+        /**
+         * Sets if the Nearby Share feature is supported
+         *
+         * @param fastPairSupported {@code true} if the Fast Pair feature is supported
+         */
+        @NonNull
+        public Builder setFastPairSupported(boolean fastPairSupported) {
+            mFastPairSupported = fastPairSupported;
+            return this;
+        }
+
+        /**
+         * Sets if the Nearby Share feature is supported.
+         *
+         * @param nearbyShareSupported {@code true} if the Nearby Share feature is supported
+         */
+        @NonNull
+        public Builder setNearbyShareSupported(boolean nearbyShareSupported) {
+            mNearbyShareSupported = nearbyShareSupported;
+            return this;
+        }
+
+        /**
+         * Sets the version number of Nearby Offload.
+         *
+         * @param version Nearby Offload version number
+         */
+        @NonNull
+        public Builder setVersion(long version) {
+            mVersion = version;
+            return this;
+        }
+
+        /**
+         * Builds an OffloadCapability object.
+         */
+        @NonNull
+        public OffloadCapability build() {
+            return new OffloadCapability(mFastPairSupported, mNearbyShareSupported, mVersion);
+        }
+    }
+}
diff --git a/nearby/framework/java/android/nearby/PresenceDevice.java b/nearby/framework/java/android/nearby/PresenceDevice.java
index cb406e4..b5d9ad4 100644
--- a/nearby/framework/java/android/nearby/PresenceDevice.java
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -26,6 +26,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
@@ -134,6 +135,54 @@
         return mExtendedProperties;
     }
 
+    /**
+     * This can only be hidden because this is the System API,
+     * which cannot be changed in T timeline.
+     *
+     * @hide
+     */
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof PresenceDevice) {
+            PresenceDevice otherDevice = (PresenceDevice) other;
+            if (super.equals(otherDevice)) {
+                return Arrays.equals(mSalt, otherDevice.mSalt)
+                        && Arrays.equals(mSecretId, otherDevice.mSecretId)
+                        && Arrays.equals(mEncryptedIdentity, otherDevice.mEncryptedIdentity)
+                        && Objects.equals(mDeviceId, otherDevice.mDeviceId)
+                        && mDeviceType == otherDevice.mDeviceType
+                        && Objects.equals(mDeviceImageUrl, otherDevice.mDeviceImageUrl)
+                        && mDiscoveryTimestampMillis == otherDevice.mDiscoveryTimestampMillis
+                        && Objects.equals(mExtendedProperties, otherDevice.mExtendedProperties);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This can only be hidden because this is the System API,
+     * which cannot be changed in T timeline.
+     *
+     * @hide
+     *
+     * @return The unique hash value of the {@link PresenceDevice}
+     */
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                getName(),
+                getMediums(),
+                getRssi(),
+                Arrays.hashCode(mSalt),
+                Arrays.hashCode(mSecretId),
+                Arrays.hashCode(mEncryptedIdentity),
+                mDeviceId,
+                mDeviceType,
+                mDeviceImageUrl,
+                mDiscoveryTimestampMillis,
+                mExtendedProperties);
+    }
+
     private PresenceDevice(String deviceName, List<Integer> mMediums, int rssi, String deviceId,
             byte[] salt, byte[] secretId, byte[] encryptedIdentity, int deviceType,
             String deviceImageUrl, long discoveryTimestampMillis,
@@ -326,7 +375,6 @@
             return this;
         }
 
-
         /**
          * Sets the image url of the discovered Presence device.
          *
@@ -338,7 +386,6 @@
             return this;
         }
 
-
         /**
          * Sets discovery timestamp, the clock is based on elapsed time.
          *
@@ -350,7 +397,6 @@
             return this;
         }
 
-
         /**
          * Adds an extended property of the discovered presence device.
          *
diff --git a/nearby/framework/java/android/nearby/PresenceScanFilter.java b/nearby/framework/java/android/nearby/PresenceScanFilter.java
index f0c3c06..50e97b4 100644
--- a/nearby/framework/java/android/nearby/PresenceScanFilter.java
+++ b/nearby/framework/java/android/nearby/PresenceScanFilter.java
@@ -71,7 +71,7 @@
         super(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE, rssiThreshold);
         mCredentials = new ArrayList<>(credentials);
         mPresenceActions = new ArrayList<>(presenceActions);
-        mExtendedProperties = extendedProperties;
+        mExtendedProperties = new ArrayList<>(extendedProperties);
     }
 
     private PresenceScanFilter(Parcel in) {
@@ -132,7 +132,7 @@
         }
         dest.writeInt(mExtendedProperties.size());
         if (!mExtendedProperties.isEmpty()) {
-            dest.writeList(mExtendedProperties);
+            dest.writeParcelableList(mExtendedProperties, 0);
         }
     }
 
diff --git a/nearby/framework/java/android/nearby/ScanCallback.java b/nearby/framework/java/android/nearby/ScanCallback.java
index 1b1b4bc..7b66607 100644
--- a/nearby/framework/java/android/nearby/ScanCallback.java
+++ b/nearby/framework/java/android/nearby/ScanCallback.java
@@ -16,9 +16,13 @@
 
 package android.nearby;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Reports newly discovered devices.
  * Note: The frequency of the callback is dependent on whether the caller
@@ -31,6 +35,37 @@
  */
 @SystemApi
 public interface ScanCallback {
+
+    /** General error code for scan. */
+    int ERROR_UNKNOWN = 0;
+
+    /**
+     * Scan failed as the request is not supported.
+     */
+    int ERROR_UNSUPPORTED = 1;
+
+    /**
+     * Invalid argument such as out-of-range, illegal format etc.
+     */
+    int ERROR_INVALID_ARGUMENT = 2;
+
+    /**
+     * Request from clients who do not have permissions.
+     */
+    int ERROR_PERMISSION_DENIED = 3;
+
+    /**
+     * Request cannot be fulfilled due to limited resource.
+     */
+    int ERROR_RESOURCE_EXHAUSTED = 4;
+
+    /** @hide **/
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ERROR_UNKNOWN, ERROR_UNSUPPORTED, ERROR_INVALID_ARGUMENT, ERROR_PERMISSION_DENIED,
+            ERROR_RESOURCE_EXHAUSTED})
+    @interface ErrorCode {
+    }
+
     /**
      * Reports a {@link NearbyDevice} being discovered.
      *
@@ -51,4 +86,11 @@
      * @param device {@link NearbyDevice} that is lost.
      */
     void onLost(@NonNull NearbyDevice device);
+
+    /**
+     * Notifies clients of error from the scan.
+     *
+     * @param errorCode defined by Nearby
+     */
+    default void onError(@ErrorCode int errorCode) {}
 }
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
index c717ac7..61cbf39 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
+import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.WorkSource;
@@ -33,6 +34,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * An encapsulation of various parameters for requesting nearby scans.
@@ -62,6 +65,12 @@
      */
     public static final int SCAN_MODE_NO_POWER = -1;
     /**
+     * A special scan mode to indicate that client only wants to use CHRE to scan.
+     *
+     * @hide
+     */
+    public static final int SCAN_MODE_CHRE_ONLY = 3;
+    /**
      * Used to read a ScanRequest from a Parcel.
      */
     @NonNull
@@ -72,6 +81,7 @@
                     .setScanType(in.readInt())
                     .setScanMode(in.readInt())
                     .setBleEnabled(in.readBoolean())
+                    .setOffloadOnly(in.readBoolean())
                     .setWorkSource(in.readTypedObject(WorkSource.CREATOR));
             final int size = in.readInt();
             for (int i = 0; i < size; i++) {
@@ -89,14 +99,16 @@
     private final @ScanType int mScanType;
     private final @ScanMode int mScanMode;
     private final boolean mBleEnabled;
+    private final boolean mOffloadOnly;
     private final @NonNull WorkSource mWorkSource;
     private final List<ScanFilter> mScanFilters;
 
     private ScanRequest(@ScanType int scanType, @ScanMode int scanMode, boolean bleEnabled,
-            @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
+            boolean offloadOnly, @NonNull WorkSource workSource, List<ScanFilter> scanFilters) {
         mScanType = scanType;
         mScanMode = scanMode;
         mBleEnabled = bleEnabled;
+        mOffloadOnly = offloadOnly;
         mWorkSource = workSource;
         mScanFilters = scanFilters;
     }
@@ -162,6 +174,13 @@
     }
 
     /**
+     * Returns if CHRE enabled for scanning.
+     */
+    public boolean isOffloadOnly() {
+        return mOffloadOnly;
+    }
+
+    /**
      * Returns Scan Filters for this request.
      */
     @NonNull
@@ -197,7 +216,13 @@
         stringBuilder.append("Request[")
                 .append("scanType=").append(mScanType);
         stringBuilder.append(", scanMode=").append(scanModeToString(mScanMode));
-        stringBuilder.append(", enableBle=").append(mBleEnabled);
+        // TODO(b/286137024): Remove this when CTS R5 is rolled out.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            stringBuilder.append(", bleEnabled=").append(mBleEnabled);
+            stringBuilder.append(", offloadOnly=").append(mOffloadOnly);
+        } else {
+            stringBuilder.append(", enableBle=").append(mBleEnabled);
+        }
         stringBuilder.append(", workSource=").append(mWorkSource);
         stringBuilder.append(", scanFilters=").append(mScanFilters);
         stringBuilder.append("]");
@@ -209,6 +234,7 @@
         dest.writeInt(mScanType);
         dest.writeInt(mScanMode);
         dest.writeBoolean(mBleEnabled);
+        dest.writeBoolean(mOffloadOnly);
         dest.writeTypedObject(mWorkSource, /* parcelableFlags= */0);
         final int size = mScanFilters.size();
         dest.writeInt(size);
@@ -224,6 +250,7 @@
             return mScanType == otherRequest.mScanType
                     && (mScanMode == otherRequest.mScanMode)
                     && (mBleEnabled == otherRequest.mBleEnabled)
+                    && (mOffloadOnly == otherRequest.mOffloadOnly)
                     && (Objects.equals(mWorkSource, otherRequest.mWorkSource));
         }
         return false;
@@ -231,7 +258,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mScanType, mScanMode, mBleEnabled, mWorkSource);
+        return Objects.hash(mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource);
     }
 
     /** @hide **/
@@ -254,6 +281,7 @@
         private @ScanMode int mScanMode;
 
         private boolean mBleEnabled;
+        private boolean mOffloadOnly;
         private WorkSource mWorkSource;
         private List<ScanFilter> mScanFilters;
 
@@ -261,6 +289,7 @@
         public Builder() {
             mScanType = INVALID_SCAN_TYPE;
             mBleEnabled = true;
+            mOffloadOnly = false;
             mWorkSource = new WorkSource();
             mScanFilters = new ArrayList<>();
         }
@@ -301,6 +330,22 @@
         }
 
         /**
+         * By default, a scan request can be served by either offload or
+         * non-offload implementation, depending on the resource available in the device.
+         *
+         * A client can explicitly request a scan to be served by offload only.
+         * Before the request, the client should query the offload capability by
+         * using {@link NearbyManager#queryOffloadCapability(Executor, Consumer)}}. Otherwise,
+         * {@link ScanCallback#ERROR_UNSUPPORTED} will be returned on devices without
+         * offload capability.
+         */
+        @NonNull
+        public Builder setOffloadOnly(boolean offloadOnly) {
+            mOffloadOnly = offloadOnly;
+            return this;
+        }
+
+        /**
          * Sets the work source to use for power attribution for this scan request. Defaults to
          * empty work source, which implies the caller that sends the scan request will be used
          * for power attribution.
@@ -355,7 +400,8 @@
             Preconditions.checkState(isValidScanMode(mScanMode),
                     "invalid scan mode : " + mScanMode
                             + ", scan mode must be one of ScanMode#SCAN_MODE_");
-            return new ScanRequest(mScanType, mScanMode, mBleEnabled, mWorkSource, mScanFilters);
+            return new ScanRequest(
+                    mScanType, mScanMode, mBleEnabled, mOffloadOnly, mWorkSource, mScanFilters);
         }
     }
 }
diff --git a/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl b/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl
new file mode 100644
index 0000000..8bef817
--- /dev/null
+++ b/nearby/framework/java/android/nearby/aidl/IOffloadCallback.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.aidl;
+
+import android.nearby.OffloadCapability;
+
+/**
+ * Listener for offload queries.
+ *
+ * {@hide}
+ */
+oneway interface IOffloadCallback {
+        /** Invokes when ContextHub transaction completes. */
+        void onQueryComplete(in OffloadCapability capability);
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 8fdac87..9b32d69 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -16,9 +16,16 @@
 
 package com.android.server.nearby;
 
+import android.os.Build;
 import android.provider.DeviceConfig;
 
-import androidx.annotation.VisibleForTesting;
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
+
+import java.util.concurrent.Executors;
 
 /**
  * A utility class for encapsulating Nearby feature flag configurations.
@@ -26,33 +33,123 @@
 public class NearbyConfiguration {
 
     /**
-     * Flag use to enable presence legacy broadcast.
+     * Flag used to enable presence legacy broadcast.
      */
     public static final String NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY =
             "nearby_enable_presence_broadcast_legacy";
+    /**
+     * Flag used to for minimum nano app version to make Nearby CHRE scan work.
+     */
+    public static final String NEARBY_MAINLINE_NANO_APP_MIN_VERSION =
+            "nearby_mainline_nano_app_min_version";
 
+    /**
+     * Flag used to allow test mode and customization.
+     */
+    public static final String NEARBY_SUPPORT_TEST_APP = "nearby_support_test_app";
+
+    /**
+     * Flag to control which version of DiscoveryProviderManager should be used.
+     */
+    public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
+            "nearby_refactor_discovery_manager";
+
+    private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
+
+    private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
+    private final Object mDeviceConfigLock = new Object();
+
+    @GuardedBy("mDeviceConfigLock")
     private boolean mEnablePresenceBroadcastLegacy;
+    @GuardedBy("mDeviceConfigLock")
+    private int mNanoAppMinVersion;
+    @GuardedBy("mDeviceConfigLock")
+    private boolean mSupportTestApp;
+    @GuardedBy("mDeviceConfigLock")
+    private boolean mRefactorDiscoveryManager;
 
     public NearbyConfiguration() {
-        mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
-                NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+        mDeviceConfigListener.start();
+    }
 
+    /**
+     * Returns the DeviceConfig namespace for Nearby. The {@link DeviceConfig#NAMESPACE_NEARBY} was
+     * added in UpsideDownCake, in Tiramisu, we use {@link DeviceConfig#NAMESPACE_TETHERING}.
+     */
+    public static String getNamespace() {
+        if (SdkLevel.isAtLeastU()) {
+            return DeviceConfig.NAMESPACE_NEARBY;
+        }
+        return DeviceConfig.NAMESPACE_TETHERING;
+    }
+
+    private static boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+        final String value = getDeviceConfigProperty(name);
+        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    private static int getDeviceConfigInt(final String name, final int defaultValue) {
+        final String value = getDeviceConfigProperty(name);
+        return value != null ? Integer.parseInt(value) : defaultValue;
+    }
+
+    private static String getDeviceConfigProperty(String name) {
+        return DeviceConfig.getProperty(getNamespace(), name);
     }
 
     /**
      * Returns whether broadcasting legacy presence spec is enabled.
      */
     public boolean isPresenceBroadcastLegacyEnabled() {
-        return mEnablePresenceBroadcastLegacy;
+        synchronized (mDeviceConfigLock) {
+            return mEnablePresenceBroadcastLegacy;
+        }
     }
 
-    private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
-        final String value = getDeviceConfigProperty(name);
-        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    public int getNanoAppMinVersion() {
+        synchronized (mDeviceConfigLock) {
+            return mNanoAppMinVersion;
+        }
     }
 
-    @VisibleForTesting
-    protected String getDeviceConfigProperty(String name) {
-        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
+    /**
+     * @return {@code true} when in test mode and allows customization.
+     */
+    public boolean isTestAppSupported() {
+        synchronized (mDeviceConfigLock) {
+            return mSupportTestApp;
+        }
+    }
+
+    /**
+     * @return {@code true} if use {@link DiscoveryProviderManager} or use
+     * DiscoveryProviderManagerLegacy if {@code false}.
+     */
+    public boolean refactorDiscoveryManager() {
+        synchronized (mDeviceConfigLock) {
+            return mRefactorDiscoveryManager;
+        }
+    }
+
+    private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+        public void start() {
+            DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
+                    Executors.newSingleThreadExecutor(), this);
+            onPropertiesChanged(DeviceConfig.getProperties(getNamespace()));
+        }
+
+        @Override
+        public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+            synchronized (mDeviceConfigLock) {
+                mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
+                        NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+                mNanoAppMinVersion = getDeviceConfigInt(
+                        NEARBY_MAINLINE_NANO_APP_MIN_VERSION, 0 /* defaultValue */);
+                mSupportTestApp = !IS_USER_BUILD && getDeviceConfigBoolean(
+                        NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+                mRefactorDiscoveryManager = getDeviceConfigBoolean(
+                        NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+            }
+        }
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 1220104..3c183ec 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.hardware.location.ContextHubManager;
 import android.nearby.BroadcastRequestParcelable;
 import android.nearby.IBroadcastListener;
@@ -35,13 +36,16 @@
 import android.nearby.IScanListener;
 import android.nearby.NearbyManager;
 import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
 import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.provider.BroadcastProviderManager;
-import com.android.server.nearby.provider.DiscoveryProviderManager;
+import com.android.server.nearby.managers.BroadcastProviderManager;
+import com.android.server.nearby.managers.DiscoveryManager;
+import com.android.server.nearby.managers.DiscoveryProviderManager;
+import com.android.server.nearby.managers.DiscoveryProviderManagerLegacy;
+import com.android.server.nearby.presence.PresenceManager;
 import com.android.server.nearby.util.identity.CallerIdentity;
 import com.android.server.nearby.util.permissions.BroadcastPermissions;
 import com.android.server.nearby.util.permissions.DiscoveryPermissions;
@@ -49,8 +53,12 @@
 /** Service implementing nearby functionality. */
 public class NearbyService extends INearbyManager.Stub {
     public static final String TAG = "NearbyService";
+    // Sets to true to start BLE scan from PresenceManager for manual testing.
+    public static final Boolean MANUAL_TEST = false;
 
     private final Context mContext;
+    private final PresenceManager mPresenceManager;
+    private final NearbyConfiguration mNearbyConfiguration;
     private Injector mInjector;
     private final BroadcastReceiver mBluetoothReceiver =
             new BroadcastReceiver() {
@@ -69,14 +77,19 @@
                     }
                 }
             };
-    private DiscoveryProviderManager mProviderManager;
-    private BroadcastProviderManager mBroadcastProviderManager;
+    private final DiscoveryManager mDiscoveryProviderManager;
+    private final BroadcastProviderManager mBroadcastProviderManager;
 
     public NearbyService(Context context) {
         mContext = context;
         mInjector = new SystemInjector(context);
-        mProviderManager = new DiscoveryProviderManager(context, mInjector);
         mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
+        mPresenceManager = new PresenceManager(context);
+        mNearbyConfiguration = new NearbyConfiguration();
+        mDiscoveryProviderManager =
+                mNearbyConfiguration.refactorDiscoveryManager()
+                        ? new DiscoveryProviderManager(context, mInjector)
+                        : new DiscoveryProviderManagerLegacy(context, mInjector);
     }
 
     @VisibleForTesting
@@ -93,10 +106,7 @@
         CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
         DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
 
-        if (mProviderManager.registerScanListener(scanRequest, listener, identity)) {
-            return NearbyManager.ScanStatus.SUCCESS;
-        }
-        return NearbyManager.ScanStatus.ERROR;
+        return mDiscoveryProviderManager.registerScanListener(scanRequest, listener, identity);
     }
 
     @Override
@@ -107,7 +117,7 @@
         CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
         DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
 
-        mProviderManager.unregisterScanListener(listener);
+        mDiscoveryProviderManager.unregisterScanListener(listener);
     }
 
     @Override
@@ -133,6 +143,11 @@
         mBroadcastProviderManager.stopBroadcast(listener);
     }
 
+    @Override
+    public void queryOffloadCapability(IOffloadCallback callback) {
+        mDiscoveryProviderManager.queryOffloadCapability(callback);
+    }
+
     /**
      * Called by the service initializer.
      *
@@ -146,15 +161,21 @@
                 }
                 break;
             case PHASE_BOOT_COMPLETED:
+                // mInjector needs to be initialized before mProviderManager.
                 if (mInjector instanceof SystemInjector) {
                     // The nearby service must be functioning after this boot phase.
                     ((SystemInjector) mInjector).initializeBluetoothAdapter();
                     // Initialize ContextManager for CHRE scan.
-                    ((SystemInjector) mInjector).initializeContextHubManagerAdapter();
+                    ((SystemInjector) mInjector).initializeContextHubManager();
                 }
+                mDiscoveryProviderManager.init();
                 mContext.registerReceiver(
                         mBluetoothReceiver,
                         new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
+                // Only enable for manual Presence test on device.
+                if (MANUAL_TEST) {
+                    mPresenceManager.initiate();
+                }
                 break;
         }
     }
@@ -165,16 +186,18 @@
      * throw a {@link SecurityException}.
      */
     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
-    private static void enforceBluetoothPrivilegedPermission(Context context) {
-        context.enforceCallingOrSelfPermission(
-                android.Manifest.permission.BLUETOOTH_PRIVILEGED,
-                "Need BLUETOOTH PRIVILEGED permission");
+    private void enforceBluetoothPrivilegedPermission(Context context) {
+        if (!mNearbyConfiguration.isTestAppSupported()) {
+            context.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+                    "Need BLUETOOTH PRIVILEGED permission");
+        }
     }
 
     private static final class SystemInjector implements Injector {
         private final Context mContext;
         @Nullable private BluetoothAdapter mBluetoothAdapter;
-        @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
+        @Nullable private ContextHubManager mContextHubManager;
         @Nullable private AppOpsManager mAppOpsManager;
 
         SystemInjector(Context context) {
@@ -189,8 +212,8 @@
 
         @Override
         @Nullable
-        public ContextHubManagerAdapter getContextHubManagerAdapter() {
-            return mContextHubManagerAdapter;
+        public ContextHubManager getContextHubManager() {
+            return mContextHubManager;
         }
 
         @Override
@@ -210,15 +233,13 @@
             mBluetoothAdapter = manager.getAdapter();
         }
 
-        synchronized void initializeContextHubManagerAdapter() {
-            if (mContextHubManagerAdapter != null) {
+        synchronized void initializeContextHubManager() {
+            if (mContextHubManager != null) {
                 return;
             }
-            ContextHubManager manager = mContext.getSystemService(ContextHubManager.class);
-            if (manager == null) {
-                return;
+            if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONTEXT_HUB)) {
+                mContextHubManager = mContext.getSystemService(ContextHubManager.class);
             }
-            mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
         }
 
         synchronized void initializeAppOpsManager() {
diff --git a/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java b/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java
new file mode 100644
index 0000000..00d1570
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/CancelableAlarm.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import android.util.Log;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+
+/**
+ * A cancelable alarm with a name. This is a simple wrapper around the logic for posting a runnable
+ * on a scheduled executor service and (possibly) later canceling it.
+ */
+public class CancelableAlarm {
+
+    private static final String TAG = "NearbyCancelableAlarm";
+
+    private final String mName;
+    private final Runnable mRunnable;
+    private final long mDelayMillis;
+    private final ScheduledExecutorService mExecutor;
+    private final boolean mIsRecurring;
+
+    // The future containing the alarm.
+    private volatile ScheduledFuture<?> mFuture;
+
+    private CancellationFlag mCancellationFlag;
+
+    private CancelableAlarm(
+            String name,
+            Runnable runnable,
+            long delayMillis,
+            ScheduledExecutorService executor,
+            boolean isRecurring) {
+        this.mName = name;
+        this.mRunnable = runnable;
+        this.mDelayMillis = delayMillis;
+        this.mExecutor = executor;
+        this.mIsRecurring = isRecurring;
+    }
+
+    /**
+     * Creates an alarm.
+     *
+     * @param name the task name
+     * @param runnable command the task to execute
+     * @param delayMillis delay the time from now to delay execution
+     * @param executor the executor that schedules commands to run
+     */
+    public static CancelableAlarm createSingleAlarm(
+            String name,
+            Runnable runnable,
+            long delayMillis,
+            ScheduledExecutorService executor) {
+        CancelableAlarm cancelableAlarm =
+                new CancelableAlarm(name, runnable, delayMillis, executor, /* isRecurring= */
+                        false);
+        cancelableAlarm.scheduleExecutor();
+        return cancelableAlarm;
+    }
+
+    /**
+     * Creates a recurring alarm.
+     *
+     * @param name the task name
+     * @param runnable command the task to execute
+     * @param delayMillis delay the time from now to delay execution
+     * @param executor the executor that schedules commands to run
+     */
+    public static CancelableAlarm createRecurringAlarm(
+            String name,
+            Runnable runnable,
+            long delayMillis,
+            ScheduledExecutorService executor) {
+        CancelableAlarm cancelableAlarm =
+                new CancelableAlarm(name, runnable, delayMillis, executor, /* isRecurring= */ true);
+        cancelableAlarm.scheduleExecutor();
+        return cancelableAlarm;
+    }
+
+    // A reference to "this" should generally not be passed to another class within the constructor
+    // as it may not have completed being constructed.
+    private void scheduleExecutor() {
+        this.mFuture = mExecutor.schedule(this::processAlarm, mDelayMillis, MILLISECONDS);
+        // For tests to pass (NearbySharingChimeraServiceTest) the Cancellation Flag must come
+       // after the
+        // executor.  Doing so prevents the test code from running the callback immediately.
+        this.mCancellationFlag = new CancellationFlag();
+    }
+
+    /**
+     * Cancels the pending alarm.
+     *
+     * @return true if the alarm was canceled, or false if there was a problem canceling the alarm.
+     */
+    public boolean cancel() {
+        mCancellationFlag.cancel();
+        try {
+            return mFuture.cancel(/* mayInterruptIfRunning= */ true);
+        } finally {
+            Log.v(TAG, "Canceled " + mName + " alarm");
+        }
+    }
+
+    private void processAlarm() {
+        if (mCancellationFlag.isCancelled()) {
+            Log.v(TAG, "Ignoring " + mName + " alarm because it has previously been canceled");
+            return;
+        }
+
+        Log.v(TAG, "Running " + mName + " alarm");
+        mRunnable.run();
+        if (mIsRecurring) {
+            this.mFuture = mExecutor.schedule(this::processAlarm, mDelayMillis, MILLISECONDS);
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java b/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java
new file mode 100644
index 0000000..f0bb075
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/CancellationFlag.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import android.util.ArraySet;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A cancellation flag to mark an operation has been cancelled and should be cleaned up as soon as
+ * possible.
+ */
+public class CancellationFlag {
+
+    private final Set<OnCancelListener> mListeners = new ArraySet<>();
+    private final AtomicBoolean mIsCancelled = new AtomicBoolean();
+
+    public CancellationFlag() {
+        this(false);
+    }
+
+    public CancellationFlag(boolean isCancelled) {
+        this.mIsCancelled.set(isCancelled);
+    }
+
+    /** Set the flag as cancelled. */
+    public void cancel() {
+        if (mIsCancelled.getAndSet(true)) {
+            // Someone already cancelled. Return immediately.
+            return;
+        }
+
+        // Don't invoke OnCancelListener#onCancel inside the synchronization block, as it makes
+        // deadlocks more likely.
+        Set<OnCancelListener> clonedListeners;
+        synchronized (this) {
+            clonedListeners = new ArraySet<>(mListeners);
+        }
+        for (OnCancelListener listener : clonedListeners) {
+            listener.onCancel();
+        }
+    }
+
+    /** Returns {@code true} if the flag has been set to cancelled. */
+    public synchronized boolean isCancelled() {
+        return mIsCancelled.get();
+    }
+
+    /** Returns the flag as an {@link AtomicBoolean} object. */
+    public synchronized AtomicBoolean asAtomicBoolean() {
+        return mIsCancelled;
+    }
+
+    /** Registers a {@link OnCancelListener} to listen to cancel() event. */
+    public synchronized void registerOnCancelListener(OnCancelListener listener) {
+        mListeners.add(listener);
+    }
+
+    /**
+     * Unregisters a {@link OnCancelListener} that was previously registed through {@link
+     * #registerOnCancelListener(OnCancelListener)}.
+     */
+    public synchronized void unregisterOnCancelListener(OnCancelListener listener) {
+        mListeners.remove(listener);
+    }
+
+    /** Listens to {@link CancellationFlag#cancel()}. */
+    public interface OnCancelListener {
+        /**
+         * When CancellationFlag is canceled.
+         */
+        void onCancel();
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/injector/Injector.java b/nearby/service/java/com/android/server/nearby/injector/Injector.java
index 57784a9..3152ee6 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -18,6 +18,7 @@
 
 import android.app.AppOpsManager;
 import android.bluetooth.BluetoothAdapter;
+import android.hardware.location.ContextHubManager;
 
 /**
  * Nearby dependency injector. To be used for accessing various Nearby class instances and as a
@@ -29,7 +30,7 @@
     BluetoothAdapter getBluetoothAdapter();
 
     /** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
-    ContextHubManagerAdapter getContextHubManagerAdapter();
+    ContextHubManager getContextHubManager();
 
     /** Get the AppOpsManager to control access. */
     AppOpsManager getAppOpsManager();
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
similarity index 70%
rename from nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
rename to nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
index 3fffda5..024bff8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/BroadcastProviderManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,8 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.nearby.BroadcastCallback;
 import android.nearby.BroadcastRequest;
@@ -27,7 +28,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.Advertisement;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
 import com.android.server.nearby.presence.FastAdvertisement;
+import com.android.server.nearby.provider.BleBroadcastProvider;
 import com.android.server.nearby.util.ForegroundThread;
 
 import java.util.concurrent.Executor;
@@ -66,10 +70,12 @@
     public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
         synchronized (mLock) {
             mExecutor.execute(() -> {
-                NearbyConfiguration configuration = new NearbyConfiguration();
-                if (!configuration.isPresenceBroadcastLegacyEnabled()) {
-                    reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
-                    return;
+                if (!mNearbyConfiguration.isTestAppSupported()) {
+                    NearbyConfiguration configuration = new NearbyConfiguration();
+                    if (!configuration.isPresenceBroadcastLegacyEnabled()) {
+                        reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                        return;
+                    }
                 }
                 if (broadcastRequest.getType() != BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE) {
                     reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
@@ -77,25 +83,38 @@
                 }
                 PresenceBroadcastRequest presenceBroadcastRequest =
                         (PresenceBroadcastRequest) broadcastRequest;
-                if (presenceBroadcastRequest.getVersion() != BroadcastRequest.PRESENCE_VERSION_V0) {
+                Advertisement advertisement = getAdvertisement(presenceBroadcastRequest);
+                if (advertisement == null) {
+                    Log.e(TAG, "Failed to start broadcast because broadcastRequest is illegal.");
                     reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
                     return;
                 }
-                FastAdvertisement fastAdvertisement = FastAdvertisement.createFromRequest(
-                        presenceBroadcastRequest);
-                byte[] advertisementPackets = fastAdvertisement.toBytes();
                 mBroadcastListener = listener;
-                mBleBroadcastProvider.start(advertisementPackets, this);
+                mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
+                        advertisement.toBytes(), this);
             });
         }
     }
 
+    @Nullable
+    private Advertisement getAdvertisement(PresenceBroadcastRequest request) {
+        switch (request.getVersion()) {
+            case BroadcastRequest.PRESENCE_VERSION_V0:
+                return FastAdvertisement.createFromRequest(request);
+            case BroadcastRequest.PRESENCE_VERSION_V1:
+                return ExtendedAdvertisement.createFromRequest(request);
+            default:
+                return null;
+        }
+    }
+
     /**
      * Stops the nearby broadcast.
      */
     public void stopBroadcast(IBroadcastListener listener) {
         synchronized (mLock) {
-            if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+            if (!mNearbyConfiguration.isTestAppSupported()
+                    && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
                 reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
                 return;
             }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
new file mode 100644
index 0000000..c9b9a43
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryManager.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import android.nearby.IScanListener;
+import android.nearby.NearbyManager;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+/**
+ * Interface added for flagging DiscoveryProviderManager refactor. After the
+ * nearby_refactor_discovery_manager flag is fully rolled out, this can be deleted.
+ */
+public interface DiscoveryManager {
+
+    /**
+     * Registers the listener in the manager and starts scan according to the requested scan mode.
+     */
+    @NearbyManager.ScanStatus
+    int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+            CallerIdentity callerIdentity);
+
+    /**
+     * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+     */
+    void unregisterScanListener(IScanListener listener);
+
+    /** Query offload capability in a device. */
+    void queryOffloadCapability(IOffloadCallback callback);
+
+    /** Called after boot completed. */
+    void init();
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
new file mode 100644
index 0000000..0c41426
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.registration.DiscoveryRegistration;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManager extends
+        ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> implements
+        AbstractDiscoveryProvider.Listener,
+        DiscoveryManager {
+
+    protected final Object mLock = new Object();
+    @VisibleForTesting
+    @Nullable
+    final ChreDiscoveryProvider mChreDiscoveryProvider;
+    private final Context mContext;
+    private final BleDiscoveryProvider mBleDiscoveryProvider;
+    private final Injector mInjector;
+    private final Executor mExecutor;
+
+    public DiscoveryProviderManager(Context context, Injector injector) {
+        Log.v(TAG, "DiscoveryProviderManager: ");
+        mContext = context;
+        mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+        mExecutor = Executors.newSingleThreadExecutor();
+        mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
+                new ChreCommunication(injector, mContext, mExecutor), mExecutor);
+        mInjector = injector;
+    }
+
+    @VisibleForTesting
+    DiscoveryProviderManager(Context context, Executor executor, Injector injector,
+            BleDiscoveryProvider bleDiscoveryProvider,
+            ChreDiscoveryProvider chreDiscoveryProvider) {
+        mContext = context;
+        mExecutor = executor;
+        mInjector = injector;
+        mBleDiscoveryProvider = bleDiscoveryProvider;
+        mChreDiscoveryProvider = chreDiscoveryProvider;
+    }
+
+    private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
+        for (ScanFilter scanFilter : scanFilters) {
+            List<DataElement> dataElements =
+                    ((PresenceScanFilter) scanFilter).getExtendedProperties();
+            for (DataElement dataElement : dataElements) {
+                if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+                    continue;
+                }
+                byte[] scanModeValue = dataElement.getValue();
+                if (scanModeValue == null || scanModeValue.length == 0) {
+                    break;
+                }
+                if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+                    return true;
+                }
+            }
+
+        }
+        return false;
+    }
+
+    @Override
+    public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+        synchronized (mMultiplexerLock) {
+            Log.d(TAG, "Found device" + nearbyDevice);
+            deliverToListeners(registration -> {
+                try {
+                    return registration.onNearbyDeviceDiscovered(nearbyDevice);
+                } catch (Exception e) {
+                    Log.w(TAG, "DiscoveryProviderManager failed to report callback.", e);
+                    return null;
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onError(int errorCode) {
+        synchronized (mMultiplexerLock) {
+            Log.e(TAG, "Error found during scanning.");
+            deliverToListeners(registration -> {
+                try {
+                    return registration.reportError(errorCode);
+                } catch (Exception e) {
+                    Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                    return null;
+                }
+            });
+        }
+    }
+
+    /** Called after boot completed. */
+    public void init() {
+        if (mInjector.getContextHubManager() != null) {
+            mChreDiscoveryProvider.init();
+        }
+        mChreDiscoveryProvider.getController().setListener(this);
+    }
+
+    /**
+     * Registers the listener in the manager and starts scan according to the requested scan mode.
+     */
+    @NearbyManager.ScanStatus
+    public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+            CallerIdentity callerIdentity) {
+        DiscoveryRegistration registration = new DiscoveryRegistration(this, scanRequest, listener,
+                mExecutor, callerIdentity, mMultiplexerLock, mInjector.getAppOpsManager());
+        synchronized (mMultiplexerLock) {
+            putRegistration(listener.asBinder(), registration);
+            return NearbyManager.ScanStatus.SUCCESS;
+        }
+    }
+
+    @Override
+    public void onRegister() {
+        Log.v(TAG, "Registering the DiscoveryProviderManager.");
+        startProviders();
+    }
+
+    @Override
+    public void onUnregister() {
+        Log.v(TAG, "Unregistering the DiscoveryProviderManager.");
+        stopProviders();
+    }
+
+    /**
+     * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+     */
+    public void unregisterScanListener(IScanListener listener) {
+        Log.v(TAG, "Unregister scan listener");
+        synchronized (mMultiplexerLock) {
+            removeRegistration(listener.asBinder());
+        }
+        // TODO(b/221082271): updates the scan with reduced filters.
+    }
+
+    /**
+     * Query offload capability in a device.
+     */
+    public void queryOffloadCapability(IOffloadCallback callback) {
+        mChreDiscoveryProvider.queryOffloadCapability(callback);
+    }
+
+    /**
+     * @return {@code null} when all providers are initializing
+     * {@code false} when fail to start all the providers
+     * {@code true} when any one of the provider starts successfully
+     */
+    @VisibleForTesting
+    @Nullable
+    Boolean startProviders() {
+        synchronized (mMultiplexerLock) {
+            if (!mMerged.getMediums().contains(MergedDiscoveryRequest.Medium.BLE)) {
+                Log.w(TAG, "failed to start any provider because client disabled BLE");
+                return false;
+            }
+            Set<ScanFilter> scanFilters = mMerged.getScanFilters();
+            boolean chreOnly = isChreOnly(scanFilters);
+            Boolean chreAvailable = mChreDiscoveryProvider.available();
+            Log.v(TAG, "startProviders: chreOnly " + chreOnly + " chreAvailable " + chreAvailable);
+            if (chreAvailable == null) {
+                if (chreOnly) {
+                    Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+                            + " status");
+                    return null;
+                }
+                startBleProvider(scanFilters);
+                return true;
+            }
+
+            if (!chreAvailable) {
+                if (chreOnly) {
+                    Log.w(TAG,
+                            "failed to start any provider because client wants CHRE only and CHRE"
+                                    + " is not available");
+                    return false;
+                }
+                startBleProvider(scanFilters);
+                return true;
+            }
+
+            if (mMerged.getScanTypes().contains(SCAN_TYPE_NEARBY_PRESENCE)) {
+                startChreProvider(scanFilters);
+                return true;
+            }
+
+            startBleProvider(scanFilters);
+            return true;
+        }
+    }
+
+    @GuardedBy("mMultiplexerLock")
+    private void startBleProvider(Set<ScanFilter> scanFilters) {
+        if (!mBleDiscoveryProvider.getController().isStarted()) {
+            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            mBleDiscoveryProvider.getController().setListener(this);
+            mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+            mBleDiscoveryProvider.getController().setProviderScanFilters(
+                    new ArrayList<>(scanFilters));
+            mBleDiscoveryProvider.getController().start();
+        }
+    }
+
+    @VisibleForTesting
+    @GuardedBy("mMultiplexerLock")
+    void startChreProvider(Collection<ScanFilter> scanFilters) {
+        Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning. " + mMerged);
+        mChreDiscoveryProvider.getController().setProviderScanFilters(new ArrayList<>(scanFilters));
+        mChreDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+        mChreDiscoveryProvider.getController().start();
+    }
+
+    private void stopProviders() {
+        stopBleProvider();
+        stopChreProvider();
+    }
+
+    private void stopBleProvider() {
+        mBleDiscoveryProvider.getController().stop();
+    }
+
+    @VisibleForTesting
+    protected void stopChreProvider() {
+        mChreDiscoveryProvider.getController().stop();
+    }
+
+    @VisibleForTesting
+    void invalidateProviderScanMode() {
+        if (mBleDiscoveryProvider.getController().isStarted()) {
+            synchronized (mMultiplexerLock) {
+                mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
+            }
+        } else {
+            Log.d(TAG, "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+                    + "started.");
+        }
+    }
+
+    @Override
+    public MergedDiscoveryRequest mergeRegistrations(
+            @NonNull Collection<DiscoveryRegistration> registrations) {
+        MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+        int scanMode = ScanRequest.SCAN_MODE_NO_POWER;
+        for (DiscoveryRegistration registration : registrations) {
+            builder.addActions(registration.getActions());
+            builder.addScanFilters(registration.getPresenceScanFilters());
+            Log.d(TAG,
+                    "mergeRegistrations: type is " + registration.getScanRequest().getScanType());
+            builder.addScanType(registration.getScanRequest().getScanType());
+            if (registration.getScanRequest().isBleEnabled()) {
+                builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+            }
+            int requestScanMode = registration.getScanRequest().getScanMode();
+            if (scanMode < requestScanMode) {
+                scanMode = requestScanMode;
+            }
+        }
+        builder.setScanMode(scanMode);
+        return builder.build();
+    }
+
+    @Override
+    public void onMergedRegistrationsUpdated() {
+        invalidateProviderScanMode();
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
new file mode 100644
index 0000000..4b76eba
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.metrics.NearbyMetrics;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.provider.AbstractDiscoveryProvider;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.PrivacyFilter;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** Manages all aspects of discovery providers. */
+public class DiscoveryProviderManagerLegacy implements AbstractDiscoveryProvider.Listener,
+        DiscoveryManager {
+
+    protected final Object mLock = new Object();
+    @VisibleForTesting
+    @Nullable
+    final ChreDiscoveryProvider mChreDiscoveryProvider;
+    private final Context mContext;
+    private final BleDiscoveryProvider mBleDiscoveryProvider;
+    private final Injector mInjector;
+    @ScanRequest.ScanMode
+    private int mScanMode;
+    @GuardedBy("mLock")
+    private final Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
+
+    public DiscoveryProviderManagerLegacy(Context context, Injector injector) {
+        mContext = context;
+        mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+        Executor executor = Executors.newSingleThreadExecutor();
+        mChreDiscoveryProvider =
+                new ChreDiscoveryProvider(
+                        mContext, new ChreCommunication(injector, mContext, executor), executor);
+        mScanTypeScanListenerRecordMap = new HashMap<>();
+        mInjector = injector;
+        Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
+    }
+
+    @VisibleForTesting
+    DiscoveryProviderManagerLegacy(Context context, Injector injector,
+            BleDiscoveryProvider bleDiscoveryProvider,
+            ChreDiscoveryProvider chreDiscoveryProvider,
+            Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+        mContext = context;
+        mInjector = injector;
+        mBleDiscoveryProvider = bleDiscoveryProvider;
+        mChreDiscoveryProvider = chreDiscoveryProvider;
+        mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+    }
+
+    private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+        for (ScanFilter scanFilter : scanFilters) {
+            List<DataElement> dataElements =
+                    ((PresenceScanFilter) scanFilter).getExtendedProperties();
+            for (DataElement dataElement : dataElements) {
+                if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+                    continue;
+                }
+                byte[] scanModeValue = dataElement.getValue();
+                if (scanModeValue == null || scanModeValue.length == 0) {
+                    break;
+                }
+                if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+                    return true;
+                }
+            }
+
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    static boolean presenceFilterMatches(
+            NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
+        if (scanFilters.isEmpty()) {
+            return true;
+        }
+        PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+        for (ScanFilter scanFilter : scanFilters) {
+            PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+            if (discoveryResult.matches(presenceScanFilter)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+        synchronized (mLock) {
+            AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+            for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+                ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+                if (record == null) {
+                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    continue;
+                }
+                CallerIdentity callerIdentity = record.getCallerIdentity();
+                if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+                        appOpsManager, callerIdentity)) {
+                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                            + "- not forwarding results");
+                    try {
+                        record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                    }
+                    return;
+                }
+
+                if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+                    List<ScanFilter> presenceFilters =
+                            record.getScanRequest().getScanFilters().stream()
+                                    .filter(
+                                            scanFilter ->
+                                                    scanFilter.getType()
+                                                            == SCAN_TYPE_NEARBY_PRESENCE)
+                                    .collect(Collectors.toList());
+                    if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+                        Log.d(TAG, "presence filter does not match for "
+                                + "the scanned Presence Device");
+                        continue;
+                    }
+                }
+                try {
+                    record.getScanListener()
+                            .onDiscovered(
+                                    PrivacyFilter.filter(
+                                            record.getScanRequest().getScanType(), nearbyDevice));
+                    NearbyMetrics.logScanDeviceDiscovered(
+                            record.hashCode(), record.getScanRequest(), nearbyDevice);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onError(int errorCode) {
+        synchronized (mLock) {
+            AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
+            for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+                ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+                if (record == null) {
+                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    continue;
+                }
+                CallerIdentity callerIdentity = record.getCallerIdentity();
+                if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+                        appOpsManager, callerIdentity)) {
+                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                            + "- not forwarding results");
+                    try {
+                        record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                    }
+                    return;
+                }
+
+                try {
+                    record.getScanListener().onError(errorCode);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+                }
+            }
+        }
+    }
+
+    /** Called after boot completed. */
+    public void init() {
+        if (mInjector.getContextHubManager() != null) {
+            mChreDiscoveryProvider.init();
+        }
+        mChreDiscoveryProvider.getController().setListener(this);
+    }
+
+    /**
+     * Registers the listener in the manager and starts scan according to the requested scan mode.
+     */
+    @NearbyManager.ScanStatus
+    public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+            CallerIdentity callerIdentity) {
+        synchronized (mLock) {
+            ScanListenerDeathRecipient deathRecipient = (listener != null)
+                    ? new ScanListenerDeathRecipient(listener) : null;
+            IBinder listenerBinder = listener.asBinder();
+            if (listenerBinder != null && deathRecipient != null) {
+                try {
+                    listenerBinder.linkToDeath(deathRecipient, 0);
+                } catch (RemoteException e) {
+                    throw new IllegalArgumentException("Can't link to scan listener's death");
+                }
+            }
+            if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
+                ScanRequest savedScanRequest =
+                        mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
+                if (scanRequest.equals(savedScanRequest)) {
+                    Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
+                    return NearbyManager.ScanStatus.SUCCESS;
+                }
+            }
+            ScanListenerRecord scanListenerRecord =
+                    new ScanListenerRecord(scanRequest, listener, callerIdentity, deathRecipient);
+
+            mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
+            Boolean started = startProviders(scanRequest);
+            if (started == null) {
+                mScanTypeScanListenerRecordMap.remove(listenerBinder);
+                return NearbyManager.ScanStatus.UNKNOWN;
+            }
+            if (!started) {
+                mScanTypeScanListenerRecordMap.remove(listenerBinder);
+                return NearbyManager.ScanStatus.ERROR;
+            }
+            NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
+            if (mScanMode < scanRequest.getScanMode()) {
+                mScanMode = scanRequest.getScanMode();
+                invalidateProviderScanMode();
+            }
+            return NearbyManager.ScanStatus.SUCCESS;
+        }
+    }
+
+    /**
+     * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+     */
+    public void unregisterScanListener(IScanListener listener) {
+        IBinder listenerBinder = listener.asBinder();
+        synchronized (mLock) {
+            if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
+                Log.w(
+                        TAG,
+                        "Cannot unregister the scanRequest because the request is never "
+                                + "registered.");
+                return;
+            }
+
+            ScanListenerRecord removedRecord =
+                    mScanTypeScanListenerRecordMap.remove(listenerBinder);
+            ScanListenerDeathRecipient deathRecipient = removedRecord.getDeathRecipient();
+            if (listenerBinder != null && deathRecipient != null) {
+                listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
+            }
+            Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+            NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
+            if (mScanTypeScanListenerRecordMap.isEmpty()) {
+                Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+                        + "scan listener registered.");
+                stopProviders();
+                return;
+            }
+
+            // TODO(b/221082271): updates the scan with reduced filters.
+
+            // Removes current highest scan mode requested and sets the next highest scan mode.
+            if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
+                Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
+                        + "because the highest scan mode listener was unregistered.");
+                @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
+                // find the next highest scan mode;
+                for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
+                    @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
+                    if (scanMode > highestScanModeRequested) {
+                        highestScanModeRequested = scanMode;
+                    }
+                }
+                if (mScanMode != highestScanModeRequested) {
+                    mScanMode = highestScanModeRequested;
+                    invalidateProviderScanMode();
+                }
+            }
+        }
+    }
+
+    /**
+     * Query offload capability in a device.
+     */
+    public void queryOffloadCapability(IOffloadCallback callback) {
+        mChreDiscoveryProvider.queryOffloadCapability(callback);
+    }
+
+    /**
+     * @return {@code null} when all providers are initializing
+     * {@code false} when fail to start all the providers
+     * {@code true} when any one of the provider starts successfully
+     */
+    @VisibleForTesting
+    @Nullable
+    Boolean startProviders(ScanRequest scanRequest) {
+        if (!scanRequest.isBleEnabled()) {
+            Log.w(TAG, "failed to start any provider because client disabled BLE");
+            return false;
+        }
+        List<ScanFilter> scanFilters = getPresenceScanFilters();
+        boolean chreOnly = isChreOnly(scanFilters);
+        Boolean chreAvailable = mChreDiscoveryProvider.available();
+        if (chreAvailable == null) {
+            if (chreOnly) {
+                Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+                        + " status");
+                return null;
+            }
+            startBleProvider(scanFilters);
+            return true;
+        }
+
+        if (!chreAvailable) {
+            if (chreOnly) {
+                Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
+                        + " is not available");
+                return false;
+            }
+            startBleProvider(scanFilters);
+            return true;
+        }
+
+        if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+            startChreProvider(scanFilters);
+            return true;
+        }
+
+        startBleProvider(scanFilters);
+        return true;
+    }
+
+    private void startBleProvider(List<ScanFilter> scanFilters) {
+        if (!mBleDiscoveryProvider.getController().isStarted()) {
+            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            mBleDiscoveryProvider.getController().setListener(this);
+            mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+            mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+            mBleDiscoveryProvider.getController().start();
+        }
+    }
+
+    @VisibleForTesting
+    void startChreProvider(List<ScanFilter> scanFilters) {
+        Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+        mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+        mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+        mChreDiscoveryProvider.getController().start();
+    }
+
+    private List<ScanFilter> getPresenceScanFilters() {
+        synchronized (mLock) {
+            List<ScanFilter> scanFilters = new ArrayList();
+            for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
+                ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
+                List<ScanFilter> presenceFilters =
+                        record.getScanRequest().getScanFilters().stream()
+                                .filter(
+                                        scanFilter ->
+                                                scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
+                                .collect(Collectors.toList());
+                scanFilters.addAll(presenceFilters);
+            }
+            return scanFilters;
+        }
+    }
+
+    private void stopProviders() {
+        stopBleProvider();
+        stopChreProvider();
+    }
+
+    private void stopBleProvider() {
+        mBleDiscoveryProvider.getController().stop();
+    }
+
+    @VisibleForTesting
+    protected void stopChreProvider() {
+        mChreDiscoveryProvider.getController().stop();
+    }
+
+    @VisibleForTesting
+    void invalidateProviderScanMode() {
+        if (mBleDiscoveryProvider.getController().isStarted()) {
+            mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+        } else {
+            Log.d(
+                    TAG,
+                    "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+                            + "started.");
+        }
+    }
+
+    @VisibleForTesting
+    static class ScanListenerRecord {
+
+        private final ScanRequest mScanRequest;
+
+        private final IScanListener mScanListener;
+
+        private final CallerIdentity mCallerIdentity;
+
+        private final ScanListenerDeathRecipient mDeathRecipient;
+
+        ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
+                CallerIdentity callerIdentity, ScanListenerDeathRecipient deathRecipient) {
+            mScanListener = iScanListener;
+            mScanRequest = scanRequest;
+            mCallerIdentity = callerIdentity;
+            mDeathRecipient = deathRecipient;
+        }
+
+        IScanListener getScanListener() {
+            return mScanListener;
+        }
+
+        ScanRequest getScanRequest() {
+            return mScanRequest;
+        }
+
+        CallerIdentity getCallerIdentity() {
+            return mCallerIdentity;
+        }
+
+        ScanListenerDeathRecipient getDeathRecipient() {
+            return mDeathRecipient;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof ScanListenerRecord) {
+                ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
+                return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
+                        && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mScanListener, mScanRequest);
+        }
+    }
+
+    /**
+     * Class to make listener unregister after the binder is dead.
+     */
+    public class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
+        public IScanListener listener;
+
+        ScanListenerDeathRecipient(IScanListener listener) {
+            this.listener = listener;
+        }
+
+        @Override
+        public void binderDied() {
+            Log.d(TAG, "Binder is dead - unregistering scan listener");
+            unregisterScanListener(listener);
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
new file mode 100644
index 0000000..a6a9388
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/ListenerMultiplexer.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.os.IBinder;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+import com.android.server.nearby.managers.registration.BinderListenerRegistration.ListenerOperation;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A simplified class based on {@link com.android.server.location.listeners.ListenerMultiplexer}.
+ * It is a base class to multiplex broadcast and discovery events to multiple listener
+ * registrations. Every listener is represented by a registration object which stores all required
+ * state for a listener.
+ * Registrations will be merged to one request for the service to operate.
+ *
+ * @param <TListener>           callback type for clients
+ * @param <TRegistration>       child of {@link BinderListenerRegistration}
+ * @param <TMergedRegistration> merged registration type
+ */
+public abstract class ListenerMultiplexer<TListener,
+        TRegistration extends BinderListenerRegistration<TListener>, TMergedRegistration> {
+
+    /**
+     * The lock object used by the multiplexer. Acquiring this lock allows for multiple operations
+     * on the multiplexer to be completed atomically. Otherwise, it is not required to hold this
+     * lock. This lock is held while invoking all lifecycle callbacks on both the multiplexer and
+     * any registrations.
+     */
+    public final Object mMultiplexerLock = new Object();
+
+    @GuardedBy("mMultiplexerLock")
+    final ArrayMap<IBinder, TRegistration> mRegistrations = new ArrayMap<>();
+
+    // this is really @NonNull in many ways, but we explicitly null this out to allow for GC when
+    // not
+    // in use, so we can't annotate with @NonNull
+    @GuardedBy("mMultiplexerLock")
+    public TMergedRegistration mMerged;
+
+    /**
+     * Invoked when the multiplexer goes from having no registrations to having some registrations.
+     * This is a convenient entry point for registering listeners, etc, which only need to be
+     * present
+     * while there are any registrations. Invoked while holding the multiplexer's internal lock.
+     */
+    @GuardedBy("mMultiplexerLock")
+    public void onRegister() {
+        Log.v(TAG, "ListenerMultiplexer registered.");
+    }
+
+    /**
+     * Invoked when the multiplexer goes from having some registrations to having no registrations.
+     * This is a convenient entry point for unregistering listeners, etc, which only need to be
+     * present while there are any registrations. Invoked while holding the multiplexer's internal
+     * lock.
+     */
+    @GuardedBy("mMultiplexerLock")
+    public void onUnregister() {
+        Log.v(TAG, "ListenerMultiplexer unregistered.");
+    }
+
+    /**
+     * Puts a new registration with the given key, replacing any previous registration under the
+     * same key. This method cannot be called to put a registration re-entrantly.
+     */
+    public final void putRegistration(@NonNull IBinder key, @NonNull TRegistration registration) {
+        Objects.requireNonNull(key);
+        Objects.requireNonNull(registration);
+        synchronized (mMultiplexerLock) {
+            boolean wasEmpty = mRegistrations.isEmpty();
+
+            int index = mRegistrations.indexOfKey(key);
+            if (index > 0) {
+                BinderListenerRegistration<TListener> oldRegistration = mRegistrations.valueAt(
+                        index);
+                oldRegistration.onUnregister();
+                mRegistrations.setValueAt(index, registration);
+            } else {
+                mRegistrations.put(key, registration);
+            }
+
+            registration.onRegister();
+            onRegistrationsUpdated();
+            if (wasEmpty) {
+                onRegister();
+            }
+        }
+    }
+
+    /**
+     * Removes the registration with the given key.
+     */
+    public final void removeRegistration(IBinder key) {
+        synchronized (mMultiplexerLock) {
+            int index = mRegistrations.indexOfKey(key);
+            if (index < 0) {
+                return;
+            }
+
+            removeRegistration(index);
+        }
+    }
+
+    @GuardedBy("mMultiplexerLock")
+    private void removeRegistration(int index) {
+        TRegistration registration = mRegistrations.valueAt(index);
+
+        registration.onUnregister();
+        mRegistrations.removeAt(index);
+
+        onRegistrationsUpdated();
+
+        if (mRegistrations.isEmpty()) {
+            onUnregister();
+        }
+    }
+
+    /**
+     * Invoked when a registration is added, removed, or replaced. Invoked while holding the
+     * multiplexer's internal lock.
+     */
+    @GuardedBy("mMultiplexerLock")
+    public final void onRegistrationsUpdated() {
+        TMergedRegistration newMerged = mergeRegistrations(mRegistrations.values());
+        if (newMerged.equals(mMerged)) {
+            return;
+        }
+        mMerged = newMerged;
+        onMergedRegistrationsUpdated();
+    }
+
+    /**
+     * Called in order to generate a merged registration from the given set of active registrations.
+     * The list of registrations will never be empty. If the resulting merged registration is equal
+     * to the currently registered merged registration, nothing further will happen. If the merged
+     * registration differs,{@link #onMergedRegistrationsUpdated()} will be invoked with the new
+     * merged registration so that the backing service can be updated.
+     */
+    @GuardedBy("mMultiplexerLock")
+    public abstract TMergedRegistration mergeRegistrations(
+            @NonNull Collection<TRegistration> registrations);
+
+    /**
+     * The operation that the manager wants to handle when there is an update for the merged
+     * registration.
+     */
+    @GuardedBy("mMultiplexerLock")
+    public abstract void onMergedRegistrationsUpdated();
+
+    protected final void deliverToListeners(
+            Function<TRegistration, ListenerOperation<TListener>> function) {
+        synchronized (mMultiplexerLock) {
+            final int size = mRegistrations.size();
+            for (int i = 0; i < size; i++) {
+                TRegistration registration = mRegistrations.valueAt(i);
+                BinderListenerRegistration.ListenerOperation<TListener> operation = function.apply(
+                        registration);
+                if (operation != null) {
+                    registration.executeOperation(operation);
+                }
+            }
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
new file mode 100644
index 0000000..dcfb602
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/MergedDiscoveryRequest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import android.annotation.IntDef;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Collection;
+import java.util.Set;
+
+/** Internal discovery request to {@link DiscoveryProviderManager} and providers */
+public class MergedDiscoveryRequest {
+
+    private static final MergedDiscoveryRequest EMPTY_REQUEST = new MergedDiscoveryRequest(
+            /* scanMode= */ ScanRequest.SCAN_MODE_NO_POWER,
+            /* scanTypes= */ ImmutableSet.of(),
+            /* actions= */ ImmutableSet.of(),
+            /* scanFilters= */ ImmutableSet.of(),
+            /* mediums= */ ImmutableSet.of());
+    @ScanRequest.ScanMode
+    private final int mScanMode;
+    private final Set<Integer> mScanTypes;
+    private final Set<Integer> mActions;
+    private final Set<ScanFilter> mScanFilters;
+    private final Set<Integer> mMediums;
+
+    private MergedDiscoveryRequest(@ScanRequest.ScanMode int scanMode, Set<Integer> scanTypes,
+            Set<Integer> actions, Set<ScanFilter> scanFilters, Set<Integer> mediums) {
+        mScanMode = scanMode;
+        mScanTypes = scanTypes;
+        mActions = actions;
+        mScanFilters = scanFilters;
+        mMediums = mediums;
+    }
+
+    /**
+     * Returns an empty discovery request.
+     *
+     * <p>The empty request is used as the default request when the discovery engine is enabled,
+     * but
+     * there is no request yet. It's also used to notify the discovery engine all clients have
+     * removed
+     * their requests.
+     */
+    public static MergedDiscoveryRequest empty() {
+        return EMPTY_REQUEST;
+    }
+
+    /** Returns the priority of the request */
+    @ScanRequest.ScanMode
+    public final int getScanMode() {
+        return mScanMode;
+    }
+
+    /** Returns all requested scan types. */
+    public ImmutableSet<Integer> getScanTypes() {
+        return ImmutableSet.copyOf(mScanTypes);
+    }
+
+    /** Returns the actions of the request */
+    public ImmutableSet<Integer> getActions() {
+        return ImmutableSet.copyOf(mActions);
+    }
+
+    /** Returns the scan filters of the request */
+    public ImmutableSet<ScanFilter> getScanFilters() {
+        return ImmutableSet.copyOf(mScanFilters);
+    }
+
+    /** Returns the enabled scan mediums */
+    public ImmutableSet<Integer> getMediums() {
+        return ImmutableSet.copyOf(mMediums);
+    }
+
+    /**
+     * The medium where the broadcast request should be sent.
+     *
+     * @hide
+     */
+    @IntDef({Medium.BLE})
+    public @interface Medium {
+        int BLE = 1;
+    }
+
+    /** Builder for {@link MergedDiscoveryRequest}. */
+    public static class Builder {
+        private final Set<Integer> mScanTypes;
+        private final Set<Integer> mActions;
+        private final Set<ScanFilter> mScanFilters;
+        private final Set<Integer> mMediums;
+        @ScanRequest.ScanMode
+        private int mScanMode;
+
+        public Builder() {
+            mScanMode = ScanRequest.SCAN_MODE_NO_POWER;
+            mScanTypes = new ArraySet<>();
+            mActions = new ArraySet<>();
+            mScanFilters = new ArraySet<>();
+            mMediums = new ArraySet<>();
+        }
+
+        /**
+         * Sets the priority for the engine request.
+         */
+        public Builder setScanMode(@ScanRequest.ScanMode int scanMode) {
+            mScanMode = scanMode;
+            return this;
+        }
+
+        /**
+         * Adds scan type to the request.
+         */
+        public Builder addScanType(@ScanRequest.ScanType int type) {
+            mScanTypes.add(type);
+            return this;
+        }
+
+        /** Add actions to the request. */
+        public Builder addActions(Collection<Integer> actions) {
+            mActions.addAll(actions);
+            return this;
+        }
+
+        /** Add actions to the request. */
+        public Builder addScanFilters(Collection<ScanFilter> scanFilters) {
+            mScanFilters.addAll(scanFilters);
+            return this;
+        }
+
+        /**
+         * Add mediums to the request.
+         */
+        public Builder addMedium(@Medium int medium) {
+            mMediums.add(medium);
+            return this;
+        }
+
+        /** Builds an instance of {@link MergedDiscoveryRequest}. */
+        public MergedDiscoveryRequest build() {
+            return new MergedDiscoveryRequest(mScanMode, mScanTypes, mActions, mScanFilters,
+                    mMediums);
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
new file mode 100644
index 0000000..4aaa08f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/BinderListenerRegistration.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A listener registration object which holds data associated with the listener, such as an optional
+ * request, and an executor responsible for listener invocations. Key is the IBinder.
+ *
+ * @param <TListener> listener for the callback
+ */
+public abstract class BinderListenerRegistration<TListener> implements IBinder.DeathRecipient {
+
+    private final AtomicBoolean mRemoved = new AtomicBoolean(false);
+    private final Executor mExecutor;
+    private final Object mListenerLock = new Object();
+    @Nullable
+    TListener mListener;
+    @Nullable
+    private final IBinder mKey;
+
+    public BinderListenerRegistration(IBinder key, Executor executor, TListener listener) {
+        this.mKey = key;
+        this.mExecutor = executor;
+        this.mListener = listener;
+    }
+
+    /**
+     * Must be implemented to return the
+     * {@link com.android.server.nearby.managers.ListenerMultiplexer} this registration is
+     * registered
+     * with. Often this is easiest to accomplish by defining registration subclasses as non-static
+     * inner classes of the multiplexer they are to be used with.
+     */
+    public abstract ListenerMultiplexer<TListener, ?
+            extends BinderListenerRegistration<TListener>, ?> getOwner();
+
+    public final IBinder getBinder() {
+        return mKey;
+    }
+
+    public final Executor getExecutor() {
+        return mExecutor;
+    }
+
+    /**
+     * Called when the registration is put in the Multiplexer.
+     */
+    public void onRegister() {
+        try {
+            getBinder().linkToDeath(this, 0);
+        } catch (RemoteException e) {
+            remove();
+        }
+    }
+
+    /**
+     * Called when the registration is removed in the Multiplexer.
+     */
+    public void onUnregister() {
+        this.mListener = null;
+        try {
+            getBinder().unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            Log.w(TAG, "failed to unregister binder death listener", e);
+        }
+    }
+
+    /**
+     * Removes this registration. All pending listener invocations will fail.
+     *
+     * <p>Does nothing if invoked before {@link #onRegister()} or after {@link #onUnregister()}.
+     */
+    public final void remove() {
+        IBinder key = mKey;
+        if (key != null && !mRemoved.getAndSet(true)) {
+            getOwner().removeRegistration(key);
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        remove();
+    }
+
+    /**
+     * May be overridden by subclasses to handle listener operation failures. The default behavior
+     * is
+     * to further propagate any exceptions. Will always be invoked on the executor thread.
+     */
+    protected void onOperationFailure(Exception exception) {
+        throw new AssertionError(exception);
+    }
+
+    /**
+     * Executes the given listener operation on the registration executor, invoking {@link
+     * #onOperationFailure(Exception)} in case the listener operation fails. If the registration is
+     * removed prior to the operation running, the operation is considered canceled. If a null
+     * operation is supplied, nothing happens.
+     */
+    public final void executeOperation(@Nullable ListenerOperation<TListener> operation) {
+        if (operation == null) {
+            return;
+        }
+
+        synchronized (mListenerLock) {
+            if (mListener == null) {
+                return;
+            }
+
+            AtomicBoolean complete = new AtomicBoolean(false);
+            mExecutor.execute(() -> {
+                TListener listener;
+                synchronized (mListenerLock) {
+                    listener = mListener;
+                }
+
+                Exception failure = null;
+                if (listener != null) {
+                    try {
+                        operation.operate(listener);
+                    } catch (Exception e) {
+                        if (e instanceof RuntimeException) {
+                            throw (RuntimeException) e;
+                        } else {
+                            failure = e;
+                        }
+                    }
+                }
+
+                operation.onComplete(failure == null);
+                complete.set(true);
+
+                if (failure != null) {
+                    onOperationFailure(failure);
+                }
+            });
+            operation.onScheduled(complete.get());
+        }
+    }
+
+    /**
+     * An listener operation to perform.
+     *
+     * @param <ListenerT> listener type
+     */
+    public interface ListenerOperation<ListenerT> {
+
+        /**
+         * Invoked after the operation has been scheduled for execution. The {@code complete}
+         * argument
+         * will be true if {@link #onComplete(boolean)} was invoked prior to this callback (such as
+         * if
+         * using a direct executor), or false if {@link #onComplete(boolean)} will be invoked after
+         * this
+         * callback. This method is always invoked on the calling thread.
+         */
+        default void onScheduled(boolean complete) {
+        }
+
+        /**
+         * Invoked to perform an operation on the given listener. This method is always invoked on
+         * the
+         * executor thread. If this method throws a checked exception, the operation will fail and
+         * result in {@link #onOperationFailure(Exception)} being invoked. If this method throws an
+         * unchecked exception, this propagates normally and should result in a crash.
+         */
+        void operate(ListenerT listener) throws Exception;
+
+        /**
+         * Invoked after the operation is complete. The {@code success} argument will be true if
+         * the
+         * operation completed without throwing any exceptions, and false otherwise (such as if the
+         * operation was canceled prior to executing, or if it threw an exception). This invocation
+         * may
+         * happen either before or after (but never during) the invocation of {@link
+         * #onScheduled(boolean)}. This method is always invoked on the executor thread.
+         */
+        default void onComplete(boolean success) {
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
new file mode 100644
index 0000000..91237d2
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/registration/DiscoveryRegistration.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.CancelableAlarm;
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.stream.Collectors;
+
+/**
+ * Class responsible for all client based operations. Each {@link DiscoveryRegistration} is for one
+ * valid unique {@link android.nearby.NearbyManager#startScan(ScanRequest, Executor, ScanCallback)}
+ */
+public class DiscoveryRegistration extends BinderListenerRegistration<IScanListener> {
+
+    /**
+     * Timeout before a previous discovered device is reported as lost.
+     */
+    @VisibleForTesting
+    static final int ON_LOST_TIME_OUT_MS = 10000;
+    /** Lock for registration operations. */
+    final Object mMultiplexerLock;
+    private final ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest>
+            mOwner;
+    private final AppOpsManager mAppOpsManager;
+    /** Presence devices that are currently discovered, and not lost yet. */
+    @GuardedBy("mMultiplexerLock")
+    private final Map<Long, NearbyDeviceParcelable> mDiscoveredDevices;
+    /** A map of deviceId and alarms for reporting device lost. */
+    @GuardedBy("mMultiplexerLock")
+    private final Map<Long, DeviceOnLostAlarm> mDiscoveryOnLostAlarmPerDevice = new ArrayMap<>();
+    /**
+     * The single thread executor to run {@link CancelableAlarm} to report
+     * {@link NearbyDeviceParcelable} on lost after timeout.
+     */
+    private final ScheduledExecutorService mAlarmExecutor =
+            Executors.newSingleThreadScheduledExecutor();
+    private final ScanRequest mScanRequest;
+    private final CallerIdentity mCallerIdentity;
+
+    public DiscoveryRegistration(
+            ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> owner,
+            ScanRequest scanRequest, IScanListener scanListener, Executor executor,
+            CallerIdentity callerIdentity, Object multiplexerLock, AppOpsManager appOpsManager) {
+        super(scanListener.asBinder(), executor, scanListener);
+        mOwner = owner;
+        mListener = scanListener;
+        mScanRequest = scanRequest;
+        mCallerIdentity = callerIdentity;
+        mMultiplexerLock = multiplexerLock;
+        mDiscoveredDevices = new ArrayMap<>();
+        mAppOpsManager = appOpsManager;
+    }
+
+    /**
+     * Gets the scan request.
+     */
+    public ScanRequest getScanRequest() {
+        return mScanRequest;
+    }
+
+    /**
+     * Gets the actions from the scan filter(s).
+     */
+    public Set<Integer> getActions() {
+        Set<Integer> result = new ArraySet<>();
+        List<ScanFilter> filters = mScanRequest.getScanFilters();
+        for (ScanFilter filter : filters) {
+            if (filter instanceof PresenceScanFilter) {
+                result.addAll(((PresenceScanFilter) filter).getPresenceActions());
+            }
+        }
+        return ImmutableSet.copyOf(result);
+    }
+
+    /**
+     * Gets all the filters that are for Nearby Presence.
+     */
+    public Set<ScanFilter> getPresenceScanFilters() {
+        Set<ScanFilter> result = new ArraySet<>();
+        List<ScanFilter> filters = mScanRequest.getScanFilters();
+        for (ScanFilter filter : filters) {
+            if (filter.getType() == SCAN_TYPE_NEARBY_PRESENCE) {
+                result.add(filter);
+            }
+        }
+        return ImmutableSet.copyOf(result);
+    }
+
+    @VisibleForTesting
+    Map<Long, DeviceOnLostAlarm> getDiscoveryOnLostAlarms() {
+        synchronized (mMultiplexerLock) {
+            return mDiscoveryOnLostAlarmPerDevice;
+        }
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof DiscoveryRegistration) {
+            DiscoveryRegistration otherRegistration = (DiscoveryRegistration) other;
+            return Objects.equals(mScanRequest, otherRegistration.mScanRequest) && Objects.equals(
+                    mListener, otherRegistration.mListener);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mListener, mScanRequest);
+    }
+
+    @Override
+    public ListenerMultiplexer<
+            IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> getOwner() {
+        return mOwner;
+    }
+
+    @VisibleForTesting
+    ListenerOperation<IScanListener> reportDeviceLost(NearbyDeviceParcelable device) {
+        long deviceId = device.getDeviceId();
+        return reportResult(DiscoveryResult.DEVICE_LOST, device, () -> {
+            synchronized (mMultiplexerLock) {
+                // Remove the device from reporting devices after reporting lost.
+                mDiscoveredDevices.remove(deviceId);
+                DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.remove(deviceId);
+                if (alarm != null) {
+                    alarm.cancel();
+                }
+            }
+        });
+    }
+
+    /**
+     * Called when there is device discovered from the server.
+     */
+    public ListenerOperation<IScanListener> onNearbyDeviceDiscovered(
+            NearbyDeviceParcelable device) {
+        if (!filterCheck(device)) {
+            Log.d(TAG, "presence filter does not match for the scanned Presence Device");
+            return null;
+        }
+        synchronized (mMultiplexerLock) {
+            long deviceId = device.getDeviceId();
+            boolean deviceReported = mDiscoveredDevices.containsKey(deviceId);
+            scheduleOnLostAlarm(device);
+            if (deviceReported) {
+                NearbyDeviceParcelable oldDevice = mDiscoveredDevices.get(deviceId);
+                if (device.equals(oldDevice)) {
+                    return null;
+                }
+                return reportUpdated(device);
+            }
+            return reportDiscovered(device);
+        }
+    }
+
+    @VisibleForTesting
+    static boolean presenceFilterMatches(NearbyDeviceParcelable device,
+            List<ScanFilter> scanFilters) {
+        if (scanFilters.isEmpty()) {
+            return true;
+        }
+        PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
+        for (ScanFilter scanFilter : scanFilters) {
+            PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
+            if (discoveryResult.matches(presenceScanFilter)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Nullable
+    ListenerOperation<IScanListener> reportDiscovered(NearbyDeviceParcelable device) {
+        long deviceId = device.getDeviceId();
+        return reportResult(DiscoveryResult.DEVICE_DISCOVERED, device, () -> {
+            synchronized (mMultiplexerLock) {
+                // Add the device to discovered devices after reporting device is
+                // discovered.
+                mDiscoveredDevices.put(deviceId, device);
+                scheduleOnLostAlarm(device);
+            }
+        });
+    }
+
+    @Nullable
+    ListenerOperation<IScanListener> reportUpdated(NearbyDeviceParcelable device) {
+        long deviceId = device.getDeviceId();
+        return reportResult(DiscoveryResult.DEVICE_UPDATED, device, () -> {
+            synchronized (mMultiplexerLock) {
+                // Update the new device to discovered devices after reporting device is
+                // discovered.
+                mDiscoveredDevices.put(deviceId, device);
+                scheduleOnLostAlarm(device);
+            }
+        });
+
+    }
+
+    /** Reports an error to the client. */
+    public ListenerOperation<IScanListener> reportError(@ScanCallback.ErrorCode int errorCode) {
+        return listener -> listener.onError(errorCode);
+    }
+
+    @Nullable
+    ListenerOperation<IScanListener> reportResult(@DiscoveryResult int result,
+            NearbyDeviceParcelable device, @Nullable Runnable successReportCallback) {
+        // Report the operation to AppOps.
+        // NOTE: AppOps report has to be the last operation before delivering the result. Otherwise
+        // we may over-report when the discovery result doesn't end up being delivered.
+        if (!checkIdentity()) {
+            return reportError(ScanCallback.ERROR_PERMISSION_DENIED);
+        }
+
+        return new ListenerOperation<>() {
+
+            @Override
+            public void operate(IScanListener listener) throws Exception {
+                switch (result) {
+                    case DiscoveryResult.DEVICE_DISCOVERED:
+                        listener.onDiscovered(device);
+                        break;
+                    case DiscoveryResult.DEVICE_UPDATED:
+                        listener.onUpdated(device);
+                        break;
+                    case DiscoveryResult.DEVICE_LOST:
+                        listener.onLost(device);
+                        break;
+                }
+            }
+
+            @Override
+            public void onComplete(boolean success) {
+                if (success) {
+                    if (successReportCallback != null) {
+                        successReportCallback.run();
+                        Log.d(TAG, "Successfully delivered result to caller.");
+                    }
+                }
+            }
+        };
+    }
+
+    private boolean filterCheck(NearbyDeviceParcelable device) {
+        if (device.getScanType() != SCAN_TYPE_NEARBY_PRESENCE) {
+            return true;
+        }
+        List<ScanFilter> presenceFilters = mScanRequest.getScanFilters().stream().filter(
+                scanFilter -> scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE).collect(
+                Collectors.toList());
+        return presenceFilterMatches(device, presenceFilters);
+    }
+
+    private boolean checkIdentity() {
+        boolean result = DiscoveryPermissions.noteDiscoveryResultDelivery(mAppOpsManager,
+                mCallerIdentity);
+        if (!result) {
+            Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    + "- not forwarding results for the registration.");
+        }
+        return result;
+    }
+
+    @GuardedBy("mMultiplexerLock")
+    private void scheduleOnLostAlarm(NearbyDeviceParcelable device) {
+        long deviceId = device.getDeviceId();
+        DeviceOnLostAlarm alarm = mDiscoveryOnLostAlarmPerDevice.get(deviceId);
+        if (alarm == null) {
+            alarm = new DeviceOnLostAlarm(device, mAlarmExecutor);
+            mDiscoveryOnLostAlarmPerDevice.put(deviceId, alarm);
+        }
+        alarm.start();
+        Log.d(TAG, "DiscoveryProviderManager updated state for " + device.getDeviceId());
+    }
+
+    /** Status of the discovery result. */
+    @IntDef({DiscoveryResult.DEVICE_DISCOVERED, DiscoveryResult.DEVICE_UPDATED,
+            DiscoveryResult.DEVICE_LOST})
+    public @interface DiscoveryResult {
+        int DEVICE_DISCOVERED = 0;
+        int DEVICE_UPDATED = 1;
+        int DEVICE_LOST = 2;
+    }
+
+    private class DeviceOnLostAlarm {
+
+        private static final String NAME = "DeviceOnLostAlarm";
+        private final NearbyDeviceParcelable mDevice;
+        private final ScheduledExecutorService mAlarmExecutor;
+        @Nullable
+        private CancelableAlarm mTimeoutAlarm;
+
+        DeviceOnLostAlarm(NearbyDeviceParcelable device, ScheduledExecutorService alarmExecutor) {
+            mDevice = device;
+            mAlarmExecutor = alarmExecutor;
+        }
+
+        synchronized void start() {
+            cancel();
+            this.mTimeoutAlarm = CancelableAlarm.createSingleAlarm(NAME, () -> {
+                Log.d(TAG, String.format("%s timed out after %d ms. Reporting %s on lost.", NAME,
+                        ON_LOST_TIME_OUT_MS, mDevice.getName()));
+                synchronized (mMultiplexerLock) {
+                    executeOperation(reportDeviceLost(mDevice));
+                }
+            }, ON_LOST_TIME_OUT_MS, mAlarmExecutor);
+        }
+
+        synchronized void cancel() {
+            if (mTimeoutAlarm != null) {
+                mTimeoutAlarm.cancel();
+                mTimeoutAlarm = null;
+            }
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/Advertisement.java b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
new file mode 100644
index 0000000..d42f6c7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.PresenceCredential;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A Nearby Presence advertisement to be advertised. */
+public abstract class Advertisement {
+
+    @BroadcastRequest.BroadcastVersion
+    int mVersion = BroadcastRequest.PRESENCE_VERSION_UNKNOWN;
+    int mLength;
+    @PresenceCredential.IdentityType int mIdentityType;
+    byte[] mIdentity;
+    byte[] mSalt;
+    List<Integer> mActions;
+
+    /** Serialize an {@link Advertisement} object into bytes. */
+    @Nullable
+    public byte[] toBytes() {
+        return new byte[0];
+    }
+
+    /** Returns the length of the advertisement. */
+    public int getLength() {
+        return mLength;
+    }
+
+    /** Returns the version in the advertisement. */
+    @BroadcastRequest.BroadcastVersion
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /** Returns the identity type in the advertisement. */
+    @PresenceCredential.IdentityType
+    public int getIdentityType() {
+        return mIdentityType;
+    }
+
+    /** Returns the identity bytes in the advertisement. */
+    public byte[] getIdentity() {
+        return mIdentity.clone();
+    }
+
+    /** Returns the salt of the advertisement. */
+    public byte[] getSalt() {
+        return mSalt.clone();
+    }
+
+    /** Returns the actions in the advertisement. */
+    public List<Integer> getActions() {
+        return new ArrayList<>(mActions);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
new file mode 100644
index 0000000..ae4a728
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Represents a data element header in Nearby Presence.
+ * Each header has 3 parts: tag, length and style.
+ * Tag: 1 bit (MSB at each byte). 1 for extending, which means there will be more bytes after
+ * the current one for the header.
+ * Length: The total length of a Data Element field. Length is up to 127 and is limited within
+ * the entire first byte in the header. (7 bits, MSB is the tag).
+ * Type: Represents {@link DataElement.DataType}. There is no limit for the type number.
+ *
+ * @hide
+ */
+public class DataElementHeader {
+    // Each Data reserved MSB for tag.
+    static final int TAG_BITMASK = 0b10000000;
+    static final int TAG_OFFSET = 7;
+
+    // If the header is only 1 byte, it has the format: 0b0LLLTTTT. (L for length, T for type.)
+    static final int SINGLE_AVAILABLE_LENGTH_BIT = 3;
+    static final int SINGLE_AVAILABLE_TYPE_BIT = 4;
+    static final int SINGLE_LENGTH_BITMASK = 0b01110000;
+    static final int SINGLE_LENGTH_OFFSET = SINGLE_AVAILABLE_TYPE_BIT;
+    static final int SINGLE_TYPE_BITMASK = 0b00001111;
+
+    // If there are multiple data element headers.
+    // First byte is always the length.
+    static final int MULTIPLE_LENGTH_BYTE = 1;
+    // Each byte reserves MSB for tag.
+    static final int MULTIPLE_BITMASK = 0b01111111;
+
+    @BroadcastRequest.BroadcastVersion
+    private final int mVersion;
+    @DataElement.DataType
+    private final int mDataType;
+    private final int mDataLength;
+
+    DataElementHeader(@BroadcastRequest.BroadcastVersion int version,
+            @DataElement.DataType int dataType, int dataLength) {
+        Preconditions.checkArgument(version == BroadcastRequest.PRESENCE_VERSION_V1,
+                "DataElementHeader is only supported in V1.");
+        Preconditions.checkArgument(dataLength >= 0, "Length should not be negative.");
+        Preconditions.checkArgument(dataLength < (1 << TAG_OFFSET),
+                "Data element should be equal or shorter than 128.");
+
+        this.mVersion = version;
+        this.mDataType = dataType;
+        this.mDataLength = dataLength;
+    }
+
+    /**
+     * The total type of the data element.
+     */
+    @DataElement.DataType
+    public int getDataType() {
+        return mDataType;
+    }
+
+    /**
+     * The total length of a Data Element field.
+     */
+    public int getDataLength() {
+        return mDataLength;
+    }
+
+    /** Serialize a {@link DataElementHeader} object into bytes. */
+    public byte[] toBytes() {
+        Preconditions.checkState(mVersion == BroadcastRequest.PRESENCE_VERSION_V1,
+                "DataElementHeader is only supported in V1.");
+        // Only 1 byte needed for the header
+        if (mDataType < (1 << SINGLE_AVAILABLE_TYPE_BIT)
+                && mDataLength < (1 << SINGLE_AVAILABLE_LENGTH_BIT)) {
+            return new byte[]{createSingleByteHeader(mDataType, mDataLength)};
+        }
+
+        return createMultipleBytesHeader(mDataType, mDataLength);
+    }
+
+    /** Creates a {@link DataElementHeader} object from bytes. */
+    @Nullable
+    public static DataElementHeader fromBytes(@BroadcastRequest.BroadcastVersion int version,
+            @Nonnull byte[] bytes) {
+        Objects.requireNonNull(bytes, "Data parsed in for DataElement should not be null.");
+
+        if (bytes.length == 0) {
+            return null;
+        }
+
+        if (bytes.length == 1) {
+            if (isExtending(bytes[0])) {
+                throw new IllegalArgumentException("The header is not complete.");
+            }
+            return new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+                    getTypeSingleByte(bytes[0]), getLengthSingleByte(bytes[0]));
+        }
+
+        // The first byte should be length and there should be at least 1 more byte following to
+        // represent type.
+        // The last header byte's MSB should be 0.
+        if (!isExtending(bytes[0]) || isExtending(bytes[bytes.length - 1])) {
+            throw new IllegalArgumentException("The header format is wrong.");
+        }
+
+        return new DataElementHeader(version,
+                getTypeMultipleBytes(Arrays.copyOfRange(bytes, 1, bytes.length)),
+                getHeaderValue(bytes[0]));
+    }
+
+    /** Creates a header based on type and length.
+     * This is used when the type is <= 16 and length is <= 7. */
+    static byte createSingleByteHeader(int type, int length) {
+        return (byte) (convertTag(/* extend= */ false)
+                | convertLengthSingleByte(length)
+                | convertTypeSingleByte(type));
+    }
+
+    /** Creates a header based on type and length.
+     * This is used when the type is > 16 or length is > 7. */
+    static byte[] createMultipleBytesHeader(int type, int length) {
+        List<Byte> typeIntList = convertTypeMultipleBytes(type);
+        byte[] res = new byte[typeIntList.size() + MULTIPLE_LENGTH_BYTE];
+        int index = 0;
+        res[index++] = convertLengthMultipleBytes(length);
+
+        for (int typeInt : typeIntList) {
+            res[index++] = (byte) typeInt;
+        }
+        return res;
+    }
+
+    /** Constructs a Data Element header with length indicated in byte format.
+     * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+     */
+    @VisibleForTesting
+    static int convertLengthSingleByte(int length) {
+        Preconditions.checkArgument(length >= 0, "Length should not be negative.");
+        Preconditions.checkArgument(length < (1 << SINGLE_AVAILABLE_LENGTH_BIT),
+                "In single Data Element header, length should be shorter than 8.");
+        return (length << SINGLE_LENGTH_OFFSET) & SINGLE_LENGTH_BITMASK;
+    }
+
+    /** Constructs a Data Element header with type indicated in byte format.
+     * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+     */
+    @VisibleForTesting
+    static int convertTypeSingleByte(int type) {
+        Preconditions.checkArgument(type >= 0, "Type should not be negative.");
+        Preconditions.checkArgument(type < (1 << SINGLE_AVAILABLE_TYPE_BIT),
+                "In single Data Element header, type should be smaller than 16.");
+
+        return type & SINGLE_TYPE_BITMASK;
+    }
+
+    /**
+     * Gets the length of Data Element from the header. (When there is only 1 byte of header)
+     */
+    static int getLengthSingleByte(byte header) {
+        Preconditions.checkArgument(!isExtending(header),
+                "Cannot apply this method for the extending header.");
+        return (header & SINGLE_LENGTH_BITMASK) >> SINGLE_LENGTH_OFFSET;
+    }
+
+    /**
+     * Gets the type of Data Element from the header. (When there is only 1 byte of header)
+     */
+    static int getTypeSingleByte(byte header) {
+        Preconditions.checkArgument(!isExtending(header),
+                "Cannot apply this method for the extending header.");
+        return header & SINGLE_TYPE_BITMASK;
+    }
+
+    /** Creates a DE(data element) header based on length.
+     * This is used when header is more than 1 byte. The first byte is always the length.
+     */
+    static byte convertLengthMultipleBytes(int length) {
+        Preconditions.checkArgument(length < (1 << TAG_OFFSET),
+                "Data element should be equal or shorter than 128.");
+        return (byte) (convertTag(/* extend= */ true) | (length & MULTIPLE_BITMASK));
+    }
+
+    /** Creates a DE(data element) header based on type.
+     * This is used when header is more than 1 byte. The first byte is always the length.
+     */
+    @VisibleForTesting
+    static List<Byte> convertTypeMultipleBytes(int type) {
+        List<Byte> typeBytes = new ArrayList<>();
+        while (type > 0) {
+            byte current = (byte) (type & MULTIPLE_BITMASK);
+            type = type >> TAG_OFFSET;
+            typeBytes.add(current);
+        }
+
+        Collections.reverse(typeBytes);
+        int size = typeBytes.size();
+        // The last byte's MSB should be 0.
+        for (int i = 0; i < size - 1; i++) {
+            typeBytes.set(i, (byte) (convertTag(/* extend= */ true) | typeBytes.get(i)));
+        }
+        return typeBytes;
+    }
+
+    /** Creates a DE(data element) header based on type.
+     * This is used when header is more than 1 byte. The first byte is always the length.
+     * Uses Integer when doing bit operation to avoid error.
+     */
+    @VisibleForTesting
+    static int getTypeMultipleBytes(byte[] typeByteArray) {
+        int type = 0;
+        int size = typeByteArray.length;
+        for (int i = 0; i < size; i++) {
+            type = (type << TAG_OFFSET) | getHeaderValue(typeByteArray[i]);
+        }
+        return type;
+    }
+
+    /** Gets the integer value of the 7 bits in the header. (The MSB is tag) */
+    @VisibleForTesting
+    static int getHeaderValue(byte header) {
+        return (header & MULTIPLE_BITMASK);
+    }
+
+    /** Sets the MSB of the header byte. If this is the last byte of headers, MSB is 0.
+     * If there are at least header following, the MSB is 1.
+     */
+    @VisibleForTesting
+    static byte convertTag(boolean extend) {
+        return (byte) (extend ? 0b10000000 : 0b00000000);
+    }
+
+    /** Returns {@code true} if there are at least 1 byte of header after the current one. */
+    @VisibleForTesting
+    static boolean isExtending(byte header) {
+        return (header & TAG_BITMASK) != 0;
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
new file mode 100644
index 0000000..34a7514
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PublicCredential;
+import android.util.Log;
+
+import com.android.server.nearby.util.encryption.Cryptor;
+import com.android.server.nearby.util.encryption.CryptorImpFake;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A Nearby Presence advertisement to be advertised on BT5.0 devices.
+ *
+ * <p>Serializable between Java object and bytes formats. Java object is used at the upper scanning
+ * and advertising interface as an abstraction of the actual bytes. Bytes format is used at the
+ * underlying BLE and mDNS stacks, which do necessary slicing and merging based on advertising
+ * capacities.
+ *
+ * The extended advertisement is defined in the format below:
+ * Header (1 byte) | salt (1+2 bytes) | Identity + filter (2+16 bytes)
+ * | repeated DE fields (various bytes)
+ * The header contains:
+ * version (3 bits) | 5 bit reserved for future use (RFU)
+ */
+public class ExtendedAdvertisement extends Advertisement{
+
+    public static final int SALT_DATA_LENGTH = 2;
+
+    static final int HEADER_LENGTH = 1;
+
+    static final int IDENTITY_DATA_LENGTH = 16;
+
+    private final List<DataElement> mDataElements;
+
+    private final byte[] mAuthenticityKey;
+
+    // All Data Elements including salt and identity.
+    // Each list item (byte array) is a Data Element (with its header).
+    private final List<byte[]> mCompleteDataElementsBytes;
+    // Signature generated from data elements.
+    private final byte[] mHmacTag;
+
+    /**
+     * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+     * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
+     */
+    @Nullable
+    public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
+        if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+            Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
+            return null;
+        }
+
+        byte[] salt = request.getSalt();
+        if (salt.length != SALT_DATA_LENGTH) {
+            Log.v(TAG, "Salt does not match correct length");
+            return null;
+        }
+
+        byte[] identity = request.getCredential().getMetadataEncryptionKey();
+        byte[] authenticityKey = request.getCredential().getAuthenticityKey();
+        if (identity.length != IDENTITY_DATA_LENGTH) {
+            Log.v(TAG, "Identity does not match correct length");
+            return null;
+        }
+
+        List<Integer> actions = request.getActions();
+        if (actions.isEmpty()) {
+            Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
+            return null;
+        }
+
+        List<DataElement> dataElements = request.getExtendedProperties();
+        return new ExtendedAdvertisement(
+                request.getCredential().getIdentityType(),
+                identity,
+                salt,
+                authenticityKey,
+                actions,
+                dataElements);
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytes() {
+        ByteBuffer buffer = ByteBuffer.allocate(getLength());
+
+        // Header
+        buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
+
+        // Salt
+        buffer.put(mCompleteDataElementsBytes.get(0));
+
+        // Identity
+        buffer.put(mCompleteDataElementsBytes.get(1));
+
+        List<Byte> rawDataBytes = new ArrayList<>();
+        // Data Elements (Already includes salt and identity)
+        for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
+            byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
+            for (Byte b : dataElementBytes) {
+                rawDataBytes.add(b);
+            }
+        }
+
+        byte[] dataElements = new byte[rawDataBytes.size()];
+        for (int i = 0; i < rawDataBytes.size(); i++) {
+            dataElements[i] = rawDataBytes.get(i);
+        }
+
+        buffer.put(
+                getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
+
+        buffer.put(mHmacTag);
+
+        return buffer.array();
+    }
+
+    /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+     * {@code null} when there is something when parsing.
+     */
+    @Nullable
+    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
+        @BroadcastRequest.BroadcastVersion
+        int version = ExtendedAdvertisementUtils.getVersion(bytes);
+        if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+            Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
+            return null;
+        }
+
+        byte[] authenticityKey = publicCredential.getAuthenticityKey();
+
+        int index = HEADER_LENGTH;
+        // Salt
+        byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+        DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
+        if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
+            Log.v(TAG, "First data element has to be salt.");
+            return null;
+        }
+        index += saltHeaderArray.length;
+        byte[] salt = new byte[saltHeader.getDataLength()];
+        for (int i = 0; i < saltHeader.getDataLength(); i++) {
+            salt[i] = bytes[index++];
+        }
+
+        // Identity
+        byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+        DataElementHeader identityHeader =
+                DataElementHeader.fromBytes(version, identityHeaderArray);
+        if (identityHeader == null) {
+            Log.v(TAG, "The second element has to be identity.");
+            return null;
+        }
+        index += identityHeaderArray.length;
+        @PresenceCredential.IdentityType int identityType =
+                toPresenceCredentialIdentityType(identityHeader.getDataType());
+        if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
+            Log.v(TAG, "The identity type is unknown.");
+            return null;
+        }
+        byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
+        for (int i = 0; i < identityHeader.getDataLength(); i++) {
+            encryptedIdentity[i] = bytes[index++];
+        }
+        byte[] identity =
+                CryptorImpIdentityV1
+                        .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
+
+        Cryptor cryptor = getCryptor(/* encrypt= */ true);
+        byte[] encryptedDataElements =
+                new byte[bytes.length - index - cryptor.getSignatureLength()];
+        // Decrypt other data elements
+        System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
+        byte[] decryptedDataElements =
+                cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
+        if (decryptedDataElements == null) {
+            return null;
+        }
+
+        // Verify the computed HMAC tag is equal to HMAC tag in advertisement
+        if (cryptor.getSignatureLength() > 0) {
+            byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+            System.arraycopy(
+                    bytes, bytes.length - cryptor.getSignatureLength(),
+                    expectedHmacTag, 0, cryptor.getSignatureLength());
+            if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
+                Log.e(TAG, "HMAC tags not match.");
+                return null;
+            }
+        }
+
+        int dataElementArrayIndex = 0;
+        // Other Data Elements
+        List<Integer> actions = new ArrayList<>();
+        List<DataElement> dataElements = new ArrayList<>();
+        while (dataElementArrayIndex < decryptedDataElements.length) {
+            byte[] deHeaderArray = ExtendedAdvertisementUtils
+                    .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
+            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+            dataElementArrayIndex += deHeaderArray.length;
+
+            @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
+            if (type == DataElement.DataType.ACTION) {
+                if (deHeader.getDataLength() != 1) {
+                    Log.v(TAG, "Action id should only 1 byte.");
+                    return null;
+                }
+                actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
+            } else {
+                if (isSaltOrIdentity(type)) {
+                    Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+                            + " and one identity in the advertisement.");
+                    return null;
+                }
+                byte[] deData = new byte[deHeader.getDataLength()];
+                for (int i = 0; i < deHeader.getDataLength(); i++) {
+                    deData[i] = decryptedDataElements[dataElementArrayIndex++];
+                }
+                dataElements.add(new DataElement(type, deData));
+            }
+        }
+
+        return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
+                dataElements);
+    }
+
+    /** Returns the {@link DataElement}s in the advertisement. */
+    public List<DataElement> getDataElements() {
+        return new ArrayList<>(mDataElements);
+    }
+
+    /** Returns the {@link DataElement}s in the advertisement according to the key. */
+    public List<DataElement> getDataElements(@DataElement.DataType int key) {
+        List<DataElement> res = new ArrayList<>();
+        for (DataElement dataElement : mDataElements) {
+            if (key == dataElement.getKey()) {
+                res.add(dataElement);
+            }
+        }
+        return res;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "ExtendedAdvertisement:"
+                        + "<VERSION: %s, length: %s, dataElementCount: %s, identityType: %s,"
+                        + " identity: %s, salt: %s, actions: %s>",
+                getVersion(),
+                getLength(),
+                getDataElements().size(),
+                getIdentityType(),
+                Arrays.toString(getIdentity()),
+                Arrays.toString(getSalt()),
+                getActions());
+    }
+
+    ExtendedAdvertisement(
+            @PresenceCredential.IdentityType int identityType,
+            byte[] identity,
+            byte[] salt,
+            byte[] authenticityKey,
+            List<Integer> actions,
+            List<DataElement> dataElements) {
+        this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
+        this.mIdentityType = identityType;
+        this.mIdentity = identity;
+        this.mSalt = salt;
+        this.mAuthenticityKey = authenticityKey;
+        this.mActions = actions;
+        this.mDataElements = dataElements;
+        this.mCompleteDataElementsBytes = new ArrayList<>();
+
+        int length = HEADER_LENGTH; // header
+
+        // Salt
+        DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
+        byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
+        mCompleteDataElementsBytes.add(saltByteArray);
+        length += saltByteArray.length;
+
+        // Identity
+        byte[] encryptedIdentity =
+                CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
+        DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
+        byte[] identityByteArray =
+                ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
+        mCompleteDataElementsBytes.add(identityByteArray);
+        length += identityByteArray.length;
+
+        List<Byte> dataElementBytes = new ArrayList<>();
+        // Intents
+        for (int action : mActions) {
+            DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
+                    new byte[] {(byte) action});
+            byte[] intentByteArray =
+                    ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
+            mCompleteDataElementsBytes.add(intentByteArray);
+            for (Byte b : intentByteArray) {
+                dataElementBytes.add(b);
+            }
+        }
+
+        // Data Elements (Extended properties)
+        for (DataElement dataElement : mDataElements) {
+            byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
+            mCompleteDataElementsBytes.add(deByteArray);
+            for (Byte b : deByteArray) {
+                dataElementBytes.add(b);
+            }
+        }
+
+        byte[] data = new byte[dataElementBytes.size()];
+        for (int i = 0; i < dataElementBytes.size(); i++) {
+            data[i] = dataElementBytes.get(i);
+        }
+        Cryptor cryptor = getCryptor(/* encrypt= */ true);
+        byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
+
+        length += encryptedDeBytes.length;
+
+        // Signature
+        byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
+        mHmacTag = hmacTag;
+        length += hmacTag.length;
+
+        this.mLength = length;
+    }
+
+    @PresenceCredential.IdentityType
+    private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
+        switch (type) {
+            case DataElement.DataType.PRIVATE_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+            case DataElement.DataType.PROVISIONED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+            case DataElement.DataType.TRUSTED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+            case DataElement.DataType.PUBLIC_IDENTITY:
+            default:
+                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+        }
+    }
+
+    @DataElement.DataType
+    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+        switch (identityType) {
+            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+                return DataElement.DataType.PRIVATE_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+                return DataElement.DataType.PROVISIONED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+                return DataElement.DataType.TRUSTED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+            default:
+                return DataElement.DataType.PUBLIC_IDENTITY;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
+     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+     */
+    private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
+        return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
+                || type == DataElement.DataType.TRUSTED_IDENTITY
+                || type == DataElement.DataType.PROVISIONED_IDENTITY
+                || type == DataElement.DataType.PUBLIC_IDENTITY;
+    }
+
+    private static Cryptor getCryptor(boolean encrypt) {
+        if (encrypt) {
+            Log.d(TAG, "get V1 Cryptor");
+            return CryptorImpV1.getInstance();
+        }
+        Log.d(TAG, "get fake Cryptor");
+        return CryptorImpFake.getInstance();
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
new file mode 100644
index 0000000..06d0f2b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.presence.ExtendedAdvertisement.HEADER_LENGTH;
+
+import android.annotation.SuppressLint;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides serialization and deserialization util methods for {@link ExtendedAdvertisement}.
+ */
+public final class ExtendedAdvertisementUtils {
+
+    // Advertisement header related static fields.
+    private static final int VERSION_MASK = 0b11100000;
+    private static final int VERSION_MASK_AFTER_SHIT = 0b00000111;
+    private static final int HEADER_INDEX = 0;
+    private static final int HEADER_VERSION_OFFSET = 5;
+
+    /**
+     * Constructs the header of a {@link ExtendedAdvertisement}.
+     * 3 bit version, and 5 bit reserved for future use (RFU).
+     */
+    public static byte constructHeader(@BroadcastRequest.BroadcastVersion int version) {
+        return (byte) ((version << 5) & VERSION_MASK);
+    }
+
+    /** Returns the {@link BroadcastRequest.BroadcastVersion} from the advertisement
+     * in bytes format. */
+    public static int getVersion(byte[] advertisement) {
+        if (advertisement.length < HEADER_LENGTH) {
+            throw new IllegalArgumentException("Advertisement must contain header");
+        }
+        return ((advertisement[HEADER_INDEX] & VERSION_MASK) >> HEADER_VERSION_OFFSET)
+                & VERSION_MASK_AFTER_SHIT;
+    }
+
+    /** Returns the {@link DataElementHeader} from the advertisement in bytes format. */
+    public static byte[] getDataElementHeader(byte[] advertisement, int startIndex) {
+        Preconditions.checkArgument(startIndex < advertisement.length,
+                "Advertisement has no longer data left.");
+        List<Byte> headerBytes = new ArrayList<>();
+        while (startIndex < advertisement.length) {
+            byte current = advertisement[startIndex];
+            headerBytes.add(current);
+            if (!DataElementHeader.isExtending(current)) {
+                int size = headerBytes.size();
+                byte[] res = new byte[size];
+                for (int i = 0; i < size; i++) {
+                    res[i] = headerBytes.get(i);
+                }
+                return res;
+            }
+            startIndex++;
+        }
+        throw new IllegalArgumentException("There is no end of the DataElement header.");
+    }
+
+    /**
+     * Constructs {@link DataElement}, including header(s) and actual data element data.
+     *
+     * Suppresses warning because {@link DataElement} checks isValidType in constructor.
+     */
+    @SuppressLint("WrongConstant")
+    public static byte[] convertDataElementToBytes(DataElement dataElement) {
+        @DataElement.DataType int type = dataElement.getKey();
+        byte[] data = dataElement.getValue();
+        DataElementHeader header = new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+                type, data.length);
+        byte[] headerByteArray = header.toBytes();
+
+        byte[] res = new byte[headerByteArray.length + data.length];
+        System.arraycopy(headerByteArray, 0, res, 0, headerByteArray.length);
+        System.arraycopy(data, 0, res, headerByteArray.length, data.length);
+        return res;
+    }
+
+    private ExtendedAdvertisementUtils() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
index e4df673..ae53ada 100644
--- a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
@@ -24,7 +24,6 @@
 import com.android.internal.util.Preconditions;
 
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -42,7 +41,7 @@
 // The header contains:
 // version (3 bits) | provision_mode_flag (1 bit) | identity_type (3 bits) |
 // extended_advertisement_mode (1 bit)
-public class FastAdvertisement {
+public class FastAdvertisement extends Advertisement {
 
     private static final int FAST_ADVERTISEMENT_MAX_LENGTH = 24;
 
@@ -85,7 +84,8 @@
                 (byte) request.getTxPower());
     }
 
-    /** Serialize an {@link FastAdvertisement} object into bytes. */
+    /** Serialize a {@link FastAdvertisement} object into bytes. */
+    @Override
     public byte[] toBytes() {
         ByteBuffer buffer = ByteBuffer.allocate(getLength());
 
@@ -100,18 +100,8 @@
         return buffer.array();
     }
 
-    private final int mLength;
-
     private final int mLtvFieldCount;
 
-    @PresenceCredential.IdentityType private final int mIdentityType;
-
-    private final byte[] mIdentity;
-
-    private final byte[] mSalt;
-
-    private final List<Integer> mActions;
-
     @Nullable
     private final Byte mTxPower;
 
@@ -121,6 +111,7 @@
             byte[] salt,
             List<Integer> actions,
             @Nullable Byte txPower) {
+        this.mVersion = BroadcastRequest.PRESENCE_VERSION_V0;
         this.mIdentityType = identityType;
         this.mIdentity = identity;
         this.mSalt = salt;
@@ -143,44 +134,12 @@
                 "FastAdvertisement exceeds maximum length");
     }
 
-    /** Returns the version in the advertisement. */
-    @BroadcastRequest.BroadcastVersion
-    public int getVersion() {
-        return BroadcastRequest.PRESENCE_VERSION_V0;
-    }
-
-    /** Returns the identity type in the advertisement. */
-    @PresenceCredential.IdentityType
-    public int getIdentityType() {
-        return mIdentityType;
-    }
-
-    /** Returns the identity bytes in the advertisement. */
-    public byte[] getIdentity() {
-        return mIdentity.clone();
-    }
-
-    /** Returns the salt of the advertisement. */
-    public byte[] getSalt() {
-        return mSalt.clone();
-    }
-
-    /** Returns the actions in the advertisement. */
-    public List<Integer> getActions() {
-        return new ArrayList<>(mActions);
-    }
-
     /** Returns the adjusted TX Power in the advertisement. Null if not available. */
     @Nullable
     public Byte getTxPower() {
         return mTxPower;
     }
 
-    /** Returns the length of the advertisement. */
-    public int getLength() {
-        return mLength;
-    }
-
     /** Returns the count of LTV fields in the advertisement. */
     public int getLtvFieldCount() {
         return mLtvFieldCount;
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
index d1c72ae..5a76d96 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -16,31 +16,55 @@
 
 package com.android.server.nearby.presence;
 
-import android.nearby.NearbyDevice;
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.nearby.DataElement;
 import android.nearby.NearbyDeviceParcelable;
 import android.nearby.PresenceDevice;
 import android.nearby.PresenceScanFilter;
 import android.nearby.PublicCredential;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 /** Represents a Presence discovery result. */
 public class PresenceDiscoveryResult {
 
     /** Creates a {@link PresenceDiscoveryResult} from the scan data. */
     public static PresenceDiscoveryResult fromDevice(NearbyDeviceParcelable device) {
+        PresenceDevice presenceDevice = device.getPresenceDevice();
+        if (presenceDevice != null) {
+            return new PresenceDiscoveryResult.Builder()
+                    .setTxPower(device.getTxPower())
+                    .setRssi(device.getRssi())
+                    .setSalt(presenceDevice.getSalt())
+                    .setPublicCredential(device.getPublicCredential())
+                    .addExtendedProperties(presenceDevice.getExtendedProperties())
+                    .setEncryptedIdentityTag(device.getEncryptionKeyTag())
+                    .build();
+        }
         byte[] salt = device.getSalt();
         if (salt == null) {
             salt = new byte[0];
         }
-        return new PresenceDiscoveryResult.Builder()
-                .setTxPower(device.getTxPower())
+
+        PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder();
+        builder.setTxPower(device.getTxPower())
                 .setRssi(device.getRssi())
                 .setSalt(salt)
                 .addPresenceAction(device.getAction())
-                .setPublicCredential(device.getPublicCredential())
-                .build();
+                .setPublicCredential(device.getPublicCredential());
+        if (device.getPresenceDevice() != null) {
+            builder.addExtendedProperties(device.getPresenceDevice().getExtendedProperties());
+        }
+        return builder.build();
     }
 
     private final int mTxPower;
@@ -48,25 +72,35 @@
     private final byte[] mSalt;
     private final List<Integer> mPresenceActions;
     private final PublicCredential mPublicCredential;
+    private final List<DataElement> mExtendedProperties;
+    private final byte[] mEncryptedIdentityTag;
 
     private PresenceDiscoveryResult(
             int txPower,
             int rssi,
             byte[] salt,
             List<Integer> presenceActions,
-            PublicCredential publicCredential) {
+            PublicCredential publicCredential,
+            List<DataElement> extendedProperties,
+            byte[] encryptedIdentityTag) {
         mTxPower = txPower;
         mRssi = rssi;
         mSalt = salt;
         mPresenceActions = presenceActions;
         mPublicCredential = publicCredential;
+        mExtendedProperties = extendedProperties;
+        mEncryptedIdentityTag = encryptedIdentityTag;
     }
 
     /** Returns whether the discovery result matches the scan filter. */
     public boolean matches(PresenceScanFilter scanFilter) {
+        if (accountKeyMatches(scanFilter.getExtendedProperties())) {
+            return true;
+        }
+
         return pathLossMatches(scanFilter.getMaxPathLoss())
                 && actionMatches(scanFilter.getPresenceActions())
-                && credentialMatches(scanFilter.getCredentials());
+                && identityMatches(scanFilter.getCredentials());
     }
 
     private boolean pathLossMatches(int maxPathLoss) {
@@ -80,21 +114,47 @@
         return filterActions.stream().anyMatch(mPresenceActions::contains);
     }
 
-    private boolean credentialMatches(List<PublicCredential> credentials) {
-        return credentials.contains(mPublicCredential);
+    @VisibleForTesting
+    boolean accountKeyMatches(List<DataElement> extendedProperties) {
+        Set<byte[]> accountKeys = new ArraySet<>();
+        for (DataElement requestedDe : mExtendedProperties) {
+            if (requestedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+                continue;
+            }
+            accountKeys.add(requestedDe.getValue());
+        }
+        for (DataElement scannedDe : extendedProperties) {
+            if (scannedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+                continue;
+            }
+            // If one account key matches, then returns true.
+            for (byte[] key : accountKeys) {
+                if (Arrays.equals(key, scannedDe.getValue())) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
     }
 
-    /** Converts a presence device from the discovery result. */
-    public PresenceDevice toPresenceDevice() {
-        return new PresenceDevice.Builder(
-                // Use the public credential hash as the device Id.
-                String.valueOf(mPublicCredential.hashCode()),
-                mSalt,
-                mPublicCredential.getSecretId(),
-                mPublicCredential.getEncryptedMetadata())
-                .setRssi(mRssi)
-                .addMedium(NearbyDevice.Medium.BLE)
-                .build();
+    @VisibleForTesting
+    /** Gets presence {@link DataElement}s of the discovery result. */
+    public List<DataElement> getExtendedProperties() {
+        return mExtendedProperties;
+    }
+
+    private boolean identityMatches(List<PublicCredential> publicCredentials) {
+        if (mEncryptedIdentityTag.length == 0) {
+            return true;
+        }
+        for (PublicCredential publicCredential : publicCredentials) {
+            if (Arrays.equals(
+                    mEncryptedIdentityTag, publicCredential.getEncryptedMetadataKeyTag())) {
+                return true;
+            }
+        }
+        return false;
     }
 
     /** Builder for {@link PresenceDiscoveryResult}. */
@@ -105,9 +165,12 @@
 
         private PublicCredential mPublicCredential;
         private final List<Integer> mPresenceActions;
+        private final List<DataElement> mExtendedProperties;
+        private byte[] mEncryptedIdentityTag = new byte[0];
 
         public Builder() {
             mPresenceActions = new ArrayList<>();
+            mExtendedProperties = new ArrayList<>();
         }
 
         /** Sets the calibrated tx power for the discovery result. */
@@ -130,7 +193,18 @@
 
         /** Sets the public credential for the discovery result. */
         public Builder setPublicCredential(PublicCredential publicCredential) {
-            mPublicCredential = publicCredential;
+            if (publicCredential != null) {
+                mPublicCredential = publicCredential;
+            }
+            return this;
+        }
+
+        /** Sets the encrypted identity tag for the discovery result. Usually it is passed from
+         * {@link NearbyDeviceParcelable} and the tag is calculated with authenticity key when
+         * receiving an advertisement.
+         */
+        public Builder setEncryptedIdentityTag(byte[] encryptedIdentityTag) {
+            mEncryptedIdentityTag = encryptedIdentityTag;
             return this;
         }
 
@@ -140,10 +214,34 @@
             return this;
         }
 
+        /** Adds presence {@link DataElement}s of the discovery result. */
+        public Builder addExtendedProperties(DataElement dataElement) {
+            if (dataElement.getKey() == DataElement.DataType.ACTION) {
+                byte[] value = dataElement.getValue();
+                if (value.length == 1) {
+                    addPresenceAction(Byte.toUnsignedInt(value[0]));
+                } else {
+                    Log.e(TAG, "invalid action data element");
+                }
+            } else {
+                mExtendedProperties.add(dataElement);
+            }
+            return this;
+        }
+
+        /** Adds presence {@link DataElement}s of the discovery result. */
+        public Builder addExtendedProperties(@NonNull List<DataElement> dataElements) {
+            for (DataElement dataElement : dataElements) {
+                addExtendedProperties(dataElement);
+            }
+            return this;
+        }
+
         /** Builds a {@link PresenceDiscoveryResult}. */
         public PresenceDiscoveryResult build() {
             return new PresenceDiscoveryResult(
-                    mTxPower, mRssi, mSalt, mPresenceActions, mPublicCredential);
+                    mTxPower, mRssi, mSalt, mPresenceActions,
+                    mPublicCredential, mExtendedProperties, mEncryptedIdentityTag);
         }
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
new file mode 100644
index 0000000..0a51068
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+
+/** PresenceManager is the class initiated in nearby service to handle presence related work. */
+public class PresenceManager {
+
+    final Context mContext;
+    private final IntentFilter mIntentFilter;
+
+    @VisibleForTesting
+    final ScanCallback mScanCallback =
+            new ScanCallback() {
+                @Override
+                public void onDiscovered(@NonNull NearbyDevice device) {
+                    Log.i(TAG, "[PresenceManager] discovered Device.");
+                    PresenceDevice presenceDevice = (PresenceDevice) device;
+                    List<DataElement> dataElements = presenceDevice.getExtendedProperties();
+                    for (DataElement dataElement : dataElements) {
+                        Log.i(TAG, "[PresenceManager] Data Element key "
+                                + dataElement.getKey());
+                        Log.i(TAG, "[PresenceManager] Data Element value "
+                                + Arrays.toString(dataElement.getValue()));
+                    }
+                }
+
+                @Override
+                public void onUpdated(@NonNull NearbyDevice device) {}
+
+                @Override
+                public void onLost(@NonNull NearbyDevice device) {}
+
+                @Override
+                public void onError(int errorCode) {
+                    Log.w(TAG, "[PresenceManager] Scan error is " + errorCode);
+                }
+            };
+
+    private final BroadcastReceiver mScreenBroadcastReceiver =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    NearbyManager manager = getNearbyManager();
+                    if (manager == null) {
+                        Log.e(TAG, "Nearby Manager is null");
+                        return;
+                    }
+                    if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+                        Log.d(TAG, "PresenceManager Start scan.");
+                        PublicCredential publicCredential =
+                                new PublicCredential.Builder(new byte[]{1}, new byte[]{1},
+                                        new byte[]{1}, new byte[]{1}, new byte[]{1}).build();
+                        PresenceScanFilter presenceScanFilter =
+                                new PresenceScanFilter.Builder()
+                                        .setMaxPathLoss(3)
+                                        .addCredential(publicCredential)
+                                        .addPresenceAction(1)
+                                        .addExtendedProperty(new DataElement(
+                                                DataElement.DataType.ACCOUNT_KEY_DATA,
+                                                new byte[16]))
+                                        .build();
+                        ScanRequest scanRequest =
+                                new ScanRequest.Builder()
+                                        .setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)
+                                        .addScanFilter(presenceScanFilter)
+                                        .build();
+                        Log.d(
+                                TAG,
+                                String.format(
+                                        Locale.getDefault(),
+                                        "[PresenceManager] Start Presence scan with request: %s",
+                                        scanRequest.toString()));
+                        manager.startScan(
+                                scanRequest, Executors.newSingleThreadExecutor(), mScanCallback);
+                    } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+                        Log.d(TAG, "PresenceManager Stop scan.");
+                        manager.stopScan(mScanCallback);
+                    }
+                }
+            };
+
+    public PresenceManager(Context context) {
+        mContext = context;
+        mIntentFilter = new IntentFilter();
+    }
+
+    /** Null when the Nearby Service is not available. */
+    @Nullable
+    private NearbyManager getNearbyManager() {
+        return (NearbyManager)
+                mContext.getApplicationContext()
+                        .getSystemService(Context.NEARBY_SERVICE);
+    }
+
+    /** Function called when nearby service start. */
+    public void initiate() {
+        mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+        mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+        mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
index f136695..3de6ff0 100644
--- a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanCallback;
 import android.nearby.ScanFilter;
 import android.nearby.ScanRequest;
 import android.util.Log;
@@ -40,15 +41,6 @@
     protected final DiscoveryProviderController mController;
     protected final Executor mExecutor;
     protected Listener mListener;
-    protected List<ScanFilter> mScanFilters;
-
-    /** Interface for listening to discovery providers. */
-    public interface Listener {
-        /**
-         * Called when a provider has a new nearby device available. May be invoked from any thread.
-         */
-        void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
-    }
 
     protected AbstractDiscoveryProvider(Context context, Executor executor) {
         mContext = context;
@@ -77,14 +69,33 @@
     protected void invalidateScanMode() {}
 
     /**
+     * Callback invoked to inform the provider of new provider scan filters which replaces any prior
+     * provider filters. Always invoked on the provider executor.
+     */
+    protected void onSetScanFilters(List<ScanFilter> filters) {}
+
+    /**
      * Retrieves the controller for this discovery provider. Should never be invoked by subclasses,
      * as a discovery provider should not be controlling itself. Using this method from subclasses
      * could also result in deadlock.
      */
-    protected DiscoveryProviderController getController() {
+    public DiscoveryProviderController getController() {
         return mController;
     }
 
+    /** Interface for listening to discovery providers. */
+    public interface Listener {
+        /**
+         * Called when a provider has a new nearby device available. May be invoked from any thread.
+         */
+        void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice);
+
+        /**
+         * Called when a provider found error from the scan.
+         */
+        void onError(@ScanCallback.ErrorCode int errorCode);
+    }
+
     private class Controller implements DiscoveryProviderController {
 
         private boolean mStarted = false;
@@ -120,6 +131,12 @@
             mExecutor.execute(AbstractDiscoveryProvider.this::onStop);
         }
 
+        @ScanRequest.ScanMode
+        @Override
+        public int getProviderScanMode() {
+            return mScanMode;
+        }
+
         @Override
         public void setProviderScanMode(@ScanRequest.ScanMode int scanMode) {
             if (mScanMode == scanMode) {
@@ -130,15 +147,9 @@
             mExecutor.execute(AbstractDiscoveryProvider.this::invalidateScanMode);
         }
 
-        @ScanRequest.ScanMode
-        @Override
-        public int getProviderScanMode() {
-            return mScanMode;
-        }
-
         @Override
         public void setProviderScanFilters(List<ScanFilter> filters) {
-            mScanFilters = filters;
+            mExecutor.execute(() -> onSetScanFilters(filters));
         }
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 67392ad..6829fba 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -16,17 +16,24 @@
 
 package com.android.server.nearby.provider;
 
+import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
+
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.le.AdvertiseCallback;
 import android.bluetooth.le.AdvertiseData;
 import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSet;
+import android.bluetooth.le.AdvertisingSetCallback;
+import android.bluetooth.le.AdvertisingSetParameters;
 import android.bluetooth.le.BluetoothLeAdvertiser;
 import android.nearby.BroadcastCallback;
-import android.os.ParcelUuid;
+import android.nearby.BroadcastRequest;
+import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.injector.Injector;
 
-import java.util.UUID;
 import java.util.concurrent.Executor;
 
 /**
@@ -37,7 +44,7 @@
     /**
      * Listener for Broadcast status changes.
      */
-    interface BroadcastListener {
+    public interface BroadcastListener {
         void onStatusChanged(int status);
     }
 
@@ -46,13 +53,19 @@
 
     private BroadcastListener mBroadcastListener;
     private boolean mIsAdvertising;
-
-    BleBroadcastProvider(Injector injector, Executor executor) {
+    @VisibleForTesting
+    AdvertisingSetCallback mAdvertisingSetCallback;
+    public BleBroadcastProvider(Injector injector, Executor executor) {
         mInjector = injector;
         mExecutor = executor;
+        mAdvertisingSetCallback = getAdvertisingSetCallback();
     }
 
-    void start(byte[] advertisementPackets, BroadcastListener listener) {
+    /**
+     * Starts to broadcast with given bytes.
+     */
+    public void start(@BroadcastRequest.BroadcastVersion int version, byte[] advertisementPackets,
+            BroadcastListener listener) {
         if (mIsAdvertising) {
             stop();
         }
@@ -63,23 +76,35 @@
                     mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
             if (bluetoothLeAdvertiser != null) {
                 advertiseStarted = true;
-                AdvertiseSettings settings =
-                        new AdvertiseSettings.Builder()
-                                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
-                                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
-                                .setConnectable(true)
-                                .build();
-
-                // TODO(b/230538655) Use empty data until Presence V1 protocol is implemented.
-                ParcelUuid emptyParcelUuid = new ParcelUuid(new UUID(0L, 0L));
-                byte[] emptyAdvertisementPackets = new byte[0];
                 AdvertiseData advertiseData =
                         new AdvertiseData.Builder()
-                                .addServiceData(emptyParcelUuid, emptyAdvertisementPackets).build();
+                                .addServiceData(PRESENCE_UUID, advertisementPackets).build();
                 try {
                     mBroadcastListener = listener;
-                    bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
+                    switch (version) {
+                        case BroadcastRequest.PRESENCE_VERSION_V0:
+                            bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
+                                    advertiseData, this);
+                            break;
+                        case BroadcastRequest.PRESENCE_VERSION_V1:
+                            if (adapter.isLeExtendedAdvertisingSupported()) {
+                                bluetoothLeAdvertiser.startAdvertisingSet(
+                                        getAdvertisingSetParameters(),
+                                        advertiseData,
+                                        null, null, null, mAdvertisingSetCallback);
+                            } else {
+                                Log.w(TAG, "Failed to start advertising set because the chipset"
+                                        + " does not supports LE Extended Advertising feature.");
+                                advertiseStarted = false;
+                            }
+                            break;
+                        default:
+                            Log.w(TAG, "Failed to start advertising set because the advertisement"
+                                    + " is wrong.");
+                            advertiseStarted = false;
+                    }
                 } catch (NullPointerException | IllegalStateException | SecurityException e) {
+                    Log.w(TAG, "Failed to start advertising.", e);
                     advertiseStarted = false;
                 }
             }
@@ -89,7 +114,10 @@
         }
     }
 
-    void stop() {
+    /**
+     * Stops current advertisement.
+     */
+    public void stop() {
         if (mIsAdvertising) {
             BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
             if (adapter != null) {
@@ -97,6 +125,7 @@
                         mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
                 if (bluetoothLeAdvertiser != null) {
                     bluetoothLeAdvertiser.stopAdvertising(this);
+                    bluetoothLeAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
                 }
             }
             mBroadcastListener = null;
@@ -120,4 +149,41 @@
             mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
         }
     }
+
+    private static AdvertiseSettings getAdvertiseSettings() {
+        return new AdvertiseSettings.Builder()
+                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+                .setConnectable(true)
+                .build();
+    }
+
+    private static AdvertisingSetParameters getAdvertisingSetParameters() {
+        return new AdvertisingSetParameters.Builder()
+                .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
+                .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM)
+                .setIncludeTxPower(true)
+                .setConnectable(true)
+                .build();
+    }
+
+    private AdvertisingSetCallback getAdvertisingSetCallback() {
+        return new AdvertisingSetCallback() {
+            @Override
+            public void onAdvertisingSetStarted(AdvertisingSet advertisingSet,
+                    int txPower, int status) {
+                if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+                    if (mBroadcastListener != null) {
+                        mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_OK);
+                    }
+                    mIsAdvertising = true;
+                } else {
+                    Log.e(TAG, "Starts advertising failed in status " + status);
+                    if (mBroadcastListener != null) {
+                        mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+                    }
+                }
+            }
+        };
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index e2fbe77..355f7cf 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.provider;
 
+import static android.nearby.ScanCallback.ERROR_UNKNOWN;
+
 import static com.android.server.nearby.NearbyService.TAG;
 import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
 
@@ -28,18 +30,27 @@
 import android.bluetooth.le.ScanResult;
 import android.bluetooth.le.ScanSettings;
 import android.content.Context;
+import android.nearby.DataElement;
 import android.nearby.NearbyDevice;
 import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
 import android.nearby.ScanRequest;
 import android.os.ParcelUuid;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
+import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.ForegroundThread;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
 
 import com.google.common.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
@@ -52,15 +63,32 @@
     // Don't block the thread as it may be used by other services.
     private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
     private final Injector mInjector;
+    private final Object mLock = new Object();
+    // Null when the filters are never set
+    @VisibleForTesting
+    @GuardedBy("mLock")
+    @Nullable
+    private List<android.nearby.ScanFilter> mScanFilters;
+    private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
+            new android.bluetooth.le.ScanCallback() {
+                @Override
+                public void onScanResult(int callbackType, ScanResult scanResult) {
+                }
+                @Override
+                public void onScanFailed(int errorCode) {
+                }
+            };
     private android.bluetooth.le.ScanCallback mScanCallback =
             new android.bluetooth.le.ScanCallback() {
                 @Override
                 public void onScanResult(int callbackType, ScanResult scanResult) {
                     NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
-                    builder.setMedium(NearbyDevice.Medium.BLE)
+                    String bleAddress = scanResult.getDevice().getAddress();
+                    builder.setDeviceId(bleAddress.hashCode())
+                            .setMedium(NearbyDevice.Medium.BLE)
                             .setRssi(scanResult.getRssi())
                             .setTxPower(scanResult.getTxPower())
-                            .setBluetoothAddress(scanResult.getDevice().getAddress());
+                            .setBluetoothAddress(bleAddress);
 
                     ScanRecord record = scanResult.getScanRecord();
                     if (record != null) {
@@ -72,7 +100,8 @@
                         if (serviceDataMap != null) {
                             byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
                             if (presenceData != null) {
-                                builder.setData(serviceDataMap.get(PRESENCE_UUID));
+                                setPresenceDevice(presenceData, builder, deviceName,
+                                        scanResult.getRssi());
                             }
                         }
                     }
@@ -81,7 +110,8 @@
 
                 @Override
                 public void onScanFailed(int errorCode) {
-                    Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+                    Log.w(TAG, "BLE 5.0 Scan failed with error code " + errorCode);
+                    mExecutor.execute(() -> mListener.onError(ERROR_UNKNOWN));
                 }
             };
 
@@ -90,6 +120,29 @@
         mInjector = injector;
     }
 
+    private static PresenceDevice getPresenceDevice(ExtendedAdvertisement advertisement,
+            String deviceName, int rssi) {
+        // TODO(238458326): After implementing encryption, use real data.
+        byte[] secretIdBytes = new byte[0];
+        PresenceDevice.Builder builder =
+                new PresenceDevice.Builder(
+                        String.valueOf(advertisement.hashCode()),
+                        advertisement.getSalt(),
+                        secretIdBytes,
+                        advertisement.getIdentity())
+                        .addMedium(NearbyDevice.Medium.BLE)
+                        .setName(deviceName)
+                        .setRssi(rssi);
+        for (int i : advertisement.getActions()) {
+            builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
+                    new byte[]{(byte) i}));
+        }
+        for (DataElement dataElement : advertisement.getDataElements()) {
+            builder.addExtendedProperty(dataElement);
+        }
+        return builder.build();
+    }
+
     private static List<ScanFilter> getScanFilters() {
         List<ScanFilter> scanFilterList = new ArrayList<>();
         scanFilterList.add(
@@ -120,8 +173,9 @@
     @Override
     protected void onStart() {
         if (isBleAvailable()) {
-            Log.d(TAG, "BleDiscoveryProvider started.");
-            startScan(getScanFilters(), getScanSettings(), mScanCallback);
+            Log.d(TAG, "BleDiscoveryProvider started");
+            startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
+            startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
             return;
         }
         Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -138,6 +192,12 @@
         }
         Log.v(TAG, "Ble scan stopped.");
         bluetoothLeScanner.stopScan(mScanCallback);
+        bluetoothLeScanner.stopScan(mScanCallbackLegacy);
+        synchronized (mLock) {
+            if (mScanFilters != null) {
+                mScanFilters = null;
+            }
+        }
     }
 
     @Override
@@ -146,6 +206,20 @@
         onStart();
     }
 
+    @Override
+    protected void onSetScanFilters(List<android.nearby.ScanFilter> filters) {
+        synchronized (mLock) {
+            mScanFilters = filters == null ? null : List.copyOf(filters);
+        }
+    }
+
+    @VisibleForTesting
+    protected List<android.nearby.ScanFilter> getFiltersLocked() {
+        synchronized (mLock) {
+            return mScanFilters == null ? null : List.copyOf(mScanFilters);
+        }
+    }
+
     private void startScan(
             List<ScanFilter> scanFilters, ScanSettings scanSettings,
             android.bluetooth.le.ScanCallback scanCallback) {
@@ -169,7 +243,7 @@
         }
     }
 
-    private ScanSettings getScanSettings() {
+    private ScanSettings getScanSettings(boolean legacy) {
         int bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
         switch (mController.getProviderScanMode()) {
             case ScanRequest.SCAN_MODE_LOW_LATENCY:
@@ -185,11 +259,45 @@
                 bleScanMode = ScanSettings.SCAN_MODE_OPPORTUNISTIC;
                 break;
         }
-        return new ScanSettings.Builder().setScanMode(bleScanMode).build();
+        return new ScanSettings.Builder().setScanMode(bleScanMode).setLegacy(legacy).build();
     }
 
     @VisibleForTesting
     ScanCallback getScanCallback() {
         return mScanCallback;
     }
+
+    private void setPresenceDevice(byte[] data, NearbyDeviceParcelable.Builder builder,
+            String deviceName, int rssi) {
+        synchronized (mLock) {
+            if (mScanFilters == null) {
+                return;
+            }
+            for (android.nearby.ScanFilter scanFilter : mScanFilters) {
+                if (scanFilter instanceof PresenceScanFilter) {
+                    // Iterate all possible authenticity key and identity combinations to decrypt
+                    // advertisement
+                    PresenceScanFilter presenceFilter = (PresenceScanFilter) scanFilter;
+                    for (PublicCredential credential : presenceFilter.getCredentials()) {
+                        ExtendedAdvertisement advertisement =
+                                ExtendedAdvertisement.fromBytes(data, credential);
+                        if (advertisement == null) {
+                            continue;
+                        }
+                        if (CryptorImpIdentityV1.getInstance().verify(
+                                advertisement.getIdentity(),
+                                credential.getEncryptedMetadataKeyTag())) {
+                            builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+                                    rssi));
+                            builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+                            if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+                                builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
+                            }
+                            return;
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
index 5077ffe..020c7b2 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -19,15 +19,18 @@
 import static com.android.server.nearby.NearbyService.TAG;
 
 import android.annotation.Nullable;
+import android.content.Context;
 import android.hardware.location.ContextHubClient;
 import android.hardware.location.ContextHubClientCallback;
 import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
 import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
 import android.util.Log;
 
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 
 import com.google.common.base.Preconditions;
@@ -36,7 +39,9 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Responsible for setting up communication with the appropriate contexthub on the device and
@@ -44,6 +49,8 @@
  */
 public class ChreCommunication extends ContextHubClientCallback {
 
+    public static final int INVALID_NANO_APP_VERSION = -1;
+
     /** Callback that receives messages forwarded from the context hub. */
     public interface ContextHubCommsCallback {
         /** Indicates whether {@link ChreCommunication} was started successfully. */
@@ -63,19 +70,30 @@
     }
 
     private final Injector mInjector;
+    private final Context mContext;
     private final Executor mExecutor;
 
     private boolean mStarted = false;
+    // null when CHRE availability result has not been returned
+    @Nullable private Boolean mChreSupport = null;
+    private long mNanoAppVersion = INVALID_NANO_APP_VERSION;
     @Nullable private ContextHubCommsCallback mCallback;
     @Nullable private ContextHubClient mContextHubClient;
+    private CountDownLatch mCountDownLatch;
 
-    public ChreCommunication(Injector injector, Executor executor) {
+    public ChreCommunication(Injector injector, Context context, Executor executor) {
         mInjector = injector;
+        mContext = context;
         mExecutor = executor;
     }
 
-    public boolean available() {
-        return mContextHubClient != null;
+    /**
+     * @return {@code true} if NanoApp is available and {@code null} when CHRE availability result
+     * has not been returned
+     */
+    @Nullable
+    public Boolean available() {
+        return mChreSupport;
     }
 
     /**
@@ -86,12 +104,12 @@
      *     contexthub.
      */
     public synchronized void start(ContextHubCommsCallback callback, Set<Long> nanoAppIds) {
-        ContextHubManagerAdapter manager = mInjector.getContextHubManagerAdapter();
+        ContextHubManager manager = mInjector.getContextHubManager();
         if (manager == null) {
             Log.e(TAG, "ContexHub not available in this device");
             return;
         } else {
-            Log.i(TAG, "Start ChreCommunication");
+            Log.i(TAG, "[ChreCommunication] Start ChreCommunication");
         }
         Preconditions.checkNotNull(callback);
         Preconditions.checkArgument(!nanoAppIds.isEmpty());
@@ -134,6 +152,7 @@
         if (mContextHubClient != null) {
             mContextHubClient.close();
             mContextHubClient = null;
+            mChreSupport = null;
         }
     }
 
@@ -156,6 +175,25 @@
         return true;
     }
 
+    /**
+     * Checks the Nano App version
+     */
+    public long queryNanoAppVersion() {
+        if (mCountDownLatch == null || mCountDownLatch.getCount() == 0) {
+            // already gets result from CHRE
+            return mNanoAppVersion;
+        }
+        try {
+            boolean success = mCountDownLatch.await(1, TimeUnit.SECONDS);
+            if (!success) {
+                Log.w(TAG, "Failed to get ContextHubTransaction result before the timeout.");
+            }
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return mNanoAppVersion;
+    }
+
     @Override
     public synchronized void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
         mCallback.onMessageFromNanoApp(message);
@@ -172,7 +210,8 @@
         mCallback.onNanoAppRestart(nanoAppId);
     }
 
-    private static String contextHubTransactionResultToString(int result) {
+    @VisibleForTesting
+    static String contextHubTransactionResultToString(int result) {
         switch (result) {
             case ContextHubTransaction.RESULT_SUCCESS:
                 return "RESULT_SUCCESS";
@@ -207,13 +246,13 @@
         private final ContextHubInfo mQueriedContextHub;
         private final List<ContextHubInfo> mContextHubs;
         private final Set<Long> mNanoAppIds;
-        private final ContextHubManagerAdapter mManager;
+        private final ContextHubManager mManager;
 
         OnQueryCompleteListener(
                 ContextHubInfo queriedContextHub,
                 List<ContextHubInfo> contextHubs,
                 Set<Long> nanoAppIds,
-                ContextHubManagerAdapter manager) {
+                ContextHubManager manager) {
             this.mQueriedContextHub = queriedContextHub;
             this.mContextHubs = contextHubs;
             this.mNanoAppIds = nanoAppIds;
@@ -231,21 +270,32 @@
                 return;
             }
 
+            mCountDownLatch = new CountDownLatch(1);
             if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
                 for (NanoAppState state : response.getContents()) {
+                    long version = state.getNanoAppVersion();
+                    NearbyConfiguration configuration = new NearbyConfiguration();
+                    long minVersion = configuration.getNanoAppMinVersion();
+                    if (version < minVersion) {
+                        Log.w(TAG, String.format("Current nano app version is %s, which does not  "
+                                + "meet minimum version required %s", version, minVersion));
+                        continue;
+                    }
                     if (mNanoAppIds.contains(state.getNanoAppId())) {
                         Log.i(
                                 TAG,
                                 String.format(
                                         "Found valid contexthub: %s", mQueriedContextHub.getId()));
-                        mContextHubClient =
-                                mManager.createClient(
-                                        mQueriedContextHub, ChreCommunication.this, mExecutor);
+                        mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,
+                                mExecutor, ChreCommunication.this);
+                        mChreSupport = true;
                         mCallback.started(true);
+                        mNanoAppVersion = version;
+                        mCountDownLatch.countDown();
                         return;
                     }
                 }
-                Log.e(
+                Log.i(
                         TAG,
                         String.format(
                                 "Didn't find the nanoapp on contexthub: %s",
@@ -259,10 +309,12 @@
             }
 
             mContextHubs.remove(mQueriedContextHub);
+            mCountDownLatch.countDown();
             // If this is the last context hub response left to receive, indicate that
             // there isn't a valid context available on this device.
             if (mContextHubs.isEmpty()) {
                 mCallback.started(false);
+                mChreSupport = false;
             }
         }
     }
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index f20c6d8..7ab0523 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,20 +20,33 @@
 
 import static com.android.server.nearby.NearbyService.TAG;
 
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.hardware.location.NanoAppMessage;
+import android.nearby.DataElement;
 import android.nearby.NearbyDevice;
 import android.nearby.NearbyDeviceParcelable;
+import android.nearby.OffloadCapability;
+import android.nearby.PresenceDevice;
 import android.nearby.PresenceScanFilter;
 import android.nearby.PublicCredential;
 import android.nearby.ScanFilter;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 
-import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ByteString;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 import service.proto.Blefilter;
@@ -42,52 +55,122 @@
 public class ChreDiscoveryProvider extends AbstractDiscoveryProvider {
     // Nanoapp ID reserved for Nearby Presence.
     /** @hide */
-    @VisibleForTesting public static final long NANOAPP_ID = 0x476f6f676c001031L;
+    @VisibleForTesting
+    public static final long NANOAPP_ID = 0x476f6f676c001031L;
     /** @hide */
-    @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
+    @VisibleForTesting
+    public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
     /** @hide */
-    @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+    @VisibleForTesting
+    public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+    /** @hide */
+    @VisibleForTesting
+    public static final int NANOAPP_MESSAGE_TYPE_CONFIG = 5;
 
-    private static final int PRESENCE_UUID = 0xFCF1;
+    private final ChreCommunication mChreCommunication;
+    private final ChreCallback mChreCallback;
+    private final Object mLock = new Object();
 
-    private ChreCommunication mChreCommunication;
-    private ChreCallback mChreCallback;
     private boolean mChreStarted = false;
-    private Blefilter.BleFilters mFilters = null;
-    private int mFilterId;
+    private Context mContext;
+    private NearbyConfiguration mNearbyConfiguration;
+    private final IntentFilter mIntentFilter;
+    // Null when CHRE not started and the filters are never set. Empty the list every time the scan
+    // stops.
+    @GuardedBy("mLock")
+    @Nullable
+    private List<ScanFilter> mScanFilters;
+
+    private final BroadcastReceiver mScreenBroadcastReceiver =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    Boolean screenOn = intent.getAction().equals(Intent.ACTION_SCREEN_ON)
+                            || intent.getAction().equals(Intent.ACTION_USER_PRESENT);
+                    Log.d(TAG, String.format(
+                            "[ChreDiscoveryProvider] update nanoapp screen status: %B", screenOn));
+                    sendScreenUpdate(screenOn);
+                }
+            };
 
     public ChreDiscoveryProvider(
             Context context, ChreCommunication chreCommunication, Executor executor) {
         super(context, executor);
+        mContext = context;
         mChreCommunication = chreCommunication;
         mChreCallback = new ChreCallback();
-        mFilterId = 0;
+        mIntentFilter = new IntentFilter();
+    }
+
+    /** Initialize the CHRE discovery provider. */
+    public void init() {
+        mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     @Override
     protected void onStart() {
         Log.d(TAG, "Start CHRE scan");
-        mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
-        updateFilters();
+        synchronized (mLock) {
+            updateFiltersLocked();
+        }
     }
 
     @Override
     protected void onStop() {
-        mChreStarted = false;
-        mChreCommunication.stop();
+        Log.d(TAG, "Stop CHRE scan");
+        synchronized (mLock) {
+            if (mScanFilters != null) {
+                // Cleaning the filters by assigning an empty list
+                mScanFilters = List.of();
+            }
+            updateFiltersLocked();
+        }
     }
 
     @Override
-    protected void invalidateScanMode() {
-        onStop();
-        onStart();
+    protected void onSetScanFilters(List<ScanFilter> filters) {
+        synchronized (mLock) {
+            mScanFilters = filters == null ? null : List.copyOf(filters);
+            updateFiltersLocked();
+        }
     }
 
-    public boolean available() {
+    /**
+     * @return {@code true} if CHRE is available and {@code null} when CHRE availability result
+     * has not been returned
+     */
+    @Nullable
+    public Boolean available() {
         return mChreCommunication.available();
     }
 
-    private synchronized void updateFilters() {
+    /**
+     * Query offload capability in a device.
+     */
+    public void queryOffloadCapability(IOffloadCallback callback) {
+        OffloadCapability.Builder builder = new OffloadCapability.Builder();
+        mExecutor.execute(() -> {
+            long version = mChreCommunication.queryNanoAppVersion();
+            builder.setVersion(version);
+            builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
+            try {
+                callback.onQueryComplete(builder.build());
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+        });
+    }
+
+    @VisibleForTesting
+    public List<ScanFilter> getFiltersLocked() {
+        synchronized (mLock) {
+            return mScanFilters == null ? null : List.copyOf(mScanFilters);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void updateFiltersLocked() {
         if (mScanFilters == null) {
             Log.e(TAG, "ScanFilters not set.");
             return;
@@ -95,29 +178,69 @@
         Blefilter.BleFilters.Builder filtersBuilder = Blefilter.BleFilters.newBuilder();
         for (ScanFilter scanFilter : mScanFilters) {
             PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
-            Blefilter.BleFilter filter =
-                    Blefilter.BleFilter.newBuilder()
-                            .setId(mFilterId)
-                            .setUuid(PRESENCE_UUID)
-                            .setIntent(presenceScanFilter.getPresenceActions().get(0))
-                            .build();
-            filtersBuilder.addFilter(filter);
-            mFilterId++;
+            Blefilter.BleFilter.Builder filterBuilder = Blefilter.BleFilter.newBuilder();
+            for (PublicCredential credential : presenceScanFilter.getCredentials()) {
+                filterBuilder.addCertificate(toProtoPublicCredential(credential));
+            }
+            for (DataElement dataElement : presenceScanFilter.getExtendedProperties()) {
+                if (dataElement.getKey() == DataElement.DataType.ACCOUNT_KEY_DATA) {
+                    filterBuilder.addDataElement(toProtoDataElement(dataElement));
+                } else if (mNearbyConfiguration.isTestAppSupported()
+                        && DataElement.isTestDeType(dataElement.getKey())) {
+                    filterBuilder.addDataElement(toProtoDataElement(dataElement));
+                }
+            }
+            if (!presenceScanFilter.getPresenceActions().isEmpty()) {
+                filterBuilder.setIntent(presenceScanFilter.getPresenceActions().get(0));
+            }
+            filtersBuilder.addFilter(filterBuilder.build());
         }
-        mFilters = filtersBuilder.build();
         if (mChreStarted) {
-            sendFilters(mFilters);
-            mFilters = null;
+            sendFilters(filtersBuilder.build());
         }
     }
 
+    private Blefilter.PublicateCertificate toProtoPublicCredential(PublicCredential credential) {
+        Log.d(TAG, String.format("Returns a PublicCertificate with authenticity key size %d and"
+                        + " encrypted metadata key tag size %d",
+                credential.getAuthenticityKey().length,
+                credential.getEncryptedMetadataKeyTag().length));
+        return Blefilter.PublicateCertificate.newBuilder()
+                .setAuthenticityKey(ByteString.copyFrom(credential.getAuthenticityKey()))
+                .setMetadataEncryptionKeyTag(
+                        ByteString.copyFrom(credential.getEncryptedMetadataKeyTag()))
+                .build();
+    }
+
+    private Blefilter.DataElement toProtoDataElement(DataElement dataElement) {
+        return Blefilter.DataElement.newBuilder()
+                .setKey(dataElement.getKey())
+                .setValue(ByteString.copyFrom(dataElement.getValue()))
+                .setValueLength(dataElement.getValue().length)
+                .build();
+    }
+
     private void sendFilters(Blefilter.BleFilters filters) {
         NanoAppMessage message =
                 NanoAppMessage.createMessageToNanoApp(
                         NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER, filters.toByteArray());
-        if (!mChreCommunication.sendMessageToNanoApp(message)) {
-            Log.e(TAG, "Failed to send filters to CHRE.");
+        if (mChreCommunication.sendMessageToNanoApp(message)) {
+            Log.v(TAG, "Successfully sent filters to CHRE.");
+            return;
         }
+        Log.e(TAG, "Failed to send filters to CHRE.");
+    }
+
+    private void sendScreenUpdate(Boolean screenOn) {
+        Blefilter.BleConfig config = Blefilter.BleConfig.newBuilder().setScreenOn(screenOn).build();
+        NanoAppMessage message =
+                NanoAppMessage.createMessageToNanoApp(
+                        NANOAPP_ID, NANOAPP_MESSAGE_TYPE_CONFIG, config.toByteArray());
+        if (mChreCommunication.sendMessageToNanoApp(message)) {
+            Log.v(TAG, "Successfully sent config to CHRE.");
+            return;
+        }
+        Log.e(TAG, "Failed to send config to CHRE.");
     }
 
     private class ChreCallback implements ChreCommunication.ContextHubCommsCallback {
@@ -127,11 +250,11 @@
             if (success) {
                 synchronized (ChreDiscoveryProvider.this) {
                     Log.i(TAG, "CHRE communication started");
+                    mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+                    mIntentFilter.addAction(Intent.ACTION_USER_PRESENT);
+                    mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+                    mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
                     mChreStarted = true;
-                    if (mFilters != null) {
-                        sendFilters(mFilters);
-                        mFilters = null;
-                    }
                 }
             }
         }
@@ -163,32 +286,123 @@
                     Blefilter.BleFilterResults results =
                             Blefilter.BleFilterResults.parseFrom(message.getMessageBody());
                     for (Blefilter.BleFilterResult filterResult : results.getResultList()) {
-                        Blefilter.PublicCredential credential = filterResult.getPublicCredential();
+                        // TODO(b/234653356): There are some duplicate fields set both in
+                        //  PresenceDevice and NearbyDeviceParcelable, cleanup is needed.
+                        byte[] salt = {1};
+                        byte[] secretId = {1};
+                        byte[] authenticityKey = {1};
+                        byte[] publicKey = {1};
+                        byte[] encryptedMetaData = {1};
+                        byte[] encryptedMetaDataTag = {1};
+                        if (filterResult.hasPublicCredential()) {
+                            Blefilter.PublicCredential credential =
+                                    filterResult.getPublicCredential();
+                            secretId = credential.getSecretId().toByteArray();
+                            authenticityKey = credential.getAuthenticityKey().toByteArray();
+                            publicKey = credential.getPublicKey().toByteArray();
+                            encryptedMetaData = credential.getEncryptedMetadata().toByteArray();
+                            encryptedMetaDataTag =
+                                    credential.getEncryptedMetadataTag().toByteArray();
+                        }
+                        PresenceDevice.Builder presenceDeviceBuilder =
+                                new PresenceDevice.Builder(
+                                        String.valueOf(filterResult.hashCode()),
+                                        salt,
+                                        secretId,
+                                        encryptedMetaData)
+                                        .setRssi(filterResult.getRssi())
+                                        .addMedium(NearbyDevice.Medium.BLE);
+                        // Data Elements reported from nanoapp added to Data Elements.
+                        // i.e. Fast Pair account keys, connection status and battery
+                        for (Blefilter.DataElement element : filterResult.getDataElementList()) {
+                            addDataElementsToPresenceDevice(element, presenceDeviceBuilder);
+                        }
+                        // BlE address appended to Data Element.
+                        if (filterResult.hasBluetoothAddress()) {
+                            presenceDeviceBuilder.addExtendedProperty(
+                                    new DataElement(
+                                            DataElement.DataType.BLE_ADDRESS,
+                                            filterResult.getBluetoothAddress().toByteArray()));
+                        }
+                        // BlE TX Power appended to Data Element.
+                        if (filterResult.hasTxPower()) {
+                            presenceDeviceBuilder.addExtendedProperty(
+                                    new DataElement(
+                                            DataElement.DataType.TX_POWER,
+                                            new byte[]{(byte) filterResult.getTxPower()}));
+                        }
+                        // BLE Service data appended to Data Elements.
+                        if (filterResult.hasBleServiceData()) {
+                            // Retrieves the length of the service data from the first byte,
+                            // and then skips the first byte and returns data[1 .. dataLength)
+                            // as the DataElement value.
+                            int dataLength = Byte.toUnsignedInt(
+                                    filterResult.getBleServiceData().byteAt(0));
+                            presenceDeviceBuilder.addExtendedProperty(
+                                    new DataElement(
+                                            DataElement.DataType.BLE_SERVICE_DATA,
+                                            filterResult.getBleServiceData()
+                                                    .substring(1, 1 + dataLength).toByteArray()));
+                        }
+                        // Add action
+                        if (filterResult.hasIntent()) {
+                            presenceDeviceBuilder.addExtendedProperty(
+                                    new DataElement(
+                                            DataElement.DataType.ACTION,
+                                            new byte[]{(byte) filterResult.getIntent()}));
+                        }
+
                         PublicCredential publicCredential =
                                 new PublicCredential.Builder(
-                                                credential.getSecretId().toByteArray(),
-                                                credential.getAuthenticityKey().toByteArray(),
-                                                credential.getPublicKey().toByteArray(),
-                                                credential.getEncryptedMetadata().toByteArray(),
-                                                credential.getEncryptedMetadataTag().toByteArray())
+                                        secretId,
+                                        authenticityKey,
+                                        publicKey,
+                                        encryptedMetaData,
+                                        encryptedMetaDataTag)
                                         .build();
+
                         NearbyDeviceParcelable device =
                                 new NearbyDeviceParcelable.Builder()
+                                        .setDeviceId(Arrays.hashCode(secretId))
                                         .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
                                         .setMedium(NearbyDevice.Medium.BLE)
                                         .setTxPower(filterResult.getTxPower())
                                         .setRssi(filterResult.getRssi())
                                         .setAction(filterResult.getIntent())
                                         .setPublicCredential(publicCredential)
+                                        .setPresenceDevice(presenceDeviceBuilder.build())
+                                        .setEncryptionKeyTag(encryptedMetaDataTag)
                                         .build();
                         mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(device));
                     }
-                } catch (InvalidProtocolBufferException e) {
-                    Log.e(
-                            TAG,
-                            String.format("Failed to decode the filter result %s", e.toString()));
+                } catch (Exception e) {
+                    Log.e(TAG, String.format("Failed to decode the filter result %s", e));
                 }
             }
         }
+
+        private void addDataElementsToPresenceDevice(Blefilter.DataElement element,
+                PresenceDevice.Builder presenceDeviceBuilder) {
+            int endIndex = element.hasValueLength() ? element.getValueLength() :
+                    element.getValue().size();
+            int key = element.getKey();
+            switch (key) {
+                case DataElement.DataType.ACCOUNT_KEY_DATA:
+                case DataElement.DataType.CONNECTION_STATUS:
+                case DataElement.DataType.BATTERY:
+                    presenceDeviceBuilder.addExtendedProperty(
+                            new DataElement(key,
+                                    element.getValue().substring(0, endIndex).toByteArray()));
+                    break;
+                default:
+                    if (mNearbyConfiguration.isTestAppSupported()
+                            && DataElement.isTestDeType(key)) {
+                        presenceDeviceBuilder.addExtendedProperty(
+                                new DataElement(key,
+                                        element.getValue().substring(0, endIndex).toByteArray()));
+                    }
+                    break;
+            }
+        }
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
index fa1a874..71ffda5 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderController.java
@@ -23,7 +23,7 @@
 import java.util.List;
 
 /** Interface for controlling discovery providers. */
-interface DiscoveryProviderController {
+public interface DiscoveryProviderController {
 
     /**
      * Sets the listener which can expect to receive all state updates from after this point. May be
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
deleted file mode 100644
index bdeab51..0000000
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.nearby.provider;
-
-import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.annotation.Nullable;
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.nearby.IScanListener;
-import android.nearby.NearbyDeviceParcelable;
-import android.nearby.PresenceScanFilter;
-import android.nearby.ScanFilter;
-import android.nearby.ScanRequest;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.metrics.NearbyMetrics;
-import com.android.server.nearby.presence.PresenceDiscoveryResult;
-import com.android.server.nearby.util.identity.CallerIdentity;
-import com.android.server.nearby.util.permissions.DiscoveryPermissions;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.stream.Collectors;
-
-/** Manages all aspects of discovery providers. */
-public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
-
-    protected final Object mLock = new Object();
-    private final Context mContext;
-    private final BleDiscoveryProvider mBleDiscoveryProvider;
-    @Nullable private final ChreDiscoveryProvider mChreDiscoveryProvider;
-    private @ScanRequest.ScanMode int mScanMode;
-    private final Injector mInjector;
-
-    @GuardedBy("mLock")
-    private Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
-
-    @Override
-    public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
-        synchronized (mLock) {
-            AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
-            for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
-                ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
-                if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
-                    continue;
-                }
-                CallerIdentity callerIdentity = record.getCallerIdentity();
-                if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
-                        appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
-                            + "- not forwarding results");
-                    try {
-                        record.getScanListener().onError();
-                    } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
-                    }
-                    return;
-                }
-
-                if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
-                    List<ScanFilter> presenceFilters =
-                            record.getScanRequest().getScanFilters().stream()
-                                    .filter(
-                                            scanFilter ->
-                                                    scanFilter.getType()
-                                                            == SCAN_TYPE_NEARBY_PRESENCE)
-                                    .collect(Collectors.toList());
-                    Log.i(
-                            TAG,
-                            String.format("match with filters size: %d", presenceFilters.size()));
-                    if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
-                        continue;
-                    }
-                }
-                try {
-                    record.getScanListener()
-                            .onDiscovered(
-                                    PrivacyFilter.filter(
-                                            record.getScanRequest().getScanType(), nearbyDevice));
-                    NearbyMetrics.logScanDeviceDiscovered(
-                            record.hashCode(), record.getScanRequest(), nearbyDevice);
-                } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
-                }
-            }
-        }
-    }
-
-    public DiscoveryProviderManager(Context context, Injector injector) {
-        mContext = context;
-        mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
-        Executor executor = Executors.newSingleThreadExecutor();
-        mChreDiscoveryProvider =
-                new ChreDiscoveryProvider(
-                        mContext, new ChreCommunication(injector, executor), executor);
-        mScanTypeScanListenerRecordMap = new HashMap<>();
-        mInjector = injector;
-    }
-
-    /**
-     * Registers the listener in the manager and starts scan according to the requested scan mode.
-     */
-    public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener,
-            CallerIdentity callerIdentity) {
-        synchronized (mLock) {
-            IBinder listenerBinder = listener.asBinder();
-            if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
-                ScanRequest savedScanRequest =
-                        mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
-                if (scanRequest.equals(savedScanRequest)) {
-                    Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
-                    return true;
-                }
-            }
-            ScanListenerRecord scanListenerRecord =
-                    new ScanListenerRecord(scanRequest, listener, callerIdentity);
-            mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
-
-            if (!startProviders(scanRequest)) {
-                return false;
-            }
-
-            NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
-            if (mScanMode < scanRequest.getScanMode()) {
-                mScanMode = scanRequest.getScanMode();
-                invalidateProviderScanMode();
-            }
-            return true;
-        }
-    }
-
-    /**
-     * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
-     */
-    public void unregisterScanListener(IScanListener listener) {
-        IBinder listenerBinder = listener.asBinder();
-        synchronized (mLock) {
-            if (!mScanTypeScanListenerRecordMap.containsKey(listenerBinder)) {
-                Log.w(
-                        TAG,
-                        "Cannot unregister the scanRequest because the request is never "
-                                + "registered.");
-                return;
-            }
-
-            ScanListenerRecord removedRecord =
-                    mScanTypeScanListenerRecordMap.remove(listenerBinder);
-            Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
-            NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
-            if (mScanTypeScanListenerRecordMap.isEmpty()) {
-                Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
-                        + "scan listener registered.");
-                stopProviders();
-                return;
-            }
-
-            // TODO(b/221082271): updates the scan with reduced filters.
-
-            // Removes current highest scan mode requested and sets the next highest scan mode.
-            if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
-                Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
-                        + "because the highest scan mode listener was unregistered.");
-                @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
-                // find the next highest scan mode;
-                for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
-                    @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
-                    if (scanMode > highestScanModeRequested) {
-                        highestScanModeRequested = scanMode;
-                    }
-                }
-                if (mScanMode != highestScanModeRequested) {
-                    mScanMode = highestScanModeRequested;
-                    invalidateProviderScanMode();
-                }
-            }
-        }
-    }
-
-    // Returns false when fail to start all the providers. Returns true if any one of the provider
-    // starts successfully.
-    private boolean startProviders(ScanRequest scanRequest) {
-        if (scanRequest.isBleEnabled()) {
-            if (mChreDiscoveryProvider.available()
-                    && scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
-                startChreProvider();
-            } else {
-                startBleProvider(scanRequest);
-            }
-            return true;
-        }
-        return false;
-    }
-
-    private void startBleProvider(ScanRequest scanRequest) {
-        if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
-            mBleDiscoveryProvider.getController().start();
-            mBleDiscoveryProvider.getController().setListener(this);
-            mBleDiscoveryProvider.getController().setProviderScanMode(scanRequest.getScanMode());
-        }
-    }
-
-    private void startChreProvider() {
-        Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
-        synchronized (mLock) {
-            mChreDiscoveryProvider.getController().setListener(this);
-            List<ScanFilter> scanFilters = new ArrayList();
-            for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
-                ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
-                List<ScanFilter> presenceFilters =
-                        record.getScanRequest().getScanFilters().stream()
-                                .filter(
-                                        scanFilter ->
-                                                scanFilter.getType() == SCAN_TYPE_NEARBY_PRESENCE)
-                                .collect(Collectors.toList());
-                scanFilters.addAll(presenceFilters);
-            }
-            mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
-            mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
-            mChreDiscoveryProvider.getController().start();
-        }
-    }
-
-    private void stopProviders() {
-        stopBleProvider();
-        stopChreProvider();
-    }
-
-    private void stopBleProvider() {
-        mBleDiscoveryProvider.getController().stop();
-    }
-
-    private void stopChreProvider() {
-        mChreDiscoveryProvider.getController().stop();
-    }
-
-    private void invalidateProviderScanMode() {
-        if (mBleDiscoveryProvider.getController().isStarted()) {
-            mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
-        } else {
-            Log.d(
-                    TAG,
-                    "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
-                            + "started.");
-        }
-    }
-
-    private static boolean presenceFilterMatches(
-            NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
-        if (scanFilters.isEmpty()) {
-            return true;
-        }
-        PresenceDiscoveryResult discoveryResult = PresenceDiscoveryResult.fromDevice(device);
-        for (ScanFilter scanFilter : scanFilters) {
-            PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
-            if (discoveryResult.matches(presenceScanFilter)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private static class ScanListenerRecord {
-
-        private final ScanRequest mScanRequest;
-
-        private final IScanListener mScanListener;
-
-        private final CallerIdentity mCallerIdentity;
-
-        ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
-                CallerIdentity callerIdentity) {
-            mScanListener = iScanListener;
-            mScanRequest = scanRequest;
-            mCallerIdentity = callerIdentity;
-        }
-
-        IScanListener getScanListener() {
-            return mScanListener;
-        }
-
-        ScanRequest getScanRequest() {
-            return mScanRequest;
-        }
-
-        CallerIdentity getCallerIdentity() {
-            return mCallerIdentity;
-        }
-
-        @Override
-        public boolean equals(Object other) {
-            if (other instanceof ScanListenerRecord) {
-                ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
-                return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
-                        && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
-            }
-            return false;
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mScanListener, mScanRequest);
-        }
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index 599843c..35251d8 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -45,4 +45,11 @@
         }
         return result;
     }
+
+    /**
+     * @return true when the array is null or length is 0
+     */
+    public static boolean isEmpty(byte[] bytes) {
+        return bytes == null || bytes.length == 0;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
new file mode 100644
index 0000000..3c5132d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Class for encryption/decryption functionality. */
+public abstract class Cryptor {
+
+    /** AES only supports key sizes of 16, 24 or 32 bytes. */
+    static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
+
+    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+    /**
+     * Encrypt the provided data blob.
+     *
+     * @param data data blob to be encrypted.
+     * @param salt used for IV
+     * @param secretKeyBytes secrete key accessed from credentials
+     * @return encrypted data, {@code null} if failed to encrypt.
+     */
+    @Nullable
+    public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+        return data;
+    }
+
+    /**
+     * Decrypt the original data blob from the provided byte array.
+     *
+     * @param encryptedData data blob to be decrypted.
+     * @param salt used for IV
+     * @param secretKeyBytes secrete key accessed from credentials
+     * @return decrypted data, {@code null} if failed to decrypt.
+     */
+    @Nullable
+    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+        return encryptedData;
+    }
+
+    /**
+     * Generates a digital signature for the data.
+     *
+     * @return signature {@code null} if failed to sign
+     */
+    @Nullable
+    public byte[] sign(byte[] data, byte[] key) {
+        return new byte[0];
+    }
+
+    /**
+     * Verifies the signature generated by data and key, with the original signed data
+     */
+    public boolean verify(byte[] data, byte[] key, byte[] signature) {
+        return true;
+    }
+
+    /**
+     * @return length of the signature generated
+     */
+    public int getSignatureLength() {
+        return 0;
+    }
+
+    /**
+     * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
+     * given size.
+     */
+    // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+    @Nullable
+    static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
+        Mac mac;
+        try {
+            mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+        } catch (NoSuchAlgorithmException e) {
+            Log.w(TAG, "HMAC_SHA256_ALGORITHM is not supported.", e);
+            return null;
+        }
+
+        if (size > 255 * mac.getMacLength()) {
+            Log.w(TAG, "Size too large.");
+            return null;
+        }
+
+        if (salt.length == 0) {
+            Log.w(TAG, "Salt cannot be empty.");
+            return null;
+        }
+
+        try {
+            mac.init(new SecretKeySpec(salt, HMAC_SHA256_ALGORITHM));
+        } catch (InvalidKeyException e) {
+            Log.w(TAG, "Invalid key.", e);
+            return null;
+        }
+
+        byte[] prk = mac.doFinal(ikm);
+        byte[] result = new byte[size];
+        try {
+            mac.init(new SecretKeySpec(prk, HMAC_SHA256_ALGORITHM));
+        } catch (InvalidKeyException e) {
+            Log.w(TAG, "Invalid key.", e);
+            return null;
+        }
+
+        byte[] digest = new byte[0];
+        int ctr = 1;
+        int pos = 0;
+        while (true) {
+            mac.update(digest);
+            mac.update((byte) ctr);
+            digest = mac.doFinal();
+            if (pos + digest.length < size) {
+                System.arraycopy(digest, 0, result, pos, digest.length);
+                pos += digest.length;
+                ctr++;
+            } else {
+                System.arraycopy(digest, 0, result, pos, size - pos);
+                break;
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
new file mode 100644
index 0000000..1c0ec9e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A Cryptor that returns the original data without actual encryption
+ */
+public class CryptorImpFake extends Cryptor {
+    // Lazily instantiated when {@link #getInstance()} is called.
+    @Nullable
+    private static CryptorImpFake sCryptor;
+
+    /** Returns an instance of CryptorImpFake. */
+    public static CryptorImpFake getInstance() {
+        if (sCryptor == null) {
+            sCryptor = new CryptorImpFake();
+        }
+        return sCryptor;
+    }
+
+    private CryptorImpFake() {
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
new file mode 100644
index 0000000..b0e19b4
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
+ * encryption and decryption.
+ */
+public class CryptorImpIdentityV1 extends Cryptor {
+
+    // 3 16 byte arrays known by both the encryptor and decryptor.
+    private static final byte[] EK_IV =
+            new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
+    private static final byte[] ESALT_IV =
+            new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
+    private static final byte[] KTAG_IV =
+            {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
+
+    /** Length of encryption key required by AES/GCM encryption. */
+    private static final int ENCRYPTION_KEY_SIZE = 32;
+
+    /** Length of salt required by AES/GCM encryption. */
+    private static final int AES_CTR_IV_SIZE = 16;
+
+    /** Length HMAC tag */
+    public static final int HMAC_TAG_SIZE = 8;
+
+    /**
+     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+     */
+    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+    @VisibleForTesting
+    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+    // Lazily instantiated when {@link #getInstance()} is called.
+    @Nullable private static CryptorImpIdentityV1 sCryptor;
+
+    /** Returns an instance of CryptorImpIdentityV1. */
+    public static CryptorImpIdentityV1 getInstance() {
+        if (sCryptor == null) {
+            sCryptor = new CryptorImpIdentityV1();
+        }
+        return sCryptor;
+    }
+
+    @Nullable
+    @Override
+    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+            Log.w(TAG, "Illegal authenticity key size");
+            return null;
+        }
+
+        // Generates a 32 bytes encryption key from authenticity_key
+        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+        if (encryptionKey == null) {
+            Log.e(TAG, "Failed to generate encryption key.");
+            return null;
+        }
+
+        // Encrypts the data using the encryption key
+        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+        if (esalt == null) {
+            Log.e(TAG, "Failed to generate salt.");
+            return null;
+        }
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+        try {
+            return cipher.doFinal(data);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+            Log.w(TAG, "Illegal authenticity key size");
+            return null;
+        }
+
+        // Generates a 32 bytes encryption key from authenticity_key
+        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+        if (encryptionKey == null) {
+            Log.e(TAG, "Failed to generate encryption key.");
+            return null;
+        }
+
+        // Decrypts the data using the encryption key
+        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to get cipher instance.", e);
+            return null;
+        }
+        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+        if (esalt == null) {
+            return null;
+        }
+        try {
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+
+        try {
+            return cipher.doFinal(encryptedData);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+            return null;
+        }
+    }
+
+    /**
+     * Generates a digital signature for the data.
+     *
+     * @return signature {@code null} if failed to sign
+     */
+    @Nullable
+    @Override
+    public byte[] sign(byte[] data, byte[] salt) {
+        if (data == null) {
+            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+            return null;
+        }
+
+        // Generates a 8 bytes HMAC tag
+        return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
+    }
+
+    /**
+     * Generates a digital signature for the data.
+     * Uses KTAG_IV as salt value.
+     */
+    @Nullable
+    public byte[] sign(byte[] data) {
+        // Generates a 8 bytes HMAC tag
+        return sign(data, KTAG_IV);
+    }
+
+    @Override
+    public boolean verify(byte[] data, byte[] key, byte[] signature) {
+        return Arrays.equals(sign(data, key), signature);
+    }
+
+    /**
+     * Verifies the signature generated by data and key, with the original signed data. Uses
+     * KTAG_IV as salt value.
+     */
+    public boolean verify(byte[] data, byte[] signature) {
+        return verify(data, KTAG_IV, signature);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
new file mode 100644
index 0000000..15073fb
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
+ */
+public class CryptorImpV1 extends Cryptor {
+
+    /**
+     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+     */
+    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+    @VisibleForTesting
+    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+    /** Length of encryption key required by AES/GCM encryption. */
+    private static final int ENCRYPTION_KEY_SIZE = 32;
+
+    /** Length of salt required by AES/GCM encryption. */
+    private static final int AES_CTR_IV_SIZE = 16;
+
+    /** Length HMAC tag */
+    public static final int HMAC_TAG_SIZE = 16;
+
+    // 3 16 byte arrays known by both the encryptor and decryptor.
+    private static final byte[] AK_IV =
+            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+    private static final byte[] ASALT_IV =
+            new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
+    private static final byte[] HK_IV =
+            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+
+    // Lazily instantiated when {@link #getInstance()} is called.
+    @Nullable private static CryptorImpV1 sCryptor;
+
+    /** Returns an instance of CryptorImpV1. */
+    public static CryptorImpV1 getInstance() {
+        if (sCryptor == null) {
+            sCryptor = new CryptorImpV1();
+        }
+        return sCryptor;
+    }
+
+    private CryptorImpV1() {
+    }
+
+    @Nullable
+    @Override
+    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+            Log.w(TAG, "Illegal authenticity key size");
+            return null;
+        }
+
+        // Generates a 32 bytes encryption key from authenticity_key
+        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+        if (encryptionKey == null) {
+            Log.e(TAG, "Failed to generate encryption key.");
+            return null;
+        }
+
+        // Encrypts the data using the encryption key
+        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+        if (asalt == null) {
+            Log.e(TAG, "Failed to generate salt.");
+            return null;
+        }
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+        try {
+            return cipher.doFinal(data);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+            Log.w(TAG, "Illegal authenticity key size");
+            return null;
+        }
+
+        // Generates a 32 bytes encryption key from authenticity_key
+        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+        if (encryptionKey == null) {
+            Log.e(TAG, "Failed to generate encryption key.");
+            return null;
+        }
+
+        // Decrypts the data using the encryption key
+        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to get cipher instance.", e);
+            return null;
+        }
+        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+        if (asalt == null) {
+            return null;
+        }
+        try {
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+
+        try {
+            return cipher.doFinal(encryptedData);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+            return null;
+        }
+    }
+
+    @Override
+    @Nullable
+    public byte[] sign(byte[] data, byte[] key) {
+        return generateHmacTag(data, key);
+    }
+
+    @Override
+    public int getSignatureLength() {
+        return HMAC_TAG_SIZE;
+    }
+
+    @Override
+    public boolean verify(byte[] data, byte[] key, byte[] signature) {
+        return Arrays.equals(sign(data, key), signature);
+    }
+
+    /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+     * is equal to HMAC tag in advertisement to see data integrity. */
+    @Nullable
+    @VisibleForTesting
+    byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
+        if (data == null || authenticityKey == null) {
+            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+            return null;
+        }
+
+        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+            Log.e(TAG, "Illegal authenticity key size");
+            return null;
+        }
+
+        // Generates a 32 bytes HMAC key from authenticity_key
+        byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
+        if (hmacKey == null) {
+            Log.e(TAG, "Failed to generate HMAC key.");
+            return null;
+        }
+
+        // Generates a 16 bytes HMAC tag from authenticity_key
+        return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
+    }
+}
diff --git a/nearby/service/lint-baseline.xml b/nearby/service/lint-baseline.xml
new file mode 100644
index 0000000..a4761ab
--- /dev/null
+++ b/nearby/service/lint-baseline.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.0.0-dev" type="baseline" dependencies="true" variant="all" version="8.0.0-dev">
+
+    <issue
+        id="NewApi"
+        message="Call requires API level 31 (current min is 30): `android.hardware.location.ContextHubManager#createClient`"
+        errorLine1="                        mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,"
+        errorLine2="                                                     ~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Connectivity/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java"
+            line="263"
+            column="54"/>
+    </issue>
+
+</issues>
\ No newline at end of file
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index 9f75d34..e1bf455 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -47,6 +47,7 @@
   optional bytes metadata_encryption_key_tag = 2;
 }
 
+// Public credential returned in BleFilterResult.
 message PublicCredential {
   optional bytes secret_id = 1;
   optional bytes authenticity_key = 2;
@@ -55,6 +56,23 @@
   optional bytes encrypted_metadata_tag = 5;
 }
 
+message DataElement {
+  enum ElementType {
+    DE_NONE = 0;
+    DE_FAST_PAIR_ACCOUNT_KEY = 9;
+    DE_CONNECTION_STATUS = 10;
+    DE_BATTERY_STATUS = 11;
+    // Reserves 128 Test DEs.
+    DE_TEST_BEGIN = 2147483520;  // INT_MAX - 127
+    DE_TEST_END = 2147483647;    // INT_MAX
+  }
+
+  optional int32 key = 1;
+  optional bytes value = 2;
+  optional uint32 value_length = 3;
+}
+
+// A single filter used to filter BLE events.
 message BleFilter {
   optional uint32 id = 1;  // Required, unique id of this filter.
   // Maximum delay to notify the client after an event occurs.
@@ -71,7 +89,9 @@
   // the period of latency defined above.
   optional float distance_m = 7;
   // Used to verify the list of trusted devices.
-  repeated PublicateCertificate certficate = 8;
+  repeated PublicateCertificate certificate = 8;
+  // Data Elements for extended properties.
+  repeated DataElement data_element = 9;
 }
 
 message BleFilters {
@@ -80,14 +100,33 @@
 
 // FilterResult is returned to host when a BLE event matches a Filter.
 message BleFilterResult {
+  enum ResultType {
+    RESULT_NONE = 0;
+    RESULT_PRESENCE = 1;
+    RESULT_FAST_PAIR = 2;
+  }
+
   optional uint32 id = 1;  // id of the matched Filter.
-  optional uint32 tx_power = 2;
-  optional uint32 rssi = 3;
+  optional int32 tx_power = 2;
+  optional int32 rssi = 3;
   optional uint32 intent = 4;
   optional bytes bluetooth_address = 5;
   optional PublicCredential public_credential = 6;
+  repeated DataElement data_element = 7;
+  optional bytes ble_service_data = 8;
+  optional ResultType result_type = 9;
 }
 
 message BleFilterResults {
   repeated BleFilterResult result = 1;
 }
+
+message BleConfig {
+  // True to start BLE scan. Otherwise, stop BLE scan.
+  optional bool start_scan = 1;
+  // True when screen is turned on. Otherwise, set to false when screen is
+  // turned off.
+  optional bool screen_on = 2;
+  // Fast Pair cache expires after this time period.
+  optional uint64 fast_pair_cache_expire_time_sec = 3;
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 0410cd5..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/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index aacb6d8..a2da967 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -42,7 +42,6 @@
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         CredentialElement element = new CredentialElement(KEY, VALUE);
-
         assertThat(element.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
     }
@@ -58,9 +57,31 @@
         CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
                 parcel);
         parcel.recycle();
-
         assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        CredentialElement element = new CredentialElement(KEY, VALUE);
+        assertThat(element.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEqual() {
+        CredentialElement element1 = new CredentialElement(KEY, VALUE);
+        CredentialElement element2 = new CredentialElement(KEY, VALUE);
+        assertThat(element1.equals(element2)).isTrue();
+        assertThat(element1.hashCode()).isEqualTo(element2.hashCode());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        CredentialElement [] elements =
+                CredentialElement.CREATOR.newArray(2);
+        assertThat(elements.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
index 3654d0d..84814ae 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
@@ -16,6 +16,13 @@
 
 package android.nearby.cts;
 
+import static android.nearby.DataElement.DataType.PRIVATE_IDENTITY;
+import static android.nearby.DataElement.DataType.PROVISIONED_IDENTITY;
+import static android.nearby.DataElement.DataType.PUBLIC_IDENTITY;
+import static android.nearby.DataElement.DataType.SALT;
+import static android.nearby.DataElement.DataType.TRUSTED_IDENTITY;
+import static android.nearby.DataElement.DataType.TX_POWER;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.nearby.DataElement;
@@ -31,7 +38,6 @@
 
 import java.util.Arrays;
 
-
 @RunWith(AndroidJUnit4.class)
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class DataElementTest {
@@ -63,4 +69,59 @@
         assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        DataElement dataElement = new DataElement(KEY, VALUE);
+        assertThat(dataElement.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        DataElement[] elements =
+                DataElement.CREATOR.newArray(2);
+        assertThat(elements.length).isEqualTo(2);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEquals() {
+        DataElement dataElement = new DataElement(KEY, VALUE);
+        DataElement dataElement2 = new DataElement(KEY, VALUE);
+
+        assertThat(dataElement.equals(dataElement2)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testIsIdentity() {
+        DataElement privateIdentity = new DataElement(PRIVATE_IDENTITY, new byte[]{1, 2, 3});
+        DataElement trustedIdentity = new DataElement(TRUSTED_IDENTITY, new byte[]{1, 2, 3});
+        DataElement publicIdentity = new DataElement(PUBLIC_IDENTITY, new byte[]{1, 2, 3});
+        DataElement provisionedIdentity =
+                new DataElement(PROVISIONED_IDENTITY, new byte[]{1, 2, 3});
+
+        DataElement salt = new DataElement(SALT, new byte[]{1, 2, 3});
+        DataElement txPower = new DataElement(TX_POWER, new byte[]{1, 2, 3});
+
+        assertThat(privateIdentity.isIdentityDataType()).isTrue();
+        assertThat(trustedIdentity.isIdentityDataType()).isTrue();
+        assertThat(publicIdentity.isIdentityDataType()).isTrue();
+        assertThat(provisionedIdentity.isIdentityDataType()).isTrue();
+        assertThat(salt.isIdentityDataType()).isFalse();
+        assertThat(txPower.isIdentityDataType()).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_notEquals() {
+        DataElement dataElement = new DataElement(KEY, VALUE);
+        DataElement dataElement2 = new DataElement(KEY, new byte[]{1, 2, 1, 1});
+        DataElement dataElement3 = new DataElement(6, VALUE);
+
+        assertThat(dataElement.equals(dataElement2)).isFalse();
+        assertThat(dataElement.equals(dataElement3)).isFalse();
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
index f37800a..8ca5a94 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -16,6 +16,8 @@
 
 package android.nearby.cts;
 
+import static android.nearby.NearbyDevice.Medium.BLE;
+
 import android.annotation.TargetApi;
 import android.nearby.FastPairDevice;
 import android.nearby.NearbyDevice;
@@ -34,13 +36,18 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 @TargetApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyDeviceTest {
+    private static final String NAME = "NearbyDevice";
+    private static final String MODEL_ID = "112233";
+    private static final int TX_POWER = -10;
+    private static final int RSSI = -60;
+    private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+    private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void test_isValidMedium() {
         assertThat(NearbyDevice.isValidMedium(1)).isTrue();
         assertThat(NearbyDevice.isValidMedium(2)).isTrue();
-
         assertThat(NearbyDevice.isValidMedium(0)).isFalse();
         assertThat(NearbyDevice.isValidMedium(3)).isFalse();
     }
@@ -49,11 +56,55 @@
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void test_getMedium_fromChild() {
         FastPairDevice fastPairDevice = new FastPairDevice.Builder()
-                .addMedium(NearbyDevice.Medium.BLE)
-                .setRssi(-60)
+                .addMedium(BLE)
+                .setRssi(RSSI)
                 .build();
 
         assertThat(fastPairDevice.getMediums()).contains(1);
-        assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
+        assertThat(fastPairDevice.getRssi()).isEqualTo(RSSI);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEqual() {
+        FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+                .setModelId(MODEL_ID)
+                .setTxPower(TX_POWER)
+                .setBluetoothAddress(BLUETOOTH_ADDRESS)
+                .setData(SCAN_DATA)
+                .setRssi(RSSI)
+                .addMedium(BLE)
+                .setName(NAME)
+                .build();
+        FastPairDevice fastPairDevice2 = new FastPairDevice.Builder()
+                .setModelId(MODEL_ID)
+                .setTxPower(TX_POWER)
+                .setBluetoothAddress(BLUETOOTH_ADDRESS)
+                .setData(SCAN_DATA)
+                .setRssi(RSSI)
+                .addMedium(BLE)
+                .setName(NAME)
+                .build();
+
+        assertThat(fastPairDevice1.equals(fastPairDevice1)).isTrue();
+        assertThat(fastPairDevice1.equals(fastPairDevice2)).isTrue();
+        assertThat(fastPairDevice1.equals(null)).isFalse();
+        assertThat(fastPairDevice1.hashCode()).isEqualTo(fastPairDevice2.hashCode());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testToString() {
+        FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+                .addMedium(BLE)
+                .setRssi(RSSI)
+                .setModelId(MODEL_ID)
+                .setTxPower(TX_POWER)
+                .setBluetoothAddress(BLUETOOTH_ADDRESS)
+                .build();
+
+        assertThat(fastPairDevice1.toString())
+                .isEqualTo("FastPairDevice [medium={BLE} rssi=-60 "
+                        + "txPower=-10 modelId=112233 bluetoothAddress=00:11:22:33:FF:EE]");
     }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 7696a61..bc9691d 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -20,7 +20,7 @@
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+import static android.nearby.ScanCallback.ERROR_UNSUPPORTED;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -35,7 +35,9 @@
 import android.nearby.BroadcastRequest;
 import android.nearby.NearbyDevice;
 import android.nearby.NearbyManager;
+import android.nearby.OffloadCapability;
 import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceDevice;
 import android.nearby.PrivateCredential;
 import android.nearby.ScanCallback;
 import android.nearby.ScanRequest;
@@ -48,6 +50,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 
+import com.android.modules.utils.build.SdkLevel;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -57,6 +61,7 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 /**
  * TODO(b/215435939) This class doesn't include any logic yet. Because SELinux denies access to
@@ -66,7 +71,7 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyManagerTest {
     private static final byte[] SALT = new byte[]{1, 2};
-    private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+    private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
     private static final byte[] META_DATA_ENCRYPTION_KEY = new byte[14];
     private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
     private static final String DEVICE_NAME = "test_device";
@@ -82,6 +87,9 @@
             .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
             .setBleEnabled(true)
             .build();
+    private PresenceDevice.Builder mBuilder =
+            new PresenceDevice.Builder("deviceId", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY);
+
     private  ScanCallback mScanCallback = new ScanCallback() {
         @Override
         public void onDiscovered(@NonNull NearbyDevice device) {
@@ -94,14 +102,21 @@
         @Override
         public void onLost(@NonNull NearbyDevice device) {
         }
+
+        @Override
+        public void onError(int errorCode) {
+        }
     };
+
     private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
 
     @Before
     public void setUp() {
         mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
                 BLUETOOTH_PRIVILEGED);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING,
+        String nameSpace = SdkLevel.isAtLeastU() ? DeviceConfig.NAMESPACE_NEARBY
+                : DeviceConfig.NAMESPACE_TETHERING;
+        DeviceConfig.setProperty(nameSpace,
                 "nearby_enable_presence_broadcast_legacy",
                 "true", false);
 
@@ -137,7 +152,7 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testStartStopBroadcast() throws InterruptedException {
-        PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY,
+        PrivateCredential credential = new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY,
                 META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
                 .setIdentityType(IDENTITY_TYPE_PRIVATE)
                 .build();
@@ -158,6 +173,22 @@
         mNearbyManager.stopBroadcast(callback);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void queryOffloadScanSupport() {
+        OffloadCallback callback = new OffloadCallback();
+        mNearbyManager.queryOffloadCapability(EXECUTOR, callback);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testAllCallbackMethodsExits() {
+        mScanCallback.onDiscovered(mBuilder.setRssi(-10).build());
+        mScanCallback.onUpdated(mBuilder.setRssi(-5).build());
+        mScanCallback.onLost(mBuilder.setRssi(-8).build());
+        mScanCallback.onError(ERROR_UNSUPPORTED);
+    }
+
     private void enableBluetooth() {
         BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
         BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -165,4 +196,11 @@
             assertThat(BTAdapterUtils.enableAdapter(bluetoothAdapter, mContext)).isTrue();
         }
     }
+
+    private static class OffloadCallback implements Consumer<OffloadCapability> {
+        @Override
+        public void accept(OffloadCapability aBoolean) {
+            // no-op for now
+        }
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java
new file mode 100644
index 0000000..a745c7d
--- /dev/null
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/OffloadCapabilityTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.OffloadCapability;
+import android.os.Build;
+import android.os.Parcel;
+
+import androidx.annotation.RequiresApi;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class OffloadCapabilityTest {
+    private static final long VERSION = 123456;
+
+    @Test
+    public void testDefault() {
+        OffloadCapability offloadCapability = new OffloadCapability.Builder().build();
+
+        assertThat(offloadCapability.isFastPairSupported()).isFalse();
+        assertThat(offloadCapability.isNearbyShareSupported()).isFalse();
+        assertThat(offloadCapability.getVersion()).isEqualTo(0);
+    }
+
+    @Test
+    public void testBuilder() {
+        OffloadCapability offloadCapability = new OffloadCapability.Builder()
+                .setFastPairSupported(true)
+                .setNearbyShareSupported(true)
+                .setVersion(VERSION)
+                .build();
+
+        assertThat(offloadCapability.isFastPairSupported()).isTrue();
+        assertThat(offloadCapability.isNearbyShareSupported()).isTrue();
+        assertThat(offloadCapability.getVersion()).isEqualTo(VERSION);
+    }
+
+    @Test
+    public void testWriteParcel() {
+        OffloadCapability offloadCapability = new OffloadCapability.Builder()
+                .setFastPairSupported(true)
+                .setNearbyShareSupported(false)
+                .setVersion(VERSION)
+                .build();
+
+        Parcel parcel = Parcel.obtain();
+        offloadCapability.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        OffloadCapability capability = OffloadCapability.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        assertThat(capability.isFastPairSupported()).isTrue();
+        assertThat(capability.isNearbyShareSupported()).isFalse();
+        assertThat(capability.getVersion()).isEqualTo(VERSION);
+    }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index eaa5ca1..71be889 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -114,4 +114,18 @@
         assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
 
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+        assertThat(broadcastRequest.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PresenceBroadcastRequest[] presenceBroadcastRequests =
+                PresenceBroadcastRequest.CREATOR.newArray(2);
+        assertThat(presenceBroadcastRequests.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index 94f8fe7..ea1de6b 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -104,4 +104,24 @@
         assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
         assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceDevice device =
+                new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+                        .addExtendedProperty(new DataElement(KEY, VALUE))
+                        .setRssi(RSSI)
+                        .addMedium(MEDIUM)
+                        .setName(DEVICE_NAME)
+                        .build();
+        assertThat(device.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PresenceDevice[] devices =
+                PresenceDevice.CREATOR.newArray(2);
+        assertThat(devices.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index cecdfd2..821f2d0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -51,7 +51,6 @@
     private static final int KEY = 3;
     private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
 
-
     private PublicCredential mPublicCredential =
             new PublicCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
                     ENCRYPTED_METADATA, METADATA_ENCRYPTION_KEY_TAG)
@@ -90,5 +89,21 @@
         assertThat(parcelFilter.getType()).isEqualTo(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
         assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
         assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
+        assertThat(parcelFilter.getExtendedProperties().get(0).getKey()).isEqualTo(KEY);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceScanFilter filter = mBuilder.build();
+        assertThat(filter.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        PresenceScanFilter[] filters =
+                PresenceScanFilter.CREATOR.newArray(2);
+        assertThat(filters.length).isEqualTo(2);
     }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index f05f65f..fa8c954 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -99,4 +99,19 @@
         assertThat(credentialElement.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void describeContents() {
+        PrivateCredential credential = mBuilder.build();
+        assertThat(credential.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testCreatorNewArray() {
+        PrivateCredential[]  credentials =
+                PrivateCredential.CREATOR.newArray(2);
+        assertThat(credentials.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index 11bbacc..774e897 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -135,6 +135,7 @@
                         .setIdentityType(IDENTITY_TYPE_PRIVATE)
                         .build();
         assertThat(credentialOne.equals((Object) credentialTwo)).isTrue();
+        assertThat(credentialOne.equals(null)).isFalse();
     }
 
     @Test
@@ -161,4 +162,19 @@
                         .build();
         assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PublicCredential credential = mBuilder.build();
+        assertThat(credential.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        PublicCredential[] credentials  =
+        PublicCredential.CREATOR.newArray(2);
+        assertThat(credentials.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index 21f3d28..5ad52c2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -30,7 +30,6 @@
 import android.nearby.PublicCredential;
 import android.nearby.ScanRequest;
 import android.os.Build;
-import android.os.WorkSource;
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,12 +42,10 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class ScanRequestTest {
 
-    private static final int UID = 1001;
-    private static final String APP_NAME = "android.nearby.tests";
     private static final int RSSI = -40;
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testScanType() {
         ScanRequest request = new ScanRequest.Builder()
                 .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
@@ -59,13 +56,13 @@
 
     // Valid scan type must be set to one of ScanRequest#SCAN_TYPE_
     @Test(expected = IllegalStateException.class)
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testScanType_notSet_throwsException() {
         new ScanRequest.Builder().setScanMode(SCAN_MODE_BALANCED).build();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testScanMode_defaultLowPower() {
         ScanRequest request = new ScanRequest.Builder()
                 .setScanType(SCAN_TYPE_FAST_PAIR)
@@ -76,7 +73,7 @@
 
     /** Verify setting work source with null value in the scan request is allowed */
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testSetWorkSource_nullValue() {
         ScanRequest request = new ScanRequest.Builder()
                 .setScanType(SCAN_TYPE_FAST_PAIR)
@@ -87,39 +84,9 @@
         assertThat(request.getWorkSource().isEmpty()).isTrue();
     }
 
-    /** Verify toString returns expected string. */
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToString() {
-        WorkSource workSource = getWorkSource();
-        ScanRequest request = new ScanRequest.Builder()
-                .setScanType(SCAN_TYPE_FAST_PAIR)
-                .setScanMode(SCAN_MODE_BALANCED)
-                .setBleEnabled(true)
-                .setWorkSource(workSource)
-                .build();
-
-        assertThat(request.toString()).isEqualTo(
-                "Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
-                        + "enableBle=true, workSource=WorkSource{" + UID + " " + APP_NAME
-                        + "}, scanFilters=[]]");
-    }
-
-    /** Verify toString works correctly with null WorkSource. */
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToString_nullWorkSource() {
-        ScanRequest request = new ScanRequest.Builder().setScanType(
-                SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
-
-        assertThat(request.toString()).isEqualTo("Request[scanType=1, "
-                + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
-                + "scanFilters=[]]");
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testisEnableBle_defaultTrue() {
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testIsEnableBle_defaultTrue() {
         ScanRequest request = new ScanRequest.Builder()
                 .setScanType(SCAN_TYPE_FAST_PAIR)
                 .build();
@@ -128,7 +95,28 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testIsOffloadOnly_defaultFalse() {
+        ScanRequest request = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_FAST_PAIR)
+                .build();
+
+        assertThat(request.isOffloadOnly()).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testSetOffloadOnly_isOffloadOnlyTrue() {
+        ScanRequest request = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .setOffloadOnly(true)
+                .build();
+
+        assertThat(request.isOffloadOnly()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void test_isValidScanType() {
         assertThat(ScanRequest.isValidScanType(SCAN_TYPE_FAST_PAIR)).isTrue();
         assertThat(ScanRequest.isValidScanType(SCAN_TYPE_NEARBY_PRESENCE)).isTrue();
@@ -138,7 +126,7 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void test_isValidScanMode() {
         assertThat(ScanRequest.isValidScanMode(SCAN_MODE_LOW_LATENCY)).isTrue();
         assertThat(ScanRequest.isValidScanMode(SCAN_MODE_BALANCED)).isTrue();
@@ -150,7 +138,7 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void test_scanModeToString() {
         assertThat(ScanRequest.scanModeToString(2)).isEqualTo("SCAN_MODE_LOW_LATENCY");
         assertThat(ScanRequest.scanModeToString(1)).isEqualTo("SCAN_MODE_BALANCED");
@@ -162,7 +150,7 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testScanFilter() {
         ScanRequest request = new ScanRequest.Builder().setScanType(
                 SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(getPresenceScanFilter()).build();
@@ -171,6 +159,23 @@
         assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(RSSI);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void describeContents() {
+        ScanRequest request = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_FAST_PAIR)
+                .build();
+        assertThat(request.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testCreatorNewArray() {
+        ScanRequest[] requests =
+                ScanRequest.CREATOR.newArray(2);
+        assertThat(requests.length).isEqualTo(2);
+    }
+
     private static PresenceScanFilter getPresenceScanFilter() {
         final byte[] secretId = new byte[]{1, 2, 3, 4};
         final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
@@ -190,8 +195,4 @@
                 .addPresenceAction(action)
                 .build();
     }
-
-    private static WorkSource getWorkSource() {
-        return new WorkSource(UID, APP_NAME);
-    }
 }
diff --git a/nearby/tests/integration/privileged/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/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index 9f58baf..7dcb263 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -23,6 +23,7 @@
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
new file mode 100644
index 0000000..edda3c2
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FastPairDeviceTest {
+    private static final String NAME = "name";
+    private static final byte[] DATA = new byte[] {0x01, 0x02};
+    private static final String MODEL_ID = "112233";
+    private static final int RSSI = -80;
+    private static final int TX_POWER = -10;
+    private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+    private static List<Integer> sMediums = new ArrayList<Integer>(List.of(1));
+    private static FastPairDevice sDevice;
+
+
+    @Before
+    public void setup() {
+        sDevice = new FastPairDevice(NAME, sMediums, RSSI, TX_POWER, MODEL_ID, MAC_ADDRESS, DATA);
+    }
+
+    @Test
+    public void testParcelable() {
+        Parcel dest = Parcel.obtain();
+        sDevice.writeToParcel(dest, 0);
+        dest.setDataPosition(0);
+        FastPairDevice compareDevice = FastPairDevice.CREATOR.createFromParcel(dest);
+        assertThat(compareDevice.getName()).isEqualTo(NAME);
+        assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+        assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+        assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+        assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+        assertThat(compareDevice.getData()).isEqualTo(DATA);
+        assertThat(compareDevice.equals(sDevice)).isTrue();
+        assertThat(compareDevice.hashCode()).isEqualTo(sDevice.hashCode());
+    }
+
+    @Test
+    public void describeContents() {
+        assertThat(sDevice.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testToString() {
+        assertThat(sDevice.toString()).isEqualTo(
+                "FastPairDevice [name=name, medium={BLE} "
+                        + "rssi=-80 txPower=-10 "
+                        + "modelId=112233 bluetoothAddress=00:11:22:33:44:55]");
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        FastPairDevice[] fastPairDevices = FastPairDevice.CREATOR.newArray(2);
+        assertThat(fastPairDevices.length).isEqualTo(2);
+    }
+
+    @Test
+    public void testBuilder() {
+        FastPairDevice.Builder builder = new FastPairDevice.Builder();
+        FastPairDevice compareDevice = builder.setName(NAME)
+                .addMedium(1)
+                .setBluetoothAddress(MAC_ADDRESS)
+                .setRssi(RSSI)
+                .setTxPower(TX_POWER)
+                .setData(DATA)
+                .setModelId(MODEL_ID)
+                .build();
+        assertThat(compareDevice.getName()).isEqualTo(NAME);
+        assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+        assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+        assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+        assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+        assertThat(compareDevice.getData()).isEqualTo(DATA);
+    }
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
similarity index 60%
rename from nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
rename to nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
index 654b852..a4909b2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/unit/src/android/nearby/NearbyDeviceParcelableTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,20 +14,17 @@
  * limitations under the License.
  */
 
-package android.nearby.cts;
+package android.nearby;
 
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.nearby.NearbyDevice;
-import android.nearby.NearbyDeviceParcelable;
 import android.os.Build;
 import android.os.Parcel;
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -39,10 +36,15 @@
 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class NearbyDeviceParcelableTest {
 
+    private static final long DEVICE_ID = 1234;
     private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
     private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+    private static final byte[] SALT = new byte[] {1, 2, 3, 4};
     private static final String FAST_PAIR_MODEL_ID = "1234";
     private static final int RSSI = -60;
+    private static final int TX_POWER = -10;
+    private static final int ACTION = 1;
+    private static final int MEDIUM_BLE = 1;
 
     private NearbyDeviceParcelable.Builder mBuilder;
 
@@ -50,9 +52,10 @@
     public void setUp() {
         mBuilder =
                 new NearbyDeviceParcelable.Builder()
+                        .setDeviceId(DEVICE_ID)
                         .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
                         .setName("testDevice")
-                        .setMedium(NearbyDevice.Medium.BLE)
+                        .setMedium(MEDIUM_BLE)
                         .setRssi(RSSI)
                         .setFastPairModelId(FAST_PAIR_MODEL_ID)
                         .setBluetoothAddress(BLUETOOTH_ADDRESS)
@@ -60,25 +63,40 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33, codeName = "T")
-    public void test_defaultNullFields() {
+    public void testNullFields() {
+        PublicCredential publicCredential =
+                new PublicCredential.Builder(
+                        new byte[] {1},
+                        new byte[] {2},
+                        new byte[] {3},
+                        new byte[] {4},
+                        new byte[] {5})
+                        .build();
         NearbyDeviceParcelable nearbyDeviceParcelable =
                 new NearbyDeviceParcelable.Builder()
-                        .setMedium(NearbyDevice.Medium.BLE)
+                        .setMedium(MEDIUM_BLE)
+                        .setPublicCredential(publicCredential)
+                        .setAction(ACTION)
                         .setRssi(RSSI)
+                        .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                        .setTxPower(TX_POWER)
+                        .setSalt(SALT)
                         .build();
 
+        assertThat(nearbyDeviceParcelable.getDeviceId()).isEqualTo(-1);
         assertThat(nearbyDeviceParcelable.getName()).isNull();
         assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
         assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
         assertThat(nearbyDeviceParcelable.getData()).isNull();
-
-        assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
+        assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(MEDIUM_BLE);
         assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+        assertThat(nearbyDeviceParcelable.getAction()).isEqualTo(ACTION);
+        assertThat(nearbyDeviceParcelable.getPublicCredential()).isEqualTo(publicCredential);
+        assertThat(nearbyDeviceParcelable.getSalt()).isEqualTo(SALT);
+        assertThat(nearbyDeviceParcelable.getTxPower()).isEqualTo(TX_POWER);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testWriteParcel() {
         NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.build();
 
@@ -89,6 +107,7 @@
                 NearbyDeviceParcelable.CREATOR.createFromParcel(parcel);
         parcel.recycle();
 
+        assertThat(actualNearbyDevice.getDeviceId()).isEqualTo(DEVICE_ID);
         assertThat(actualNearbyDevice.getRssi()).isEqualTo(RSSI);
         assertThat(actualNearbyDevice.getFastPairModelId()).isEqualTo(FAST_PAIR_MODEL_ID);
         assertThat(actualNearbyDevice.getBluetoothAddress()).isEqualTo(BLUETOOTH_ADDRESS);
@@ -96,7 +115,6 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testWriteParcel_nullModelId() {
         NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setFastPairModelId(null).build();
 
@@ -111,10 +129,8 @@
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testWriteParcel_nullBluetoothAddress() {
         NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
-
         Parcel parcel = Parcel.obtain();
         nearbyDeviceParcelable.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
@@ -124,4 +140,34 @@
 
         assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
     }
+
+    @Test
+    public void describeContents() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+        assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testEqual() {
+        PublicCredential publicCredential =
+                new PublicCredential.Builder(
+                        new byte[] {1},
+                        new byte[] {2},
+                        new byte[] {3},
+                        new byte[] {4},
+                        new byte[] {5})
+                        .build();
+        NearbyDeviceParcelable nearbyDeviceParcelable1 =
+                mBuilder.setPublicCredential(publicCredential).build();
+        NearbyDeviceParcelable nearbyDeviceParcelable2 =
+                mBuilder.setPublicCredential(publicCredential).build();
+        assertThat(nearbyDeviceParcelable1.equals(nearbyDeviceParcelable2)).isTrue();
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        NearbyDeviceParcelable[] nearbyDeviceParcelables =
+                NearbyDeviceParcelable.CREATOR.newArray(2);
+        assertThat(nearbyDeviceParcelables.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
index 12de30e..6020c7e 100644
--- a/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
+++ b/nearby/tests/unit/src/android/nearby/ScanRequestTest.java
@@ -24,11 +24,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.Build;
 import android.os.Parcel;
 import android.os.WorkSource;
 import android.platform.test.annotations.Presubmit;
 
+import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
 import org.junit.Test;
@@ -38,14 +41,15 @@
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
 public class ScanRequestTest {
 
     private static final int RSSI = -40;
+    private static final int UID = 1001;
+    private static final String APP_NAME = "android.nearby.tests";
 
     private static WorkSource getWorkSource() {
-        final int uid = 1001;
-        final String appName = "android.nearby.tests";
-        return new WorkSource(uid, appName);
+        return new WorkSource(UID, APP_NAME);
     }
 
     /** Test creating a scan request. */
@@ -104,6 +108,7 @@
 
     /** Verify toString returns expected string. */
     @Test
+    @SdkSuppress(minSdkVersion = 34)
     public void testToString() {
         WorkSource workSource = getWorkSource();
         ScanRequest request = new ScanRequest.Builder()
@@ -115,28 +120,28 @@
 
         assertThat(request.toString()).isEqualTo(
                 "Request[scanType=1, scanMode=SCAN_MODE_BALANCED, "
-                        + "enableBle=true, workSource=WorkSource{1001 android.nearby.tests}, "
-                        + "scanFilters=[]]");
+                        + "bleEnabled=true, offloadOnly=false, "
+                        + "workSource=WorkSource{" + UID + " " + APP_NAME + "}, scanFilters=[]]");
     }
 
     /** Verify toString works correctly with null WorkSource. */
     @Test
-    public void testToString_nullWorkSource() {
+    @SdkSuppress(minSdkVersion = 34)
+    public void testToString_nullWorkSource_offloadOnly() {
         ScanRequest request = new ScanRequest.Builder().setScanType(
-                SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
+                SCAN_TYPE_FAST_PAIR).setWorkSource(null).setOffloadOnly(true).build();
 
         assertThat(request.toString()).isEqualTo("Request[scanType=1, "
-                + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}, "
-                + "scanFilters=[]]");
+                + "scanMode=SCAN_MODE_LOW_POWER, bleEnabled=true, offloadOnly=true, "
+                + "workSource=WorkSource{}, scanFilters=[]]");
     }
 
     /** Verify writing and reading from parcel for scan request. */
     @Test
     public void testParceling() {
-        final int scanType = SCAN_TYPE_NEARBY_PRESENCE;
         WorkSource workSource = getWorkSource();
         ScanRequest originalRequest = new ScanRequest.Builder()
-                .setScanType(scanType)
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
                 .setScanMode(SCAN_MODE_BALANCED)
                 .setBleEnabled(true)
                 .setWorkSource(workSource)
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
new file mode 100644
index 0000000..5ddfed3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyConfigurationTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.DeviceConfig;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class NearbyConfigurationTest {
+
+    private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+    private NearbyConfiguration mNearbyConfiguration;
+
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+    }
+
+    @Test
+    public void testDeviceConfigChanged() throws InterruptedException {
+        mNearbyConfiguration = new NearbyConfiguration();
+
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+                "false", false);
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+                "false", false);
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+                "1", false);
+        Thread.sleep(500);
+
+        assertThat(mNearbyConfiguration.isTestAppSupported()).isFalse();
+        assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isFalse();
+        assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(1);
+
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+                "true", false);
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+                "true", false);
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+                "3", false);
+        Thread.sleep(500);
+
+        assertThat(mNearbyConfiguration.isTestAppSupported()).isTrue();
+        assertThat(mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()).isTrue();
+        assertThat(mNearbyConfiguration.getNanoAppMinVersion()).isEqualTo(3);
+    }
+
+    @After
+    public void tearDown() {
+        // Sets DeviceConfig values back to default
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+                "false", true);
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+                "false", true);
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+                "0", true);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index 8a18cca..5b640cc 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -18,6 +18,9 @@
 
 import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
 
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -32,6 +35,8 @@
 import android.content.Context;
 import android.nearby.IScanListener;
 import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.provider.DeviceConfig;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -45,6 +50,7 @@
 
 public final class NearbyServiceTest {
 
+    private static final String NAMESPACE = NearbyConfiguration.getNamespace();
     private static final String PACKAGE_NAME = "android.nearby.test";
     private Context mContext;
     private NearbyService mService;
@@ -56,11 +62,16 @@
     private IScanListener mScanListener;
     @Mock
     private AppOpsManager mMockAppOpsManager;
+    @Mock
+    private IBinder mIBinder;
 
     @Before
     public void setUp()  {
         initMocks(this);
-        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
+        when(mScanListener.asBinder()).thenReturn(mIBinder);
+
+        mUiAutomation.adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mService = new NearbyService(mContext);
         mScanRequest = createScanRequest();
@@ -80,6 +91,8 @@
 
     @Test
     public void test_register_noPrivilegedPermission_throwsException() {
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+                "false", false);
         mUiAutomation.dropShellPermissionIdentity();
         assertThrows(java.lang.SecurityException.class,
                 () -> mService.registerScanListener(mScanRequest, mScanListener, PACKAGE_NAME,
@@ -88,6 +101,8 @@
 
     @Test
     public void test_unregister_noPrivilegedPermission_throwsException() {
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP,
+                "false", false);
         mUiAutomation.dropShellPermissionIdentity();
         assertThrows(java.lang.SecurityException.class,
                 () -> mService.unregisterScanListener(mScanListener, PACKAGE_NAME,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
new file mode 100644
index 0000000..719e816
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancelableAlarmTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.SystemClock;
+
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class CancelableAlarmTest {
+
+    private static final long DELAY_MILLIS = 1000;
+
+    private final ScheduledExecutorService mExecutor =
+            Executors.newScheduledThreadPool(1);
+
+    @Test
+    public void alarmRuns_singleExecution() throws InterruptedException {
+        TestCountDownLatch latch = new TestCountDownLatch(1);
+        CancelableAlarm.createSingleAlarm(
+                "alarmRuns", new CountDownRunnable(latch), DELAY_MILLIS, mExecutor);
+        latch.awaitAndExpectDelay(DELAY_MILLIS);
+    }
+
+    @Test
+    public void alarmRuns_periodicExecution() throws InterruptedException {
+        TestCountDownLatch latch = new TestCountDownLatch(2);
+        CancelableAlarm.createRecurringAlarm(
+                "alarmRunsPeriodically", new CountDownRunnable(latch), DELAY_MILLIS, mExecutor);
+        latch.awaitAndExpectDelay(DELAY_MILLIS * 2);
+    }
+
+    @Test
+    public void canceledAlarmDoesNotRun_singleExecution() throws InterruptedException {
+        TestCountDownLatch latch = new TestCountDownLatch(1);
+        CancelableAlarm alarm =
+                CancelableAlarm.createSingleAlarm(
+                        "canceledAlarmDoesNotRun",
+                        new CountDownRunnable(latch),
+                        DELAY_MILLIS,
+                        mExecutor);
+        assertThat(alarm.cancel()).isTrue();
+        latch.awaitAndExpectTimeout(DELAY_MILLIS);
+    }
+
+    @Test
+    public void canceledAlarmDoesNotRun_periodicExecution() throws InterruptedException {
+        TestCountDownLatch latch = new TestCountDownLatch(2);
+        CancelableAlarm alarm =
+                CancelableAlarm.createRecurringAlarm(
+                        "canceledAlarmDoesNotRunPeriodically",
+                        new CountDownRunnable(latch),
+                        DELAY_MILLIS,
+                        mExecutor);
+        latch.awaitAndExpectTimeout(DELAY_MILLIS);
+        assertThat(alarm.cancel()).isTrue();
+        latch.awaitAndExpectTimeout(DELAY_MILLIS);
+    }
+
+    @Test
+    public void cancelOfRunAlarmReturnsFalse() throws InterruptedException {
+        TestCountDownLatch latch = new TestCountDownLatch(1);
+        long delayMillis = 500;
+        CancelableAlarm alarm =
+                CancelableAlarm.createSingleAlarm(
+                        "cancelOfRunAlarmReturnsFalse",
+                        new CountDownRunnable(latch),
+                        delayMillis,
+                        mExecutor);
+        latch.awaitAndExpectDelay(delayMillis - 1);
+
+        assertThat(alarm.cancel()).isFalse();
+    }
+
+    private static class CountDownRunnable implements Runnable {
+        private final CountDownLatch mLatch;
+
+        CountDownRunnable(CountDownLatch latch) {
+            this.mLatch = latch;
+        }
+
+        @Override
+        public void run() {
+            mLatch.countDown();
+        }
+    }
+
+    /** A CountDownLatch for test with extra test features like throw exception on await(). */
+    private static class TestCountDownLatch extends CountDownLatch {
+
+        TestCountDownLatch(int count) {
+            super(count);
+        }
+
+        /**
+         * Asserts that the latch does not go off until delayMillis has passed and that it does in
+         * fact go off after delayMillis has passed.
+         */
+        public void awaitAndExpectDelay(long delayMillis) throws InterruptedException {
+            SystemClock.sleep(delayMillis - 1);
+            assertThat(await(0, TimeUnit.MILLISECONDS)).isFalse();
+            SystemClock.sleep(10);
+            assertThat(await(0, TimeUnit.MILLISECONDS)).isTrue();
+        }
+
+        /** Asserts that the latch does not go off within delayMillis. */
+        public void awaitAndExpectTimeout(long delayMillis) throws InterruptedException {
+            SystemClock.sleep(delayMillis + 1);
+            assertThat(await(0, TimeUnit.MILLISECONDS)).isFalse();
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java
new file mode 100644
index 0000000..eb6316e
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/CancellationFlagTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CancellationFlagTest {
+
+    @Test
+    public void initialValueIsFalse() {
+        assertThat(new CancellationFlag().isCancelled()).isFalse();
+    }
+
+    @Test
+    public void cancel() {
+        CancellationFlag flag = new CancellationFlag();
+        flag.cancel();
+        assertThat(flag.isCancelled()).isTrue();
+    }
+
+    @Test
+    public void cancelShouldOnlyCancelOnce() {
+        CancellationFlag flag = new CancellationFlag();
+        AtomicInteger record = new AtomicInteger();
+
+        flag.registerOnCancelListener(() -> record.incrementAndGet());
+        for (int i = 0; i < 3; i++) {
+            flag.cancel();
+        }
+
+        assertThat(flag.isCancelled()).isTrue();
+        assertThat(record.get()).isEqualTo(1);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
new file mode 100644
index 0000000..b577064
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.injector;
+
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ContextHubManagerAdapterTest {
+    private ContextHubManagerAdapter mContextHubManagerAdapter;
+
+    @Mock
+    ContextHubManager mContextHubManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContextHubManagerAdapter = new ContextHubManagerAdapter(mContextHubManager);
+    }
+
+    @Test
+    public void getContextHubs() {
+        mContextHubManagerAdapter.getContextHubs();
+    }
+
+    @Test
+    public void queryNanoApps() {
+        mContextHubManagerAdapter.queryNanoApps(new ContextHubInfo());
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
similarity index 79%
rename from nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
rename to nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
index d45d570..bc38210 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/BroadcastProviderManagerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.provider;
+package com.android.server.nearby.managers;
 
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 
 import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.eq;
@@ -39,6 +39,9 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.provider.BleBroadcastProvider;
+
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.Before;
@@ -51,9 +54,10 @@
 import java.util.Collections;
 
 /**
- * Unit test for {@link BroadcastProviderManager}.
+ * Unit test for {@link com.android.server.nearby.managers.BroadcastProviderManager}.
  */
 public class BroadcastProviderManagerTest {
+    private static final String NAMESPACE = NearbyConfiguration.getNamespace();
     private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
     private static final int MEDIUM_TYPE_BLE = 0;
     private static final byte[] SALT = {2, 3};
@@ -79,11 +83,12 @@
     @Before
     public void setUp() {
         mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
-                "true", false);
+        DeviceConfig.setProperty(
+                NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "true", false);
 
         mContext = ApplicationProvider.getApplicationContext();
-        mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+        mBroadcastProviderManager = new BroadcastProviderManager(
+                MoreExecutors.directExecutor(),
                 mBleBroadcastProvider);
 
         PrivateCredential privateCredential =
@@ -101,14 +106,22 @@
     @Test
     public void testStartAdvertising() {
         mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
-        verify(mBleBroadcastProvider).start(any(byte[].class), any(
-                BleBroadcastProvider.BroadcastListener.class));
+        verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
+                any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+    }
+
+    @Test
+    public void testStopAdvertising() {
+        mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
     }
 
     @Test
     public void testStartAdvertising_featureDisabled() throws Exception {
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
-                "false", false);
+        DeviceConfig.setProperty(
+                NAMESPACE, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, "false", false);
+        DeviceConfig.setProperty(
+                NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+
         mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
                 mBleBroadcastProvider);
         mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
new file mode 100644
index 0000000..aa0dad3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryProviderManagerLegacy} class.
+ */
+public class DiscoveryProviderManagerLegacyTest {
+    private static final int SCAN_MODE_CHRE_ONLY = 3;
+    private static final int DATA_TYPE_SCAN_MODE = 102;
+    private static final int UID = 1234;
+    private static final int PID = 5678;
+    private static final String PACKAGE_NAME = "android.nearby.test";
+    private static final int RSSI = -60;
+    @Mock
+    Injector mInjector;
+    @Mock
+    Context mContext;
+    @Mock
+    AppOpsManager mAppOpsManager;
+    @Mock
+    BleDiscoveryProvider mBleDiscoveryProvider;
+    @Mock
+    ChreDiscoveryProvider mChreDiscoveryProvider;
+    @Mock
+    DiscoveryProviderController mBluetoothController;
+    @Mock
+    DiscoveryProviderController mChreController;
+    @Mock
+    IScanListener mScanListener;
+    @Mock
+    CallerIdentity mCallerIdentity;
+    @Mock
+    DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+    @Mock
+    IBinder mIBinder;
+    private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
+    private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
+            mScanTypeScanListenerRecordMap;
+
+    private static PresenceScanFilter getPresenceScanFilter() {
+        final byte[] secretId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+        PublicCredential credential = new PublicCredential.Builder(
+                secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+                .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                .build();
+
+        final int action = 123;
+        return new PresenceScanFilter.Builder()
+                .addCredential(credential)
+                .setMaxPathLoss(RSSI)
+                .addPresenceAction(action)
+                .build();
+    }
+
+    private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+        final byte[] secretId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+        PublicCredential credential = new PublicCredential.Builder(
+                secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+                .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                .build();
+
+        final int action = 123;
+        DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+                new byte[]{SCAN_MODE_CHRE_ONLY});
+        return new PresenceScanFilter.Builder()
+                .addCredential(credential)
+                .setMaxPathLoss(RSSI)
+                .addPresenceAction(action)
+                .addExtendedProperty(scanModeElement)
+                .build();
+    }
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+        when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+
+        mScanTypeScanListenerRecordMap = new HashMap<>();
+        mDiscoveryProviderManager =
+                new DiscoveryProviderManagerLegacy(mContext, mInjector,
+                        mBleDiscoveryProvider,
+                        mChreDiscoveryProvider,
+                        mScanTypeScanListenerRecordMap);
+        mCallerIdentity = CallerIdentity
+                .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+    }
+
+    @Test
+    public void testOnNearbyDeviceDiscovered() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .build();
+        mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+    }
+
+    @Test
+    public void testInvalidateProviderScanMode() {
+        mDiscoveryProviderManager.invalidateProviderScanMode();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter())
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isFalse();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isNull();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void test_stopChreProvider_clearFilters() throws Exception {
+        // Cannot use mocked ChreDiscoveryProvider,
+        // so we cannot use class variable mDiscoveryProviderManager
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        DiscoveryProviderManagerLegacy manager =
+                new DiscoveryProviderManagerLegacy(mContext, mInjector,
+                        mBleDiscoveryProvider,
+                        new ChreDiscoveryProvider(
+                                mContext,
+                                new ChreCommunication(mInjector, mContext, executor), executor),
+                        mScanTypeScanListenerRecordMap);
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(
+                        scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        // This is an asynchronized process. The filters will be set in executor thread. So we need
+        // to wait for some time to get the correct result.
+        Thread.sleep(200);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        manager.stopChreProvider();
+        Thread.sleep(200);
+        // The filters should be cleared right after.
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isFalse();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+    }
+
+    @Test
+    public void test_restartChreProvider() throws Exception {
+        // Cannot use mocked ChreDiscoveryProvider,
+        // so we cannot use class variable mDiscoveryProviderManager
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        DiscoveryProviderManagerLegacy manager =
+                new DiscoveryProviderManagerLegacy(mContext, mInjector,
+                        mBleDiscoveryProvider,
+                        new ChreDiscoveryProvider(
+                                mContext,
+                                new ChreCommunication(mInjector, mContext, executor), executor),
+                        mScanTypeScanListenerRecordMap);
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManagerLegacy.ScanListenerRecord record =
+                new DiscoveryProviderManagerLegacy.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        // This is an asynchronized process. The filters will be set in executor thread. So we need
+        // to wait for some time to get the correct result.
+        Thread.sleep(200);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        // We want to make sure quickly restart the provider the filters should
+        // be reset correctly.
+        // See b/255922206, there can be a race condition that filters get cleared because onStop()
+        // get executed after onStart() if they are called from different threads.
+        manager.stopChreProvider();
+        manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+                List.of(getPresenceScanFilter()));
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        Thread.sleep(200);
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        // Wait for enough time
+        Thread.sleep(1000);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
new file mode 100644
index 0000000..7ecf631
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.provider.BleDiscoveryProvider;
+import com.android.server.nearby.provider.ChreCommunication;
+import com.android.server.nearby.provider.ChreDiscoveryProvider;
+import com.android.server.nearby.provider.DiscoveryProviderController;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class DiscoveryProviderManagerTest {
+    private static final int SCAN_MODE_CHRE_ONLY = 3;
+    private static final int DATA_TYPE_SCAN_MODE = 102;
+    private static final int UID = 1234;
+    private static final int PID = 5678;
+    private static final String PACKAGE_NAME = "android.nearby.test";
+    private static final int RSSI = -60;
+    @Mock
+    Injector mInjector;
+    @Mock
+    Context mContext;
+    @Mock
+    AppOpsManager mAppOpsManager;
+    @Mock
+    BleDiscoveryProvider mBleDiscoveryProvider;
+    @Mock
+    ChreDiscoveryProvider mChreDiscoveryProvider;
+    @Mock
+    DiscoveryProviderController mBluetoothController;
+    @Mock
+    DiscoveryProviderController mChreController;
+    @Mock
+    IScanListener mScanListener;
+    @Mock
+    CallerIdentity mCallerIdentity;
+    @Mock
+    IBinder mIBinder;
+    private Executor mExecutor;
+    private DiscoveryProviderManager mDiscoveryProviderManager;
+
+    private static PresenceScanFilter getPresenceScanFilter() {
+        final byte[] secretId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+        PublicCredential credential = new PublicCredential.Builder(
+                secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+                .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                .build();
+
+        final int action = 123;
+        return new PresenceScanFilter.Builder()
+                .addCredential(credential)
+                .setMaxPathLoss(RSSI)
+                .addPresenceAction(action)
+                .build();
+    }
+
+    private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+        final byte[] secretId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+        PublicCredential credential = new PublicCredential.Builder(
+                secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+                .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                .build();
+
+        final int action = 123;
+        DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+                new byte[]{SCAN_MODE_CHRE_ONLY});
+        return new PresenceScanFilter.Builder()
+                .addCredential(credential)
+                .setMaxPathLoss(RSSI)
+                .addPresenceAction(action)
+                .addExtendedProperty(scanModeElement)
+                .build();
+    }
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mExecutor = Executors.newSingleThreadExecutor();
+        when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+        when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+        when(mScanListener.asBinder()).thenReturn(mIBinder);
+
+        mDiscoveryProviderManager =
+                new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+                        mBleDiscoveryProvider,
+                        mChreDiscoveryProvider);
+        mCallerIdentity = CallerIdentity
+                .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+    }
+
+    @Test
+    public void testOnNearbyDeviceDiscovered() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .build();
+        mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+    }
+
+    @Test
+    public void testInvalidateProviderScanMode() {
+        mDiscoveryProviderManager.invalidateProviderScanMode();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+        reset(mBluetoothController);
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+        reset(mBluetoothController);
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+        reset(mBluetoothController);
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isFalse();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+        reset(mBluetoothController);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+        reset(mBluetoothController);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(null);
+        reset(mBluetoothController);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, never()).start();
+        assertThat(start).isNull();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(null);
+        reset(mBluetoothController);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        mDiscoveryProviderManager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        Boolean start = mDiscoveryProviderManager.startProviders();
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(start).isTrue();
+    }
+
+    @Test
+    public void test_stopChreProvider_clearFilters() throws Exception {
+        // Cannot use mocked ChreDiscoveryProvider,
+        // so we cannot use class variable mDiscoveryProviderManager
+        DiscoveryProviderManager manager =
+                new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+                        mBleDiscoveryProvider,
+                        new ChreDiscoveryProvider(
+                                mContext,
+                                new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        // This is an asynchronized process. The filters will be set in executor thread. So we need
+        // to wait for some time to get the correct result.
+        Thread.sleep(200);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        manager.stopChreProvider();
+        Thread.sleep(200);
+        // The filters should be cleared right after.
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isFalse();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isEmpty();
+    }
+
+    @Test
+    public void test_restartChreProvider() throws Exception {
+        // Cannot use mocked ChreDiscoveryProvider,
+        // so we cannot use class variable mDiscoveryProviderManager
+        DiscoveryProviderManager manager =
+                new DiscoveryProviderManager(mContext, mExecutor, mInjector,
+                        mBleDiscoveryProvider,
+                        new ChreDiscoveryProvider(
+                                mContext,
+                                new ChreCommunication(mInjector, mContext, mExecutor), mExecutor));
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        manager.registerScanListener(scanRequest, mScanListener, mCallerIdentity);
+
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        // This is an asynchronized process. The filters will be set in executor thread. So we need
+        // to wait for some time to get the correct result.
+        Thread.sleep(200);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        // We want to make sure quickly restart the provider the filters should
+        // be reset correctly.
+        // See b/255922206, there can be a race condition that filters get cleared because onStop()
+        // get executed after onStart() if they are called from different threads.
+        manager.stopChreProvider();
+        manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+                List.of(getPresenceScanFilter()));
+        manager.startChreProvider(List.of(getPresenceScanFilter()));
+        Thread.sleep(200);
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+        // Wait for enough time
+        Thread.sleep(1000);
+
+        assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+                .isTrue();
+        assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
new file mode 100644
index 0000000..104d762
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/ListenerMultiplexerTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.registration.BinderListenerRegistration;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+public class ListenerMultiplexerTest {
+
+    @Before
+    public void setUp() {
+        initMocks(this);
+    }
+
+    @Test
+    public void testAdd() {
+        TestMultiplexer multiplexer = new TestMultiplexer();
+
+        Runnable listener = mock(Runnable.class);
+        IBinder binder = mock(IBinder.class);
+        int value = 2;
+        multiplexer.addListener(binder, listener, value);
+
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+            assertThat(multiplexer.mMerged).isEqualTo(value);
+        }
+        Runnable listener2 = mock(Runnable.class);
+        IBinder binder2 = mock(IBinder.class);
+        int value2 = 1;
+        multiplexer.addListener(binder2, listener2, value2);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+            assertThat(multiplexer.mMerged).isEqualTo(value);
+        }
+    }
+
+    @Test
+    public void testReplace() {
+        TestMultiplexer multiplexer = new TestMultiplexer();
+        Runnable listener = mock(Runnable.class);
+        IBinder binder = mock(IBinder.class);
+        int value = 2;
+        multiplexer.addListener(binder, listener, value);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMerged).isEqualTo(value);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(1)).run();
+        reset(listener);
+
+        // Same key, different value
+        Runnable listener2 = mock(Runnable.class);
+        int value2 = 1;
+        multiplexer.addListener(binder, listener2, value2);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            // Should not be called again
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(0);
+            assertThat(multiplexer.mMerged).isEqualTo(value2);
+        }
+        // Run on the new listener
+        multiplexer.notifyListeners();
+        verify(listener, never()).run();
+        verify(listener2, times(1)).run();
+
+        multiplexer.removeRegistration(binder);
+
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isFalse();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mOnUnregisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+        }
+    }
+
+    @Test
+    public void testRemove() {
+        TestMultiplexer multiplexer = new TestMultiplexer();
+        Runnable listener = mock(Runnable.class);
+        IBinder binder = mock(IBinder.class);
+        int value = 2;
+        multiplexer.addListener(binder, listener, value);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mMerged).isEqualTo(value);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(1)).run();
+        reset(listener);
+
+        multiplexer.removeRegistration(binder);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isFalse();
+            assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, never()).run();
+    }
+
+    @Test
+    public void testMergeMultiple() {
+        TestMultiplexer multiplexer = new TestMultiplexer();
+
+        Runnable listener = mock(Runnable.class);
+        IBinder binder = mock(IBinder.class);
+        int value = 2;
+
+        Runnable listener2 = mock(Runnable.class);
+        IBinder binder2 = mock(IBinder.class);
+        int value2 = 1;
+
+        Runnable listener3 = mock(Runnable.class);
+        IBinder binder3 = mock(IBinder.class);
+        int value3 = 5;
+
+        multiplexer.addListener(binder, listener, value);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+            assertThat(multiplexer.mMerged).isEqualTo(value);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(1)).run();
+        verify(listener2, never()).run();
+        verify(listener3, never()).run();
+
+        multiplexer.addListener(binder2, listener2, value2);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(2);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(1);
+            assertThat(multiplexer.mMerged).isEqualTo(value);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(2)).run();
+        verify(listener2, times(1)).run();
+        verify(listener3, never()).run();
+
+        multiplexer.addListener(binder3, listener3, value3);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(3);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+            assertThat(multiplexer.mMerged).isEqualTo(value3);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(3)).run();
+        verify(listener2, times(2)).run();
+        verify(listener3, times(1)).run();
+
+        multiplexer.removeRegistration(binder);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(4);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(2);
+            assertThat(multiplexer.mMerged).isEqualTo(value3);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(3)).run();
+        verify(listener2, times(3)).run();
+        verify(listener3, times(2)).run();
+
+        multiplexer.removeRegistration(binder3);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isTrue();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(5);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(3);
+            assertThat(multiplexer.mMerged).isEqualTo(value2);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(3)).run();
+        verify(listener2, times(4)).run();
+        verify(listener3, times(2)).run();
+
+        multiplexer.removeRegistration(binder2);
+        synchronized (multiplexer.mMultiplexerLock) {
+            assertThat(multiplexer.mRegistered).isFalse();
+            assertThat(multiplexer.mOnRegisterCalledCount).isEqualTo(1);
+            assertThat(multiplexer.mMergeOperationCount).isEqualTo(6);
+            assertThat(multiplexer.mMergeUpdatedCount).isEqualTo(4);
+            assertThat(multiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+        }
+        multiplexer.notifyListeners();
+        verify(listener, times(3)).run();
+        verify(listener2, times(4)).run();
+        verify(listener3, times(2)).run();
+    }
+
+    private class TestMultiplexer extends
+            ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+        int mOnRegisterCalledCount;
+        int mOnUnregisterCalledCount;
+        boolean mRegistered;
+        private int mMergeOperationCount;
+        private int mMergeUpdatedCount;
+
+        @Override
+        public void onRegister() {
+            mOnRegisterCalledCount++;
+            mRegistered = true;
+        }
+
+        @Override
+        public void onUnregister() {
+            mOnUnregisterCalledCount++;
+            mRegistered = false;
+        }
+
+        @Override
+        public Integer mergeRegistrations(
+                @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+            mMergeOperationCount++;
+            int max = Integer.MIN_VALUE;
+            for (TestListenerRegistration registration : testListenerRegistrations) {
+                max = Math.max(max, registration.getValue());
+            }
+            return max;
+        }
+
+        @Override
+        public void onMergedRegistrationsUpdated() {
+            mMergeUpdatedCount++;
+        }
+
+        public void addListener(IBinder binder, Runnable runnable, int value) {
+            TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+                    value);
+            putRegistration(binder, registration);
+        }
+
+        public void notifyListeners() {
+            deliverToListeners(registration -> Runnable::run);
+        }
+
+        private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+            private final int mValue;
+
+            protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+                super(binder, MoreExecutors.directExecutor(), runnable);
+                mValue = value;
+            }
+
+            @Override
+            public TestMultiplexer getOwner() {
+                return TestMultiplexer.this;
+            }
+
+            public int getValue() {
+                return mValue;
+            }
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
new file mode 100644
index 0000000..9281e42
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/MergedDiscoveryRequestTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.DataElement;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.util.ArraySet;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Unit test for {@link MergedDiscoveryRequest} class.
+ */
+public class MergedDiscoveryRequestTest {
+
+    @Test
+    public void test_addScanType() {
+        MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+        builder.addScanType(ScanRequest.SCAN_TYPE_FAST_PAIR);
+        builder.addScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
+        MergedDiscoveryRequest request = builder.build();
+
+        assertThat(request.getScanTypes()).isEqualTo(new ArraySet<>(
+                Arrays.asList(ScanRequest.SCAN_TYPE_FAST_PAIR,
+                        ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)));
+    }
+
+    @Test
+    public void test_addActions() {
+        MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+        builder.addActions(new ArrayList<>(Arrays.asList(1, 2, 3)));
+        builder.addActions(new ArraySet<>(Arrays.asList(2, 3, 4)));
+        builder.addActions(new ArraySet<>(Collections.singletonList(5)));
+
+        MergedDiscoveryRequest request = builder.build();
+        assertThat(request.getActions()).isEqualTo(new ArraySet<>(new Integer[]{1, 2, 3, 4, 5}));
+    }
+
+    @Test
+    public void test_addFilters() {
+        final int rssi = -40;
+        final int action = 123;
+        final byte[] secreteId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+        final int key = 3;
+        final byte[] value = new byte[]{1, 1, 1, 1};
+
+        PublicCredential mPublicCredential = new PublicCredential.Builder(secreteId,
+                authenticityKey, publicKey, encryptedMetadata,
+                metadataEncryptionKeyTag).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+        PresenceScanFilter scanFilterBuilder = new PresenceScanFilter.Builder().setMaxPathLoss(
+                rssi).addCredential(mPublicCredential).addPresenceAction(
+                action).addExtendedProperty(new DataElement(key, value)).build();
+
+        MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+        builder.addScanFilters(Collections.singleton(scanFilterBuilder));
+        MergedDiscoveryRequest request = builder.build();
+
+        Set<ScanFilter> expectedResult = new ArraySet<>();
+        expectedResult.add(scanFilterBuilder);
+        assertThat(request.getScanFilters()).isEqualTo(expectedResult);
+    }
+
+    @Test
+    public void test_addMedium() {
+        MergedDiscoveryRequest.Builder builder = new MergedDiscoveryRequest.Builder();
+        builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+        builder.addMedium(MergedDiscoveryRequest.Medium.BLE);
+        MergedDiscoveryRequest request = builder.build();
+
+        Set<Integer> expectedResult = new ArraySet<>();
+        expectedResult.add(MergedDiscoveryRequest.Medium.BLE);
+        assertThat(request.getMediums()).isEqualTo(expectedResult);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
new file mode 100644
index 0000000..8814190
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/BinderListenerRegistrationTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+
+/**
+ * Unit test for {@link BinderListenerRegistration} class.
+ */
+public class BinderListenerRegistrationTest {
+    private TestMultiplexer mMultiplexer;
+    private boolean mOnRegisterCalled;
+    private boolean mOnUnRegisterCalled;
+
+    @Before
+    public void setUp() {
+        mMultiplexer = new TestMultiplexer();
+    }
+
+    @Test
+    public void test_addAndRemove() throws RemoteException {
+        Runnable listener = mock(Runnable.class);
+        IBinder binder = mock(IBinder.class);
+        int value = 2;
+        BinderListenerRegistration<Runnable> registration = mMultiplexer.addListener(binder,
+                listener, value);
+        // First element, onRegister should be called
+        assertThat(mOnRegisterCalled).isTrue();
+        verify(binder, times(1)).linkToDeath(any(), anyInt());
+        mMultiplexer.notifyListeners();
+        verify(listener, times(1)).run();
+        synchronized (mMultiplexer.mMultiplexerLock) {
+            assertThat(mMultiplexer.mMerged).isEqualTo(value);
+        }
+        reset(listener);
+
+        Runnable listener2 = mock(Runnable.class);
+        IBinder binder2 = mock(IBinder.class);
+        int value2 = 1;
+        BinderListenerRegistration<Runnable> registration2 = mMultiplexer.addListener(binder2,
+                listener2, value2);
+        verify(binder2, times(1)).linkToDeath(any(), anyInt());
+        mMultiplexer.notifyListeners();
+        verify(listener2, times(1)).run();
+        synchronized (mMultiplexer.mMultiplexerLock) {
+            assertThat(mMultiplexer.mMerged).isEqualTo(value);
+        }
+        reset(listener);
+        reset(listener2);
+
+        registration2.remove();
+        verify(binder2, times(1)).unlinkToDeath(any(), anyInt());
+        // Remove one element, onUnregister should NOT be called
+        assertThat(mOnUnRegisterCalled).isFalse();
+        mMultiplexer.notifyListeners();
+        verify(listener, times(1)).run();
+        synchronized (mMultiplexer.mMultiplexerLock) {
+            assertThat(mMultiplexer.mMerged).isEqualTo(value);
+        }
+        reset(listener);
+        reset(listener2);
+
+        registration.remove();
+        verify(binder, times(1)).unlinkToDeath(any(), anyInt());
+        // Remove all elements, onUnregister should NOT be called
+        assertThat(mOnUnRegisterCalled).isTrue();
+        synchronized (mMultiplexer.mMultiplexerLock) {
+            assertThat(mMultiplexer.mMerged).isEqualTo(Integer.MIN_VALUE);
+        }
+    }
+
+    private class TestMultiplexer extends
+            ListenerMultiplexer<Runnable, TestMultiplexer.TestListenerRegistration, Integer> {
+        @Override
+        public void onRegister() {
+            mOnRegisterCalled = true;
+        }
+
+        @Override
+        public void onUnregister() {
+            mOnUnRegisterCalled = true;
+        }
+
+        @Override
+        public Integer mergeRegistrations(
+                @NonNull Collection<TestListenerRegistration> testListenerRegistrations) {
+            int max = Integer.MIN_VALUE;
+            for (TestListenerRegistration registration : testListenerRegistrations) {
+                max = Math.max(max, registration.getValue());
+            }
+            return max;
+        }
+
+        @Override
+        public void onMergedRegistrationsUpdated() {
+        }
+
+        public BinderListenerRegistration<Runnable> addListener(IBinder binder, Runnable runnable,
+                int value) {
+            TestListenerRegistration registration = new TestListenerRegistration(binder, runnable,
+                    value);
+            putRegistration(binder, registration);
+            return registration;
+        }
+
+        public void notifyListeners() {
+            deliverToListeners(registration -> Runnable::run);
+        }
+
+        private class TestListenerRegistration extends BinderListenerRegistration<Runnable> {
+            private final int mValue;
+
+            protected TestListenerRegistration(IBinder binder, Runnable runnable, int value) {
+                super(binder, MoreExecutors.directExecutor(), runnable);
+                mValue = value;
+            }
+
+            @Override
+            public TestMultiplexer getOwner() {
+                return TestMultiplexer.this;
+            }
+
+            public int getValue() {
+                return mValue;
+            }
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
new file mode 100644
index 0000000..03c4f75
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/registration/DiscoveryRegistrationTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.managers.registration;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.AppOpsManager;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.nearby.managers.ListenerMultiplexer;
+import com.android.server.nearby.managers.MergedDiscoveryRequest;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit test for {@link DiscoveryRegistration} class.
+ */
+public class DiscoveryRegistrationTest {
+    private static final int RSSI = -40;
+    private static final int ACTION = 123;
+    private static final byte[] SECRETE_ID = new byte[]{1, 2, 3, 4};
+    private static final byte[] AUTHENTICITY_KEY = new byte[]{0, 1, 1, 1};
+    private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
+    private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
+    private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+    private static final int KEY = 3;
+    private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
+    private final PublicCredential mPublicCredential = new PublicCredential.Builder(SECRETE_ID,
+            AUTHENTICITY_KEY, PUBLIC_KEY, ENCRYPTED_METADATA,
+            METADATA_ENCRYPTION_KEY_TAG).setIdentityType(IDENTITY_TYPE_PRIVATE).build();
+    private final PresenceScanFilter mFilter = new PresenceScanFilter.Builder().setMaxPathLoss(
+            50).addCredential(mPublicCredential).addPresenceAction(ACTION).addExtendedProperty(
+            new DataElement(KEY, VALUE)).build();
+    private DiscoveryRegistration mDiscoveryRegistration;
+    private ScanRequest mScanRequest;
+    private TestDiscoveryManager mOwner;
+    private Object mMultiplexLock;
+    @Mock
+    private IScanListener mCallback;
+    @Mock
+    private CallerIdentity mIdentity;
+    @Mock
+    private AppOpsManager mAppOpsManager;
+    @Mock
+    private IBinder mBinder;
+
+    @Before
+    public void setUp() {
+        initMocks(this);
+        when(mCallback.asBinder()).thenReturn(mBinder);
+        when(mAppOpsManager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+                eq(null))).thenReturn(AppOpsManager.MODE_ALLOWED);
+
+        mOwner = new TestDiscoveryManager();
+        mMultiplexLock = new Object();
+        mScanRequest = new ScanRequest.Builder().setScanType(
+                ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).addScanFilter(mFilter).build();
+        mDiscoveryRegistration = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+                Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, mAppOpsManager);
+    }
+
+    @Test
+    public void test_getScanRequest() {
+        assertThat(mDiscoveryRegistration.getScanRequest()).isEqualTo(mScanRequest);
+    }
+
+    @Test
+    public void test_getActions() {
+        Set<Integer> result = new ArraySet<>();
+        result.add(ACTION);
+        assertThat(mDiscoveryRegistration.getActions()).isEqualTo(result);
+    }
+
+    @Test
+    public void test_getOwner() {
+        assertThat(mDiscoveryRegistration.getOwner()).isEqualTo(mOwner);
+    }
+
+    @Test
+    public void test_getPresenceScanFilters() {
+        Set<ScanFilter> result = new ArraySet<>();
+        result.add(mFilter);
+        assertThat(mDiscoveryRegistration.getPresenceScanFilters()).isEqualTo(result);
+    }
+
+    @Test
+    public void test_presenceFilterMatches_match() {
+        NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+                123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+                ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+                METADATA_ENCRYPTION_KEY_TAG).build();
+        assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isTrue();
+    }
+
+    @Test
+    public void test_presenceFilterMatches_emptyFilter() {
+        NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+                123).setName("test").setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).build();
+        assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of())).isTrue();
+    }
+
+    @Test
+    public void test_presenceFilterMatches_actionNotMatch() {
+        NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+                12).setName("test").setRssi(RSSI).setScanType(
+                ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(5).setEncryptionKeyTag(
+                METADATA_ENCRYPTION_KEY_TAG).build();
+        assertThat(DiscoveryRegistration.presenceFilterMatches(device, List.of(mFilter))).isFalse();
+    }
+
+    @Test
+    public void test_onDiscoveredOnUpdatedCalled() throws Exception {
+        final long deviceId = 122;
+        NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder().setDeviceId(
+                deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+                ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+                METADATA_ENCRYPTION_KEY_TAG);
+        runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.build()));
+
+        verify(mCallback, times(1)).onDiscovered(eq(builder.build()));
+        verify(mCallback, never()).onUpdated(any());
+        verify(mCallback, never()).onLost(any());
+        verify(mCallback, never()).onError(anyInt());
+        assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+        reset(mCallback);
+
+        // Update RSSI
+        runOperation(
+                mDiscoveryRegistration.onNearbyDeviceDiscovered(builder.setRssi(RSSI - 1).build()));
+        verify(mCallback, never()).onDiscovered(any());
+        verify(mCallback, times(1)).onUpdated(eq(builder.build()));
+        verify(mCallback, never()).onLost(any());
+        verify(mCallback, never()).onError(anyInt());
+        assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+    }
+
+    @Test
+    public void test_onLost() throws Exception {
+        final long deviceId = 123;
+        NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+                deviceId).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+                ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+                METADATA_ENCRYPTION_KEY_TAG).build();
+        runOperation(mDiscoveryRegistration.onNearbyDeviceDiscovered(device));
+        assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNotNull();
+        verify(mCallback, times(1)).onDiscovered(eq(device));
+        verify(mCallback, never()).onUpdated(any());
+        verify(mCallback, never()).onError(anyInt());
+        verify(mCallback, never()).onLost(any());
+        reset(mCallback);
+
+        runOperation(mDiscoveryRegistration.reportDeviceLost(device));
+
+        assertThat(mDiscoveryRegistration.getDiscoveryOnLostAlarms().get(deviceId)).isNull();
+        verify(mCallback, never()).onDiscovered(eq(device));
+        verify(mCallback, never()).onUpdated(any());
+        verify(mCallback, never()).onError(anyInt());
+        verify(mCallback, times(1)).onLost(eq(device));
+    }
+
+    @Test
+    public void test_onError() throws Exception {
+        AppOpsManager manager = mock(AppOpsManager.class);
+        when(manager.noteOp(eq("android:bluetooth_scan"), eq(0), eq(null), eq(null),
+                eq(null))).thenReturn(AppOpsManager.MODE_IGNORED);
+
+        DiscoveryRegistration r = new DiscoveryRegistration(mOwner, mScanRequest, mCallback,
+                Executors.newSingleThreadExecutor(), mIdentity, mMultiplexLock, manager);
+
+        NearbyDeviceParcelable device = new NearbyDeviceParcelable.Builder().setDeviceId(
+                123).setName("test").setTxPower(RSSI + 1).setRssi(RSSI).setScanType(
+                ScanRequest.SCAN_TYPE_NEARBY_PRESENCE).setAction(ACTION).setEncryptionKeyTag(
+                METADATA_ENCRYPTION_KEY_TAG).build();
+        runOperation(r.onNearbyDeviceDiscovered(device));
+
+        verify(mCallback, never()).onDiscovered(any());
+        verify(mCallback, never()).onUpdated(any());
+        verify(mCallback, never()).onLost(any());
+        verify(mCallback, times(1)).onError(eq(ScanCallback.ERROR_PERMISSION_DENIED));
+    }
+
+    private void runOperation(BinderListenerRegistration.ListenerOperation<IScanListener> operation)
+            throws Exception {
+        if (operation == null) {
+            return;
+        }
+        operation.onScheduled(false);
+        operation.operate(mCallback);
+        operation.onComplete(/* success= */ true);
+    }
+
+    private static class TestDiscoveryManager extends
+            ListenerMultiplexer<IScanListener, DiscoveryRegistration, MergedDiscoveryRequest> {
+
+        @Override
+        public MergedDiscoveryRequest mergeRegistrations(
+                @NonNull Collection<DiscoveryRegistration> discoveryRegistrations) {
+            return null;
+        }
+
+        @Override
+        public void onMergedRegistrationsUpdated() {
+
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
new file mode 100644
index 0000000..e186709
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.nearby.BroadcastRequest;
+
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Unit test for {@link DataElementHeader}.
+ */
+public class DataElementHeaderTest {
+
+    private static final int VERSION = BroadcastRequest.PRESENCE_VERSION_V1;
+
+    @Test
+    public void test_illegalLength() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new DataElementHeader(VERSION, 12, 128));
+    }
+
+    @Test
+    public void test_singeByteConversion() {
+        DataElementHeader header = new DataElementHeader(VERSION, 12, 3);
+        byte[] bytes = header.toBytes();
+        assertThat(bytes).isEqualTo(new byte[]{(byte) 0b00111100});
+
+        DataElementHeader afterConversionHeader = DataElementHeader.fromBytes(VERSION, bytes);
+        assertThat(afterConversionHeader.getDataLength()).isEqualTo(3);
+        assertThat(afterConversionHeader.getDataType()).isEqualTo(12);
+    }
+
+    @Test
+    public void test_multipleBytesConversion() {
+        DataElementHeader header = new DataElementHeader(VERSION, 6, 100);
+        DataElementHeader afterConversionHeader =
+                DataElementHeader.fromBytes(VERSION, header.toBytes());
+        assertThat(afterConversionHeader.getDataLength()).isEqualTo(100);
+        assertThat(afterConversionHeader.getDataType()).isEqualTo(6);
+    }
+
+    @Test
+    public void test_fromBytes() {
+        // Single byte case.
+        byte[] singleByte = new byte[]{(byte) 0b01011101};
+        DataElementHeader singeByteHeader = DataElementHeader.fromBytes(VERSION, singleByte);
+        assertThat(singeByteHeader.getDataLength()).isEqualTo(5);
+        assertThat(singeByteHeader.getDataType()).isEqualTo(13);
+
+        // Two bytes case.
+        byte[] twoBytes = new byte[]{(byte) 0b11011101, (byte) 0b01011101};
+        DataElementHeader twoBytesHeader = DataElementHeader.fromBytes(VERSION, twoBytes);
+        assertThat(twoBytesHeader.getDataLength()).isEqualTo(93);
+        assertThat(twoBytesHeader.getDataType()).isEqualTo(93);
+
+        // Three bytes case.
+        byte[] threeBytes = new byte[]{(byte) 0b11011101, (byte) 0b11111111, (byte) 0b01011101};
+        DataElementHeader threeBytesHeader = DataElementHeader.fromBytes(VERSION, threeBytes);
+        assertThat(threeBytesHeader.getDataLength()).isEqualTo(93);
+        assertThat(threeBytesHeader.getDataType()).isEqualTo(16349);
+
+        // Four bytes case.
+        byte[] fourBytes = new byte[]{
+                (byte) 0b11011101, (byte) 0b11111111, (byte) 0b11111111, (byte) 0b01011101};
+
+        DataElementHeader fourBytesHeader = DataElementHeader.fromBytes(VERSION, fourBytes);
+        assertThat(fourBytesHeader.getDataLength()).isEqualTo(93);
+        assertThat(fourBytesHeader.getDataType()).isEqualTo(2097117);
+    }
+
+    @Test
+    public void test_fromBytesIllegal_singleByte() {
+        assertThrows(IllegalArgumentException.class,
+                () -> DataElementHeader.fromBytes(VERSION, new byte[]{(byte) 0b11011101}));
+    }
+
+    @Test
+    public void test_fromBytesIllegal_twoBytes_wrongFirstByte() {
+        assertThrows(IllegalArgumentException.class,
+                () -> DataElementHeader.fromBytes(VERSION,
+                        new byte[]{(byte) 0b01011101, (byte) 0b01011101}));
+    }
+
+    @Test
+    public void test_fromBytesIllegal_twoBytes_wrongLastByte() {
+        assertThrows(IllegalArgumentException.class,
+                () -> DataElementHeader.fromBytes(VERSION,
+                        new byte[]{(byte) 0b11011101, (byte) 0b11011101}));
+    }
+
+    @Test
+    public void test_fromBytesIllegal_threeBytes() {
+        assertThrows(IllegalArgumentException.class,
+                () -> DataElementHeader.fromBytes(VERSION,
+                        new byte[]{(byte) 0b11011101, (byte) 0b11011101, (byte) 0b11011101}));
+    }
+
+    @Test
+    public void test_multipleBytesConversion_largeNumber() {
+        DataElementHeader header = new DataElementHeader(VERSION, 22213546, 66);
+        DataElementHeader afterConversionHeader =
+                DataElementHeader.fromBytes(VERSION, header.toBytes());
+        assertThat(afterConversionHeader.getDataLength()).isEqualTo(66);
+        assertThat(afterConversionHeader.getDataType()).isEqualTo(22213546);
+    }
+
+    @Test
+    public void test_isExtending() {
+        assertThat(DataElementHeader.isExtending((byte) 0b10000100)).isTrue();
+        assertThat(DataElementHeader.isExtending((byte) 0b01110100)).isFalse();
+        assertThat(DataElementHeader.isExtending((byte) 0b00000000)).isFalse();
+    }
+
+    @Test
+    public void test_convertTag() {
+        assertThat(DataElementHeader.convertTag(true)).isEqualTo((byte) 128);
+        assertThat(DataElementHeader.convertTag(false)).isEqualTo(0);
+    }
+
+    @Test
+    public void test_getHeaderValue() {
+        assertThat(DataElementHeader.getHeaderValue((byte) 0b10000100)).isEqualTo(4);
+        assertThat(DataElementHeader.getHeaderValue((byte) 0b00000100)).isEqualTo(4);
+        assertThat(DataElementHeader.getHeaderValue((byte) 0b11010100)).isEqualTo(84);
+        assertThat(DataElementHeader.getHeaderValue((byte) 0b01010100)).isEqualTo(84);
+    }
+
+    @Test
+    public void test_convertTypeMultipleIntList() {
+        List<Byte> list = DataElementHeader.convertTypeMultipleBytes(128);
+        assertThat(list.size()).isEqualTo(2);
+        assertThat(list.get(0)).isEqualTo((byte) 0b10000001);
+        assertThat(list.get(1)).isEqualTo((byte) 0b00000000);
+
+        List<Byte> list2 = DataElementHeader.convertTypeMultipleBytes(10);
+        assertThat(list2.size()).isEqualTo(1);
+        assertThat(list2.get(0)).isEqualTo((byte) 0b00001010);
+
+        List<Byte> list3 = DataElementHeader.convertTypeMultipleBytes(5242398);
+        assertThat(list3.size()).isEqualTo(4);
+        assertThat(list3.get(0)).isEqualTo((byte) 0b10000010);
+        assertThat(list3.get(1)).isEqualTo((byte) 0b10111111);
+        assertThat(list3.get(2)).isEqualTo((byte) 0b11111100);
+        assertThat(list3.get(3)).isEqualTo((byte) 0b00011110);
+    }
+
+    @Test
+    public void test_getTypeMultipleBytes() {
+        byte[] inputBytes = new byte[]{(byte) 0b11011000, (byte) 0b10000000, (byte) 0b00001001};
+        // 0b101100000000000001001
+        assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes)).isEqualTo(1441801);
+
+        byte[] inputBytes2 = new byte[]{(byte) 0b00010010};
+        assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes2)).isEqualTo(18);
+
+        byte[] inputBytes3 = new byte[]{(byte) 0b10000001, (byte) 0b00000000};
+        assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes3)).isEqualTo(128);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
new file mode 100644
index 0000000..895df69
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+import android.nearby.PublicCredential;
+
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ExtendedAdvertisementTest {
+    private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+    private static final int DATA_TYPE_MODEL_ID = 7;
+    private static final int DATA_TYPE_BLE_ADDRESS = 101;
+    private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
+    private static final byte[] MODE_ID_DATA =
+            new byte[]{2, 1, 30, 2, 10, -16, 6, 22, 44, -2, -86, -69, -52};
+    private static final byte[] BLE_ADDRESS = new byte[]{124, 4, 56, 60, 120, -29, -90};
+    private static final DataElement MODE_ID_ADDRESS_ELEMENT =
+            new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA);
+    private static final DataElement BLE_ADDRESS_ELEMENT =
+            new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
+
+    private static final byte[] IDENTITY =
+            new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+    private static final int MEDIUM_TYPE_BLE = 0;
+    private static final byte[] SALT = {2, 3};
+    private static final int PRESENCE_ACTION_1 = 1;
+    private static final int PRESENCE_ACTION_2 = 2;
+
+    private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+    private static final byte[] PUBLIC_KEY =
+            new byte[] {
+                    48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
+                    66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
+                    -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
+                    -65, 64, 91, -109, -45, -35, -56, 55, -79, 47, -85, 27, -96, -119, -82, -80,
+                    123, 41, -119, -25, 1, -112, 112
+            };
+    private static final byte[] ENCRYPTED_METADATA_BYTES =
+            new byte[] {
+                    -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
+                    -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
+                    88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
+                    -118, -61, -37, -104, 60, 105, 115, 1, 56, -89, -107, -45, -116, -1, -25, 84,
+                    -19, -128, 81, 11, 92, 77, -58, 82, 122, 123, 31, -87, -57, 70, 23, -81, 7, 2,
+                    -114, -83, 74, 124, -68, -98, 47, 91, 9, 48, -67, 41, -7, -97, 78, 66, -65, 58,
+                    -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
+            };
+    private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
+            new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+    private static final String DEVICE_NAME = "test_device";
+
+    private PresenceBroadcastRequest.Builder mBuilder;
+    private PrivateCredential mPrivateCredential;
+    private PublicCredential mPublicCredential;
+
+    @Before
+    public void setUp() {
+        mPrivateCredential =
+                new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .build();
+        mPublicCredential =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
+                        .build();
+        mBuilder =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        SALT, mPrivateCredential)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+                        .addAction(PRESENCE_ACTION_1)
+                        .addAction(PRESENCE_ACTION_2)
+                        .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
+                        .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+    }
+
+    @Test
+    public void test_createFromRequest() {
+        ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+                mBuilder.build());
+
+        assertThat(originalAdvertisement.getActions())
+                .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(originalAdvertisement.getLength()).isEqualTo(66);
+        assertThat(originalAdvertisement.getVersion()).isEqualTo(
+                BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+        assertThat(originalAdvertisement.getDataElements())
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+    }
+
+    @Test
+    public void test_createFromRequest_encodeAndDecode() {
+        ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+                mBuilder.build());
+
+        byte[] generatedBytes = originalAdvertisement.toBytes();
+
+        ExtendedAdvertisement newAdvertisement =
+                ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
+
+        assertThat(newAdvertisement.getActions())
+                .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+        assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(newAdvertisement.getLength()).isEqualTo(66);
+        assertThat(newAdvertisement.getVersion()).isEqualTo(
+                BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
+        assertThat(newAdvertisement.getDataElements())
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+    }
+
+    @Test
+    public void test_createFromRequest_invalidParameter() {
+        // invalid version
+        mBuilder.setVersion(BroadcastRequest.PRESENCE_VERSION_V0);
+        assertThat(ExtendedAdvertisement.createFromRequest(mBuilder.build())).isNull();
+
+        // invalid salt
+        PresenceBroadcastRequest.Builder builder =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        new byte[]{1, 2, 3}, mPrivateCredential)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+                        .addAction(PRESENCE_ACTION_1);
+        assertThat(ExtendedAdvertisement.createFromRequest(builder.build())).isNull();
+
+        // invalid identity
+        PrivateCredential privateCredential =
+                new PrivateCredential.Builder(SECRET_ID,
+                        AUTHENTICITY_KEY, new byte[]{1, 2, 3, 4}, DEVICE_NAME)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .build();
+        PresenceBroadcastRequest.Builder builder2 =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        new byte[]{1, 2, 3}, privateCredential)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+                        .addAction(PRESENCE_ACTION_1);
+        assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
+
+        // empty action
+        PresenceBroadcastRequest.Builder builder3 =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        SALT, mPrivateCredential)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
+    }
+
+    @Test
+    public void test_toBytes() {
+        ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+        assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
+    }
+
+    @Test
+    public void test_fromBytes() {
+        byte[] originalBytes = getExtendedAdvertisementByteArray();
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
+
+        assertThat(adv.getActions())
+                .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+        assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(adv.getLength()).isEqualTo(66);
+        assertThat(adv.getVersion()).isEqualTo(
+                BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(adv.getSalt()).isEqualTo(SALT);
+        assertThat(adv.getDataElements())
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+    }
+
+    @Test
+    public void test_toString() {
+        ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+        assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
+                + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
+                + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+                + " actions: [1, 2]>");
+    }
+
+    @Test
+    public void test_getDataElements_accordingToType() {
+        ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+        List<DataElement> dataElements = new ArrayList<>();
+
+        dataElements.add(BLE_ADDRESS_ELEMENT);
+        assertThat(adv.getDataElements(DATA_TYPE_BLE_ADDRESS)).isEqualTo(dataElements);
+        assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
+    }
+
+    private static byte[] getExtendedAdvertisementByteArray() {
+        ByteBuffer buffer = ByteBuffer.allocate(66);
+        buffer.put((byte) 0b00100000); // Header V1
+        buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+        // Salt data
+        buffer.put(SALT);
+        // Identity header: length 16, type 1 (private identity)
+        buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
+        // Identity data
+        buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+
+        ByteBuffer deBuffer = ByteBuffer.allocate(28);
+        // Action1 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action1 data
+        deBuffer.put((byte) PRESENCE_ACTION_1);
+        // Action2 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action2 data
+        deBuffer.put((byte) PRESENCE_ACTION_2);
+        // Ble address header: length 7, type 102
+        deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
+        // Ble address data
+        deBuffer.put(BLE_ADDRESS);
+        // model id header: length 13, type 7
+        deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
+        // model id data
+        deBuffer.put(MODE_ID_DATA);
+
+        byte[] data = deBuffer.array();
+        CryptorImpV1 cryptor = CryptorImpV1.getInstance();
+        buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
+        buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+
+        return buffer.array();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
new file mode 100644
index 0000000..c4fccf7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link ExtendedAdvertisementUtils}.
+ */
+public class ExtendedAdvertisementUtilsTest {
+    private static final byte[] ADVERTISEMENT1 = new byte[]{0b00100000, 12, 34, 78, 10};
+    private static final byte[] ADVERTISEMENT2 = new byte[]{0b00100000, 0b00100011, 34, 78,
+            (byte) 0b10010000, (byte) 0b00000100,
+            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+    private static final int DATA_TYPE_SALT = 3;
+    private static final int DATA_TYPE_PRIVATE_IDENTITY = 4;
+
+    @Test
+    public void test_constructHeader() {
+        assertThat(ExtendedAdvertisementUtils.constructHeader(1)).isEqualTo(0b100000);
+        assertThat(ExtendedAdvertisementUtils.constructHeader(0)).isEqualTo(0);
+        assertThat(ExtendedAdvertisementUtils.constructHeader(6)).isEqualTo((byte) 0b11000000);
+    }
+
+    @Test
+    public void test_getVersion() {
+        assertThat(ExtendedAdvertisementUtils.getVersion(ADVERTISEMENT1)).isEqualTo(1);
+        byte[] adv = new byte[]{(byte) 0b10111100, 9, 19, 90, 23};
+        assertThat(ExtendedAdvertisementUtils.getVersion(adv)).isEqualTo(5);
+        byte[] adv2 = new byte[]{(byte) 0b10011111, 9, 19, 90, 23};
+        assertThat(ExtendedAdvertisementUtils.getVersion(adv2)).isEqualTo(4);
+    }
+
+    @Test
+    public void test_getDataElementHeader_salt() {
+        byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 1);
+        DataElementHeader header = DataElementHeader.fromBytes(
+                BroadcastRequest.PRESENCE_VERSION_V1, saltHeaderArray);
+        assertThat(header.getDataType()).isEqualTo(DATA_TYPE_SALT);
+        assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH);
+    }
+
+    @Test
+    public void test_getDataElementHeader_identity() {
+        byte[] identityHeaderArray =
+                ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 4);
+        DataElementHeader header = DataElementHeader.fromBytes(BroadcastRequest.PRESENCE_VERSION_V1,
+                identityHeaderArray);
+        assertThat(header.getDataType()).isEqualTo(DATA_TYPE_PRIVATE_IDENTITY);
+        assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH);
+    }
+
+    @Test
+    public void test_constructDataElement_salt() {
+        DataElement salt = new DataElement(DATA_TYPE_SALT, new byte[]{13, 14});
+        byte[] saltArray = ExtendedAdvertisementUtils.convertDataElementToBytes(salt);
+        // Data length and salt header length.
+        assertThat(saltArray.length).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH + 1);
+        // Header
+        assertThat(saltArray[0]).isEqualTo((byte) 0b00100011);
+        // Data
+        assertThat(saltArray[1]).isEqualTo((byte) 13);
+        assertThat(saltArray[2]).isEqualTo((byte) 14);
+    }
+
+    @Test
+    public void test_constructDataElement_privateIdentity() {
+        byte[] identityData = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+        DataElement identity = new DataElement(DATA_TYPE_PRIVATE_IDENTITY, identityData);
+        byte[] identityArray = ExtendedAdvertisementUtils.convertDataElementToBytes(identity);
+        // Data length and identity header length.
+        assertThat(identityArray.length).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH + 2);
+        // 1st header byte
+        assertThat(identityArray[0]).isEqualTo((byte) 0b10010000);
+        // 2st header byte
+        assertThat(identityArray[1]).isEqualTo((byte) 0b00000100);
+        // Data
+        assertThat(Arrays.copyOfRange(identityArray, 2, identityArray.length))
+                .isEqualTo(identityData);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
index 5e0ccbe..8e3e068 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -75,6 +75,15 @@
         assertThat(originalAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V0);
         assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+        assertThat(originalAdvertisement.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(originalAdvertisement.toString())
+                .isEqualTo("FastAdvertisement:<VERSION: 0, length: 19,"
+                        + " ltvFieldCount: 4,"
+                        + " identityType: 1,"
+                        + " identity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],"
+                        + " salt: [2, 3],"
+                        + " actions: [123],"
+                        + " txPower: 4");
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
index 39cab94..856c1a8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
 import android.nearby.PresenceCredential;
 import android.nearby.PresenceDevice;
 import android.nearby.PresenceScanFilter;
@@ -28,12 +30,15 @@
 import org.junit.Before;
 import org.junit.Test;
 
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Unit tests for {@link PresenceDiscoveryResult}.
  */
 public class PresenceDiscoveryResultTest {
+    private static final int DATA_TYPE_ACCOUNT_KEY = 9;
+    private static final int DATA_TYPE_INTENT = 6;
     private static final int PRESENCE_ACTION = 123;
     private static final int TX_POWER = -1;
     private static final int RSSI = -41;
@@ -43,6 +48,8 @@
     private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
     private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
     private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+    private static final byte[] META_DATA_ENCRYPTION_KEY =
+            new byte[] {-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
 
     private PresenceDiscoveryResult.Builder mBuilder;
     private PublicCredential mCredential;
@@ -59,18 +66,68 @@
                 .setSalt(SALT)
                 .setTxPower(TX_POWER)
                 .setRssi(RSSI)
+                .setEncryptedIdentityTag(METADATA_ENCRYPTION_KEY_TAG)
                 .addPresenceAction(PRESENCE_ACTION);
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testToDevice() {
-        PresenceDiscoveryResult discoveryResult = mBuilder.build();
-        PresenceDevice presenceDevice = discoveryResult.toPresenceDevice();
+    public void testFromDevice() {
+        NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+        builder.setTxPower(TX_POWER)
+                .setRssi(RSSI)
+                .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+                .setSalt(SALT)
+                .setPublicCredential(mCredential);
 
-        assertThat(presenceDevice.getRssi()).isEqualTo(RSSI);
-        assertThat(Arrays.equals(presenceDevice.getSalt(), SALT)).isTrue();
-        assertThat(Arrays.equals(presenceDevice.getSecretId(), SECRET_ID)).isTrue();
+        PresenceDiscoveryResult discoveryResult =
+                PresenceDiscoveryResult.fromDevice(builder.build());
+        PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+                .setMaxPathLoss(80)
+                .addCredential(mCredential)
+                .build();
+
+        assertThat(discoveryResult.matches(scanFilter)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testFromDevice_presenceDeviceAvailable() {
+        NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+        PresenceDevice presenceDevice =
+                new PresenceDevice.Builder("123", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY)
+                        .addExtendedProperty(new DataElement(
+                                DATA_TYPE_INTENT, new byte[]{(byte) PRESENCE_ACTION}))
+                        .build();
+        builder.setTxPower(TX_POWER)
+                .setRssi(RSSI)
+                .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+                .setPresenceDevice(presenceDevice)
+                .setPublicCredential(mCredential);
+
+        PresenceDiscoveryResult discoveryResult =
+                PresenceDiscoveryResult.fromDevice(builder.build());
+        PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+                .setMaxPathLoss(80)
+                .addPresenceAction(PRESENCE_ACTION)
+                .addCredential(mCredential)
+                .build();
+
+        assertThat(discoveryResult.matches(scanFilter)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testAccountMatches() {
+        DataElement accountKey = new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4});
+        mBuilder.addExtendedProperties(List.of(accountKey));
+        PresenceDiscoveryResult discoveryResult = mBuilder.build();
+
+        List<DataElement> extendedProperties = new ArrayList<>();
+        extendedProperties.add(new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4}));
+        extendedProperties.add(new DataElement(DATA_TYPE_INTENT,
+                new byte[]{(byte) PRESENCE_ACTION}));
+        assertThat(discoveryResult.accountKeyMatches(extendedProperties)).isTrue();
     }
 
     @Test
@@ -86,4 +143,24 @@
         assertThat(discoveryResult.matches(scanFilter)).isTrue();
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_notMatches() {
+        PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder()
+                .setPublicCredential(mCredential)
+                .setSalt(SALT)
+                .setTxPower(TX_POWER)
+                .setRssi(RSSI)
+                .setEncryptedIdentityTag(new byte[]{5, 4, 3, 2, 1})
+                .addPresenceAction(PRESENCE_ACTION);
+
+        PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+                .setMaxPathLoss(80)
+                .addPresenceAction(PRESENCE_ACTION)
+                .addCredential(mCredential)
+                .build();
+
+        PresenceDiscoveryResult discoveryResult = builder.build();
+        assertThat(discoveryResult.matches(scanFilter)).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
new file mode 100644
index 0000000..ca4f077
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.presence;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.PresenceDevice;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class PresenceManagerTest {
+    private static final byte[] IDENTITY =
+            new byte[] {1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+    private static final byte[] SALT = {2, 3};
+    private static final byte[] SECRET_ID =
+            new byte[] {-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+
+    @Mock private Context mContext;
+    private PresenceManager mPresenceManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mPresenceManager = new PresenceManager(mContext);
+        when(mContext.getContentResolver())
+                .thenReturn(InstrumentationRegistry.getInstrumentation()
+                        .getContext().getContentResolver());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testInit() {
+        mPresenceManager.initiate();
+
+        verify(mContext, times(1)).registerReceiver(any(), any());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testDeviceStatusUpdated() {
+        DataElement dataElement1 = new DataElement(1, new byte[] {1, 2});
+        DataElement dataElement2 = new DataElement(2, new byte[] {-1, -2, 3, 4, 5, 6, 7, 8, 9});
+
+        PresenceDevice presenceDevice =
+                new PresenceDevice.Builder(/* deviceId= */ "deviceId", SALT, SECRET_ID, IDENTITY)
+                        .addExtendedProperty(dataElement1)
+                        .addExtendedProperty(dataElement2)
+                        .build();
+
+        mPresenceManager.mScanCallback.onDiscovered(presenceDevice);
+        mPresenceManager.mScanCallback.onUpdated(presenceDevice);
+        mPresenceManager.mScanCallback.onLost(presenceDevice);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index d06a785..05b556b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.provider;
 
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -24,12 +25,14 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSetCallback;
 import android.content.Context;
+import android.hardware.location.ContextHubManager;
 import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
 
 import androidx.test.core.app.ApplicationProvider;
 
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
 import com.android.server.nearby.injector.Injector;
 
 import com.google.common.util.concurrent.MoreExecutors;
@@ -59,9 +62,10 @@
     }
 
     @Test
-    public void testOnStatus_success() {
+    public void testOnStatus_success_fastAdv() {
         byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
-        mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+        mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+                advertiseBytes, mBroadcastListener);
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
         mBleBroadcastProvider.onStartSuccess(settings);
@@ -69,15 +73,47 @@
     }
 
     @Test
-    public void testOnStatus_failure() {
+    public void testOnStatus_success_extendedAdv() {
         byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
-        mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+        mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+                advertiseBytes, mBroadcastListener);
+
+        // advertising set can not be mocked, so we will allow nulls
+        mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+                AdvertisingSetCallback.ADVERTISE_SUCCESS);
+        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+    }
+
+    @Test
+    public void testOnStatus_failure_fastAdv() {
+        byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+        mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+                advertiseBytes, mBroadcastListener);
 
         mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
         verify(mBroadcastListener, times(1))
                 .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
     }
 
+    @Test
+    public void testOnStatus_failure_extendedAdv() {
+        byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+        mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+                advertiseBytes, mBroadcastListener);
+
+        // advertising set can not be mocked, so we will allow nulls
+        mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+                AdvertisingSetCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+        // Can be additional failure if the test device does not support LE Extended Advertising.
+        verify(mBroadcastListener, atLeastOnce())
+                .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+    }
+
+    @Test
+    public void testStop() {
+        mBleBroadcastProvider.stop();
+    }
+
     private static class TestInjector implements Injector {
 
         @Override
@@ -88,7 +124,7 @@
         }
 
         @Override
-        public ContextHubManagerAdapter getContextHubManagerAdapter() {
+        public ContextHubManager getContextHubManager() {
             return null;
         }
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index 902cc33..2d8bd63 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -17,6 +17,9 @@
 package com.android.server.nearby.provider;
 
 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+import static android.nearby.ScanCallback.ERROR_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.times;
@@ -30,10 +33,13 @@
 import android.bluetooth.le.ScanRecord;
 import android.bluetooth.le.ScanResult;
 import android.content.Context;
+import android.hardware.location.ContextHubManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
 import com.android.server.nearby.injector.Injector;
 
 import org.junit.Before;
@@ -42,6 +48,8 @@
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
 
 public final class BleDiscoveryProviderTest {
 
@@ -49,6 +57,8 @@
     private BleDiscoveryProvider mBleDiscoveryProvider;
     @Mock
     private AbstractDiscoveryProvider.Listener mListener;
+//    @Mock
+//    private BluetoothAdapter mBluetoothAdapter;
 
     @Before
     public void setup() {
@@ -61,7 +71,7 @@
     }
 
     @Test
-    public void test_callback() throws InterruptedException {
+    public void test_callback_found() throws InterruptedException {
         mBleDiscoveryProvider.getController().setListener(mListener);
         mBleDiscoveryProvider.onStart();
         mBleDiscoveryProvider.getScanCallback()
@@ -73,11 +83,39 @@
     }
 
     @Test
+    public void test_callback_failed() throws InterruptedException {
+        mBleDiscoveryProvider.getController().setListener(mListener);
+        mBleDiscoveryProvider.onStart();
+        mBleDiscoveryProvider.getScanCallback().onScanFailed(1);
+
+
+        // Wait for callback to be invoked
+        Thread.sleep(500);
+        verify(mListener, times(1)).onError(ERROR_UNKNOWN);
+    }
+
+    @Test
     public void test_stopScan() {
         mBleDiscoveryProvider.onStart();
         mBleDiscoveryProvider.onStop();
     }
 
+    @Test
+    public void test_stopScan_filersReset() {
+        List<ScanFilter> filterList = new ArrayList<>();
+        filterList.add(getSanFilter());
+
+        mBleDiscoveryProvider.getController().setProviderScanFilters(filterList);
+        mBleDiscoveryProvider.onStart();
+        mBleDiscoveryProvider.onStop();
+        assertThat(mBleDiscoveryProvider.getFiltersLocked()).isNull();
+    }
+
+    @Test
+    public void testInvalidateScanMode() {
+        mBleDiscoveryProvider.invalidateScanMode();
+    }
+
     private class TestInjector implements Injector {
         @Override
         public BluetoothAdapter getBluetoothAdapter() {
@@ -85,7 +123,7 @@
         }
 
         @Override
-        public ContextHubManagerAdapter getContextHubManagerAdapter() {
+        public ContextHubManager getContextHubManager() {
             return null;
         }
 
@@ -125,4 +163,22 @@
             return null;
         }
     }
+
+    private static PresenceScanFilter getSanFilter() {
+        return new PresenceScanFilter.Builder()
+                .setMaxPathLoss(70)
+                .addCredential(getPublicCredential())
+                .addPresenceAction(124)
+                .build();
+    }
+
+    private static PublicCredential getPublicCredential() {
+        return new PublicCredential.Builder(
+                new byte[]{1, 2},
+                new byte[]{1, 2},
+                new byte[]{1, 2},
+                new byte[]{1, 2},
+                new byte[]{1, 2})
+                .build();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
index 1b29b52..ce479c8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -16,18 +16,33 @@
 
 package com.android.server.nearby.provider;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+import static com.android.server.nearby.provider.ChreCommunication.INVALID_NANO_APP_VERSION;
+
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.hardware.location.ContextHubClient;
 import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
 import android.hardware.location.ContextHubTransaction;
 import android.hardware.location.NanoAppMessage;
 import android.hardware.location.NanoAppState;
+import android.os.Build;
+import android.provider.DeviceConfig;
 
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 
 import org.junit.Before;
@@ -42,8 +57,12 @@
 import java.util.concurrent.Executor;
 
 public class ChreCommunicationTest {
+    private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+    private static final int APP_VERSION = 1;
+
     @Mock Injector mInjector;
-    @Mock ContextHubManagerAdapter mManager;
+    @Mock Context mContext;
+    @Mock ContextHubManager mManager;
     @Mock ContextHubTransaction<List<NanoAppState>> mTransaction;
     @Mock ContextHubTransaction.Response<List<NanoAppState>> mTransactionResponse;
     @Mock ContextHubClient mClient;
@@ -56,38 +75,78 @@
 
     @Before
     public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+        DeviceConfig.setProperty(
+                NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "1", false);
+
         MockitoAnnotations.initMocks(this);
-        when(mInjector.getContextHubManagerAdapter()).thenReturn(mManager);
+        when(mInjector.getContextHubManager()).thenReturn(mManager);
         when(mManager.getContextHubs()).thenReturn(Collections.singletonList(new ContextHubInfo()));
         when(mManager.queryNanoApps(any())).thenReturn(mTransaction);
-        when(mManager.createClient(any(), any(), any())).thenReturn(mClient);
+        when(mManager.createClient(any(), any(), any(), any())).thenReturn(mClient);
         when(mTransactionResponse.getResult()).thenReturn(ContextHubTransaction.RESULT_SUCCESS);
         when(mTransactionResponse.getContents())
                 .thenReturn(
                         Collections.singletonList(
-                                new NanoAppState(ChreDiscoveryProvider.NANOAPP_ID, 1, true)));
+                                new NanoAppState(
+                                        ChreDiscoveryProvider.NANOAPP_ID,
+                                        APP_VERSION,
+                                        true)));
 
-        mChreCommunication = new ChreCommunication(mInjector, new InlineExecutor());
+        mChreCommunication = new ChreCommunication(mInjector, mContext, new InlineExecutor());
+    }
+
+    @Test
+    public void testStart() {
         mChreCommunication.start(
                 mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
 
         verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
         mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
-    }
-
-    @Test
-    public void testStart() {
         verify(mChreCallback).started(true);
     }
 
     @Test
     public void testStop() {
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+
+        verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+        mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
         mChreCommunication.stop();
         verify(mClient).close();
     }
 
     @Test
+    public void testNotReachMinVersion() {
+        DeviceConfig.setProperty(NAMESPACE, NEARBY_MAINLINE_NANO_APP_MIN_VERSION, "3", false);
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+        verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+        mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+        verify(mChreCallback).started(false);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void test_getNanoVersion() {
+        assertThat(mChreCommunication.queryNanoAppVersion()).isEqualTo(INVALID_NANO_APP_VERSION);
+
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+        verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+        mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+
+        assertThat(mChreCommunication.queryNanoAppVersion()).isEqualTo(APP_VERSION);
+    }
+
+    @Test
     public void testSendMessageToNanApp() {
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+        verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+        mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
         NanoAppMessage message =
                 NanoAppMessage.createMessageToNanoApp(
                         ChreDiscoveryProvider.NANOAPP_ID,
@@ -99,6 +158,8 @@
 
     @Test
     public void testOnMessageFromNanoApp() {
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
         NanoAppMessage message =
                 NanoAppMessage.createMessageToNanoApp(
                         ChreDiscoveryProvider.NANOAPP_ID,
@@ -109,13 +170,60 @@
     }
 
     @Test
+    public void testContextHubTransactionResultToString() {
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_SUCCESS))
+                .isEqualTo("RESULT_SUCCESS");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_UNKNOWN))
+                .isEqualTo("RESULT_FAILED_UNKNOWN");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_BAD_PARAMS))
+                .isEqualTo("RESULT_FAILED_BAD_PARAMS");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_UNINITIALIZED))
+                .isEqualTo("RESULT_FAILED_UNINITIALIZED");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_BUSY))
+                .isEqualTo("RESULT_FAILED_BUSY");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_AT_HUB))
+                .isEqualTo("RESULT_FAILED_AT_HUB");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_TIMEOUT))
+                .isEqualTo("RESULT_FAILED_TIMEOUT");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE))
+                .isEqualTo("RESULT_FAILED_SERVICE_INTERNAL_FAILURE");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE))
+                .isEqualTo("RESULT_FAILED_HAL_UNAVAILABLE");
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(9))
+                .isEqualTo("UNKNOWN_RESULT value=9");
+    }
+
+    @Test
     public void testOnHubReset() {
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
         mChreCommunication.onHubReset(mClient);
         verify(mChreCallback).onHubReset();
     }
 
     @Test
     public void testOnNanoAppLoaded() {
+        mChreCommunication.start(
+                mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
         mChreCommunication.onNanoAppLoaded(mClient, ChreDiscoveryProvider.NANOAPP_ID);
         verify(mChreCallback).onNanoAppRestart(eq(ChreDiscoveryProvider.NANOAPP_ID));
     }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 7c0dd92..154441b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -16,16 +16,34 @@
 
 package com.android.server.nearby.provider;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_SUPPORT_TEST_APP;
+
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.location.NanoAppMessage;
-import android.nearby.ScanFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.OffloadCapability;
+import android.nearby.aidl.IOffloadCallback;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+
 import com.google.protobuf.ByteString;
 
 import org.junit.Before;
@@ -35,22 +53,39 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import service.proto.Blefilter;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
+import service.proto.Blefilter;
+
 public class ChreDiscoveryProviderTest {
     @Mock AbstractDiscoveryProvider.Listener mListener;
     @Mock ChreCommunication mChreCommunication;
+    @Mock IBinder mIBinder;
 
     @Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
+    @Captor ArgumentCaptor<NearbyDeviceParcelable> mNearbyDevice;
 
+    private static final String NAMESPACE = NearbyConfiguration.getNamespace();
+    private static final int DATA_TYPE_CONNECTION_STATUS_KEY = 10;
+    private static final int DATA_TYPE_BATTERY_KEY = 11;
+    private static final int DATA_TYPE_TX_POWER_KEY = 5;
+    private static final int DATA_TYPE_BLUETOOTH_ADDR_KEY = 101;
+    private static final int DATA_TYPE_FP_ACCOUNT_KEY = 9;
+    private static final int DATA_TYPE_BLE_SERVICE_DATA_KEY = 100;
+    private static final int DATA_TYPE_TEST_DE_BEGIN_KEY = 2147483520;
+    private static final int DATA_TYPE_TEST_DE_END_KEY = 2147483647;
+
+    private final Object mLock = new Object();
     private ChreDiscoveryProvider mChreDiscoveryProvider;
+    private OffloadCapability mOffloadCapability;
 
     @Before
     public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+
         MockitoAnnotations.initMocks(this);
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         mChreDiscoveryProvider =
@@ -59,13 +94,68 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testOnStart() {
-        List<ScanFilter> scanFilters = new ArrayList<>();
-        mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
-        mChreDiscoveryProvider.onStart();
+    public void testInit() {
+        mChreDiscoveryProvider.init();
         verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
         mChreCallbackCaptor.getValue().started(true);
-        verify(mChreCommunication).sendMessageToNanoApp(any());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void test_queryInvalidVersion() {
+        when(mChreCommunication.queryNanoAppVersion()).thenReturn(
+                (long) ChreCommunication.INVALID_NANO_APP_VERSION);
+        IOffloadCallback callback = new IOffloadCallback() {
+            @Override
+            public void onQueryComplete(OffloadCapability capability) throws RemoteException {
+                synchronized (mLock) {
+                    mOffloadCapability = capability;
+                    mLock.notify();
+                }
+            }
+
+            @Override
+            public IBinder asBinder() {
+                return mIBinder;
+            }
+        };
+        mChreDiscoveryProvider.queryOffloadCapability(callback);
+        OffloadCapability capability =
+                new OffloadCapability
+                        .Builder()
+                        .setFastPairSupported(false)
+                        .setVersion(ChreCommunication.INVALID_NANO_APP_VERSION)
+                        .build();
+        assertThat(mOffloadCapability).isEqualTo(capability);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void test_queryVersion() {
+        long version = 500L;
+        when(mChreCommunication.queryNanoAppVersion()).thenReturn(version);
+        IOffloadCallback callback = new IOffloadCallback() {
+            @Override
+            public void onQueryComplete(OffloadCapability capability) throws RemoteException {
+                synchronized (mLock) {
+                    mOffloadCapability = capability;
+                    mLock.notify();
+                }
+            }
+
+            @Override
+            public IBinder asBinder() {
+                return mIBinder;
+            }
+        };
+        mChreDiscoveryProvider.queryOffloadCapability(callback);
+        OffloadCapability capability =
+                new OffloadCapability
+                        .Builder()
+                        .setFastPairSupported(true)
+                        .setVersion(version)
+                        .build();
+        assertThat(mOffloadCapability).isEqualTo(capability);
     }
 
     @Test
@@ -93,12 +183,220 @@
                         ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
                         results.toByteArray());
         mChreDiscoveryProvider.getController().setListener(mListener);
+        mChreDiscoveryProvider.init();
         mChreDiscoveryProvider.onStart();
         verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
         mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
         verify(mListener).onNearbyDeviceDiscovered(any());
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testOnNearbyDeviceDiscoveredWithDataElements() {
+        // Disables the setting of test app support
+        boolean isSupportedTestApp = getDeviceConfigBoolean(
+                NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+        if (isSupportedTestApp) {
+            DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+        }
+        assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
+
+        final byte [] connectionStatus = new byte[] {1, 2, 3};
+        final byte [] batteryStatus = new byte[] {4, 5, 6};
+        final byte [] txPower = new byte[] {2};
+        final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+        final byte [] fastPairAccountKey = new byte[16];
+        // First byte is length of service data, padding zeros should be thrown away.
+        final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+        final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+        expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+                connectionStatus));
+        expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+        expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+
+        Blefilter.PublicCredential credential =
+                Blefilter.PublicCredential.newBuilder()
+                        .setSecretId(ByteString.copyFrom(new byte[] {1}))
+                        .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+                        .setPublicKey(ByteString.copyFrom(new byte[3]))
+                        .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+                        .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+                        .build();
+        Blefilter.BleFilterResult result =
+                Blefilter.BleFilterResult.newBuilder()
+                        .setTxPower(2)
+                        .setRssi(1)
+                        .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+                        .setBleServiceData(ByteString.copyFrom(bleServiceData))
+                        .setPublicCredential(credential)
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_CONNECTION_STATUS_KEY)
+                                .setValue(ByteString.copyFrom(connectionStatus))
+                                .setValueLength(connectionStatus.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_BATTERY_KEY)
+                                .setValue(ByteString.copyFrom(batteryStatus))
+                                .setValueLength(batteryStatus.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_FP_ACCOUNT_KEY)
+                                .setValue(ByteString.copyFrom(fastPairAccountKey))
+                                .setValueLength(fastPairAccountKey.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_TEST_DE_BEGIN_KEY)
+                                .setValue(ByteString.copyFrom(testData))
+                                .setValueLength(testData.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_TEST_DE_END_KEY)
+                                .setValue(ByteString.copyFrom(testData))
+                                .setValueLength(testData.length)
+                        )
+                        .build();
+        Blefilter.BleFilterResults results =
+                Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+        NanoAppMessage chre_message =
+                NanoAppMessage.createMessageToNanoApp(
+                        ChreDiscoveryProvider.NANOAPP_ID,
+                        ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+                        results.toByteArray());
+        mChreDiscoveryProvider.getController().setListener(mListener);
+        mChreDiscoveryProvider.init();
+        mChreDiscoveryProvider.onStart();
+        verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+        mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+        verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+        List<DataElement> extendedProperties = PresenceDiscoveryResult
+                .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+        assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+        // Reverts the setting of test app support
+        if (isSupportedTestApp) {
+            DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
+            assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+        // Enables the setting of test app support
+        boolean isSupportedTestApp = getDeviceConfigBoolean(
+                NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
+        if (!isSupportedTestApp) {
+            DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
+        }
+        assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
+
+        final byte [] connectionStatus = new byte[] {1, 2, 3};
+        final byte [] batteryStatus = new byte[] {4, 5, 6};
+        final byte [] txPower = new byte[] {2};
+        final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+        final byte [] fastPairAccountKey = new byte[16];
+        // First byte is length of service data, padding zeros should be thrown away.
+        final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+        final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+        final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+        expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+                connectionStatus));
+        expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+        expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_TEST_DE_BEGIN_KEY, testData));
+        expectedExtendedProperties.add(
+                new DataElement(DATA_TYPE_TEST_DE_END_KEY, testData));
+
+        Blefilter.PublicCredential credential =
+                Blefilter.PublicCredential.newBuilder()
+                        .setSecretId(ByteString.copyFrom(new byte[] {1}))
+                        .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+                        .setPublicKey(ByteString.copyFrom(new byte[3]))
+                        .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+                        .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+                        .build();
+        Blefilter.BleFilterResult result =
+                Blefilter.BleFilterResult.newBuilder()
+                        .setTxPower(2)
+                        .setRssi(1)
+                        .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+                        .setBleServiceData(ByteString.copyFrom(bleServiceData))
+                        .setPublicCredential(credential)
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_CONNECTION_STATUS_KEY)
+                                .setValue(ByteString.copyFrom(connectionStatus))
+                                .setValueLength(connectionStatus.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_BATTERY_KEY)
+                                .setValue(ByteString.copyFrom(batteryStatus))
+                                .setValueLength(batteryStatus.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_FP_ACCOUNT_KEY)
+                                .setValue(ByteString.copyFrom(fastPairAccountKey))
+                                .setValueLength(fastPairAccountKey.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_TEST_DE_BEGIN_KEY)
+                                .setValue(ByteString.copyFrom(testData))
+                                .setValueLength(testData.length)
+                        )
+                        .addDataElement(Blefilter.DataElement.newBuilder()
+                                .setKey(DATA_TYPE_TEST_DE_END_KEY)
+                                .setValue(ByteString.copyFrom(testData))
+                                .setValueLength(testData.length)
+                        )
+                        .build();
+        Blefilter.BleFilterResults results =
+                Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+        NanoAppMessage chre_message =
+                NanoAppMessage.createMessageToNanoApp(
+                        ChreDiscoveryProvider.NANOAPP_ID,
+                        ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+                        results.toByteArray());
+        mChreDiscoveryProvider.getController().setListener(mListener);
+        mChreDiscoveryProvider.init();
+        mChreDiscoveryProvider.onStart();
+        verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+        mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+        verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+        List<DataElement> extendedProperties = PresenceDiscoveryResult
+                .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+        assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+        // Reverts the setting of test app support
+        if (!isSupportedTestApp) {
+            DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "false", false);
+            assertThat(new NearbyConfiguration().isTestAppSupported()).isFalse();
+        }
+    }
+
+    private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+        final String value = getDeviceConfigProperty(name);
+        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    private String getDeviceConfigProperty(String name) {
+        return DeviceConfig.getProperty(NAMESPACE, name);
+    }
+
     private static class InLineExecutor implements Executor {
         @Override
         public void execute(Runnable command) {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
new file mode 100644
index 0000000..a759baf
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+public final class ArrayUtilsTest {
+
+    private static final byte[] BYTES_ONE = new byte[] {7, 9};
+    private static final byte[] BYTES_TWO = new byte[] {8};
+    private static final byte[] BYTES_EMPTY = new byte[] {};
+    private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysNoInput() {
+        assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysOneEmptyArray() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysOneNonEmptyArray() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysMultipleNonEmptyArrays() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysMultipleArrays() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
+                .isEqualTo(BYTES_ALL);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testIsEmptyNull_returnsTrue() {
+        assertThat(ArrayUtils.isEmpty(null)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testIsEmpty_returnsTrue() {
+        assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testIsEmpty_returnsFalse() {
+        assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
index 1a22412..71ade2a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
@@ -93,4 +93,9 @@
         assertThat(BroadcastPermissions.getPermissionLevel(mMockContext, UID, PID))
                 .isEqualTo(PERMISSION_BLUETOOTH_ADVERTISE);
     }
+
+    @Test
+    public void test_enforceBroadcastPermission() {
+        BroadcastPermissions.enforceBroadcastPermission(mMockContext, mCallerIdentity);
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
new file mode 100644
index 0000000..f0294fc
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class CryptorImpIdentityV1Test {
+    private static final String TAG = "CryptorImpIdentityV1Test";
+    private static final byte[] SALT = new byte[] {102, 22};
+    private static final byte[] DATA =
+            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+    @Test
+    public void test_encrypt_decrypt() {
+        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+        assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
+    }
+
+    @Test
+    public void test_encryption() {
+        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+        // for debugging
+        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+        assertThat(encryptedData).isEqualTo(getEncryptedData());
+    }
+
+    @Test
+    public void test_decryption() {
+        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+        byte[] decryptedData =
+                identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+        // for debugging
+        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+        assertThat(decryptedData).isEqualTo(DATA);
+    }
+
+    @Test
+    public void generateHmacTag() {
+        CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
+        byte[] generatedTag = identityCryptor.sign(DATA);
+        byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
+        assertThat(generatedTag).isEqualTo(expectedTag);
+    }
+
+    private static byte[] getEncryptedData() {
+        return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
new file mode 100644
index 0000000..3ca2575
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link CryptorImpV1}
+ */
+public final class CryptorImpV1Test {
+    private static final String TAG = "CryptorImpV1Test";
+    private static final byte[] SALT = new byte[] {102, 22};
+    private static final byte[] DATA =
+            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+    @Test
+    public void test_encryption() {
+        Cryptor v1Cryptor = CryptorImpV1.getInstance();
+        byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+        // for debugging
+        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+        assertThat(encryptedData).isEqualTo(getEncryptedData());
+    }
+
+    @Test
+    public void test_encryption_invalidInput() {
+        Cryptor v1Cryptor = CryptorImpV1.getInstance();
+        assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+    }
+
+    @Test
+    public void test_decryption() {
+        Cryptor v1Cryptor = CryptorImpV1.getInstance();
+        byte[] decryptedData =
+                v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+        // for debugging
+        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+        assertThat(decryptedData).isEqualTo(DATA);
+    }
+
+    @Test
+    public void test_decryption_invalidInput() {
+        Cryptor v1Cryptor = CryptorImpV1.getInstance();
+        assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+    }
+
+    @Test
+    public void generateSign() {
+        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+        byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
+        byte[] expectedTag = new byte[]{
+                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+        assertThat(generatedTag).isEqualTo(expectedTag);
+    }
+
+    @Test
+    public void test_verify() {
+        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+        byte[] expectedTag = new byte[]{
+                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+    }
+
+    @Test
+    public void test_generateHmacTag_sameResult() {
+        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+        assertThat(res1)
+                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+    }
+
+    @Test
+    public void test_generateHmacTag_nullData() {
+        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+    }
+
+    @Test
+    public void test_generateHmacTag_nullKey() {
+        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+    }
+
+    private static byte[] getEncryptedData() {
+        return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
new file mode 100644
index 0000000..ca612e3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link Cryptor}
+ */
+public final class CryptorTest {
+
+    private static final byte[] DATA =
+            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+    @Test
+    public void test_computeHkdf() {
+        int outputSize = 16;
+        byte[] res1 = Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize);
+        byte[] res2 = Cryptor.computeHkdf(DATA,
+                new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26},
+                outputSize);
+
+        assertThat(res1).hasLength(outputSize);
+        assertThat(res2).hasLength(outputSize);
+        assertThat(res1).isNotEqualTo(res2);
+        assertThat(res1)
+                .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+    }
+
+    @Test
+    public void test_computeHkdf_invalidInput() {
+        assertThat(Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, /* size= */ 256000))
+                .isNull();
+        assertThat(Cryptor.computeHkdf(DATA, new byte[0], /* size= */ 255))
+                .isNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
new file mode 100644
index 0000000..c29cb92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util.identity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CallerIdentityTest {
+    private static final int UID = 100;
+    private static final int PID = 10002;
+    private static final String PACKAGE_NAME = "package_name";
+    private static final String ATTRIBUTION_TAG = "attribution_tag";
+
+    @Test
+    public void testToString() {
+        CallerIdentity callerIdentity =
+                CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+        assertThat(callerIdentity.toString()).isEqualTo("100/package_name[attribution_tag]");
+        assertThat(callerIdentity.isSystemServer()).isFalse();
+    }
+
+    @Test
+    public void testHashCode() {
+        CallerIdentity callerIdentity =
+                CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+        CallerIdentity callerIdentity1 =
+                CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+        assertThat(callerIdentity.hashCode()).isEqualTo(callerIdentity1.hashCode());
+    }
+}
diff --git a/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/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml
index e753cb3..7e4dd42 100644
--- a/service/ServiceConnectivityResources/res/values-as/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-as/strings.xml
@@ -18,7 +18,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="connectivityResourcesAppLabel" msgid="2476261877900882974">"ছিষ্টেম সংযোগৰ উৎস"</string>
-    <string name="wifi_available_sign_in" msgid="8041178343789805553">"ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক"</string>
+    <string name="wifi_available_sign_in" msgid="8041178343789805553">"Wi-Fi নেটৱৰ্কত ছাইন ইন কৰক"</string>
     <string name="network_available_sign_in" msgid="2622520134876355561">"নেটৱৰ্কত ছাইন ইন কৰক"</string>
     <!-- no translation found for network_available_sign_in_detailed (8439369644697866359) -->
     <skip />
diff --git a/service/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/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6fc63df..94bdd64 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -108,6 +108,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.ActivityManager.UidFrozenStateChangedCallback;
@@ -3480,6 +3481,8 @@
         sendStickyBroadcast(makeGeneralIntent(info, bcastType));
     }
 
+    // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
+    @SuppressLint("NewApi")
     // TODO: Set the mini sdk to 31 and remove @TargetApi annotation when b/205923322 is addressed.
     @TargetApi(Build.VERSION_CODES.S)
     private void sendStickyBroadcast(Intent intent) {
@@ -9135,6 +9138,8 @@
         // else not handled
     }
 
+    // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
+    @SuppressLint("NewApi")
     private void sendIntent(PendingIntent pendingIntent, Intent intent) {
         mPendingIntentWakeLock.acquire();
         try {
diff --git a/tests/cts/hostside/AndroidTest.xml b/tests/cts/hostside/AndroidTest.xml
index e83e36a..90b7875 100644
--- a/tests/cts/hostside/AndroidTest.xml
+++ b/tests/cts/hostside/AndroidTest.xml
@@ -33,6 +33,11 @@
         <option name="teardown-command" value="cmd netpolicy stop-watching" />
     </target_preparer>
 
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="force-skip-system-props" value="true" />
+        <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+    </target_preparer>
+
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="CtsHostsideNetworkTests.jar" />
         <option name="runtime-hint" value="3m56s" />
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 2245382..470bb17 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -38,7 +38,6 @@
     srcs: ["src/**/*.java"],
     // Tag this module as a cts test artifact
     test_suites: [
-        "cts",
         "general-tests",
         "sts",
     ],
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index 89a55a7..5f321bb 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
@@ -61,6 +61,7 @@
 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;
 
@@ -719,10 +720,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/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 8c38b44..5331601 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -449,13 +449,19 @@
     // this function and using PollingCheck to try to make sure the uid has updated and reduce the
     // flaky rate.
     public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
-            boolean expectedResult) throws Exception {
-        PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)));
+            boolean expectedResult) {
+        final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
+                + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
+        PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
+                errMsg);
     }
 
-    public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult)
-            throws Exception {
-        PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)));
+    public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
+        final String errMsg = String.format(
+                "Unexpected result from isUidRestrictedOnMeteredNetworks; "
+                + "uid= " + uid + ", expected=" + expectedResult);
+        PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
+                errMsg);
     }
 
     public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
diff --git a/tests/cts/hostside/app2/AndroidManifest.xml b/tests/cts/hostside/app2/AndroidManifest.xml
index ff7240d..2c2d957 100644
--- a/tests/cts/hostside/app2/AndroidManifest.xml
+++ b/tests/cts/hostside/app2/AndroidManifest.xml
@@ -20,6 +20,7 @@
 
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -45,7 +46,11 @@
         <service android:name=".MyService"
              android:exported="true"/>
         <service android:name=".MyForegroundService"
-             android:exported="true"/>
+             android:foregroundServiceType="specialUse"
+             android:exported="true">
+            <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" 
+                      android:value="Connectivity" />
+        </service>
         <service android:name=".RemoteSocketFactoryService"
              android:exported="true"/>