Merge "Add unit tests" into tm-mainline-prod
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index b1144b4..78fca29 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -27,11 +27,11 @@
     host_supported: false,
     header_libs: [
         "bpf_headers",
-        "libnetdbinder_utils_headers",  // for XtBpfProgLocations.h
+        "netd_mainline_headers",
     ],
     export_header_lib_headers: [
         "bpf_headers",
-        "libnetdbinder_utils_headers",  // for XtBpfProgLocations.h
+        "netd_mainline_headers",
     ],
     export_include_dirs: ["."],
     cflags: [
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 4b3ba2f..dd9fb07 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -116,7 +116,7 @@
 /* -=-=-=-=- WARNING -=-=-=-=-
  *
  * These 4 xt_bpf program paths are actually defined by:
- *   //system/netd/include/binder_utils/XtBpfProgLocations.h
+ *   //system/netd/include/mainline/XtBpfProgLocations.h
  * which is intentionally a non-automerged location.
  *
  * They are *UNCHANGEABLE* due to being hard coded in Android T's netd binary
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 4ce6add..a2a1ac0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -200,6 +200,8 @@
     method public int describeContents();
     method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
     method @NonNull public String getInterfaceName();
+    method @Nullable public android.net.MacAddress getMacAddress();
+    method public int getMtu();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
   }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1b0578f..39cd7f3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -983,16 +983,6 @@
     public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
 
     /**
-     * Firewall chain used for lockdown VPN.
-     * Denylist of apps that cannot receive incoming packets except on loopback because they are
-     * subject to an always-on VPN which is not currently connected.
-     *
-     * @see #BLOCKED_REASON_LOCKDOWN_VPN
-     * @hide
-     */
-    public static final int FIREWALL_CHAIN_LOCKDOWN_VPN = 6;
-
-    /**
      * Firewall chain used for OEM-specific application restrictions.
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
@@ -1024,7 +1014,6 @@
         FIREWALL_CHAIN_POWERSAVE,
         FIREWALL_CHAIN_RESTRICTED,
         FIREWALL_CHAIN_LOW_POWER_STANDBY,
-        FIREWALL_CHAIN_LOCKDOWN_VPN,
         FIREWALL_CHAIN_OEM_DENY_1,
         FIREWALL_CHAIN_OEM_DENY_2,
         FIREWALL_CHAIN_OEM_DENY_3
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 27d13c1..d18b931 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,8 +29,10 @@
  */
 interface ITestNetworkManager
 {
-    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs,
-            in @nullable String iface);
+    TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+            in LinkAddress[] addrs, in @nullable String iface);
+
+    void setCarrierEnabled(in TestNetworkInterface iface, boolean enabled);
 
     void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
             in int[] administratorUids, in IBinder binder);
diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java
index 4449ff8..26200e1 100644
--- a/framework/src/android/net/TestNetworkInterface.java
+++ b/framework/src/android/net/TestNetworkInterface.java
@@ -16,22 +16,32 @@
 package android.net;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.util.Log;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
 
 /**
- * This class is used to return the interface name and fd of the test interface
+ * This class is used to return the interface name, fd, MAC, and MTU of the test interface
  *
  * @hide
  */
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 public final class TestNetworkInterface implements Parcelable {
+    private static final String TAG = "TestNetworkInterface";
+
     @NonNull
     private final ParcelFileDescriptor mFileDescriptor;
     @NonNull
     private final String mInterfaceName;
+    @Nullable
+    private final MacAddress mMacAddress;
+    private final int mMtu;
 
     @Override
     public int describeContents() {
@@ -40,18 +50,41 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel out, int flags) {
-        out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+        out.writeParcelable(mFileDescriptor, flags);
         out.writeString(mInterfaceName);
+        out.writeParcelable(mMacAddress, flags);
+        out.writeInt(mMtu);
     }
 
     public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) {
         mFileDescriptor = pfd;
         mInterfaceName = intf;
+
+        MacAddress macAddress = null;
+        int mtu = 1500;
+        try {
+            // This constructor is called by TestNetworkManager which runs inside the system server,
+            // which has permission to read the MacAddress.
+            NetworkInterface nif = NetworkInterface.getByName(mInterfaceName);
+
+            // getHardwareAddress() returns null for tun interfaces.
+            byte[] hardwareAddress = nif.getHardwareAddress();
+            if (hardwareAddress != null) {
+                macAddress = MacAddress.fromBytes(nif.getHardwareAddress());
+            }
+            mtu = nif.getMTU();
+        } catch (SocketException e) {
+            Log.e(TAG, "Failed to fetch MacAddress or MTU size from NetworkInterface", e);
+        }
+        mMacAddress = macAddress;
+        mMtu = mtu;
     }
 
     private TestNetworkInterface(@NonNull Parcel in) {
         mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
         mInterfaceName = in.readString();
+        mMacAddress = in.readParcelable(MacAddress.class.getClassLoader());
+        mMtu = in.readInt();
     }
 
     @NonNull
@@ -64,6 +97,15 @@
         return mInterfaceName;
     }
 
+    @Nullable
+    public MacAddress getMacAddress() {
+        return mMacAddress;
+    }
+
+    public int getMtu() {
+        return mMtu;
+    }
+
     @NonNull
     public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
             new Parcelable.Creator<TestNetworkInterface>() {
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 4e78823..7b18765 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -58,6 +58,7 @@
     private static final boolean TAP = false;
     private static final boolean TUN = true;
     private static final boolean BRING_UP = true;
+    private static final boolean CARRIER_UP = true;
     private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
 
     /** @hide */
@@ -166,7 +167,7 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr),
+            return mService.createInterface(TUN, CARRIER_UP, BRING_UP, linkAddrs.toArray(arr),
                     null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -185,7 +186,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createInterface(TAP, BRING_UP, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -204,7 +205,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -226,7 +227,43 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp, @NonNull String iface) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS, iface);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface with or without carrier for testing purposes.
+     *
+     * @param carrierUp whether the created interface has a carrier or not.
+     * @param bringUp whether to bring up the interface before returning it.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
+        try {
+            return mService.createInterface(TAP, carrierUp, bringUp, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable / disable carrier on TestNetworkInterface
+     *
+     * Note: TUNSETCARRIER is not supported until kernel version 5.0.
+     * TODO: add RequiresApi annotation.
+     *
+     * @param iface the interface to configure.
+     * @param enabled true to turn carrier on, false to turn carrier off.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        try {
+            mService.setCarrierEnabled(iface, enabled);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
index 961fe7a..5ce4488 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
@@ -107,15 +107,6 @@
     }
 
     /**
-     * Sets the store discovery item mac address.
-     */
-    public void setMacAddress(String address) {
-        mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().setMacAddress(address).build();
-
-        mFastPairCacheManager.saveDiscoveryItem(this);
-    }
-
-    /**
      * Checks if the item is expired. Expired items are those over getItemExpirationMillis() eg. 2
      * minutes
      */
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
index b840091..c6134f5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
@@ -64,16 +64,6 @@
     }
 
     /**
-     * Checks if the entry can be auto deleted from the cache
-     */
-    public boolean isDeletable(Cache.ServerResponseDbItem entry) {
-        if (!entry.getExpirable()) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
      * Save discovery item into database. Discovery item is item that discovered through Ble before
      * pairing success.
      */
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
index ccd7e5e..5fb05d5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
 import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -184,7 +185,8 @@
                 + maskBluetoothAddress(address));
     }
 
-    private static void optInFootprintsForInitialPairing(
+    @VisibleForTesting
+    static void optInFootprintsForInitialPairing(
             FootprintsDeviceManager footprints,
             DiscoveryItem item,
             byte[] accountKey,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
index 4fbf042..d6a846d 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.common.bloomfilter;
 
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -23,16 +25,20 @@
 
 import org.junit.Test;
 
+import java.util.Arrays;
+
 /**
  * Unit-tests for the {@link BloomFilter} class.
  */
-public abstract class BloomFilterTest extends TestCase {
+public class BloomFilterTest extends TestCase {
     private static final int BYTE_ARRAY_LENGTH = 100;
 
     private final BloomFilter mBloomFilter =
             new BloomFilter(new byte[BYTE_ARRAY_LENGTH], newHasher());
 
-    public abstract BloomFilter.Hasher newHasher();
+    public BloomFilter.Hasher newHasher() {
+        return new FastPairBloomFilterHasher();
+    }
 
     @Test
     public void emptyFilter_returnsEmptyArray() throws Exception {
@@ -46,7 +52,6 @@
         assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
     }
 
-
     @Test
     public void add() throws Exception {
         assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
@@ -149,4 +154,156 @@
         return "ELEMENT_" + index;
     }
 
+    @Test
+    public void specificBitPattern() throws Exception {
+        // Create a new BloomFilter along with a fixed set of elements
+        // and bit patterns to verify with.
+        BloomFilter bloomFilter = new BloomFilter(new byte[6], newHasher());
+        // Combine an account key and mac address.
+        byte[] element =
+                concat(
+                        base16().decode("11223344556677889900AABBCCDDEEFF"),
+                        base16().withSeparator(":", 2).decode("84:68:3E:00:02:11"));
+        byte[] expectedBitPattern = new byte[] {0x50, 0x00, 0x04, 0x15, 0x08, 0x01};
+
+        // Add the fixed elements to the filter.
+        bloomFilter.add(element);
+
+        // Verify that the resulting bytes match the expected one.
+        byte[] bloomFilterBytes = bloomFilter.asBytes();
+        assertWithMessage(
+                "Unexpected bit pattern. Expected %s, but got %s.",
+                base16().encode(expectedBitPattern), base16().encode(bloomFilterBytes))
+                .that(Arrays.equals(expectedBitPattern, bloomFilterBytes))
+                .isTrue();
+
+        // Verify that the expected bit pattern creates a BloomFilter containing all fixed elements.
+        bloomFilter = new BloomFilter(expectedBitPattern, newHasher());
+        assertThat(bloomFilter.possiblyContains(element)).isTrue();
+    }
+
+    // This test case has been on the spec,
+    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
+    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
+    // address) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasOneAccountKey() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
+        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+
+        // Add the fixed elements to the filter.
+        bloomFilter1.add(concat(accountKey, salt1));
+        bloomFilter2.add(concat(accountKey, salt2));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("0A428810"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("020C802A"));
+    }
+
+    // Adds this test case to spec. We can easily change the parameters (e.g. account key, ble
+    // address, battery data) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasOneAccountKey_withBatteryData() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
+        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+        byte[] batteryData = {
+                0b00110011, // length = 3, show UI indication.
+                0b01000000, // Left bud: not charging, battery level = 64.
+                0b01000000, // Right bud: not charging, battery level = 64.
+                0b01000000 // Case: not charging, battery level = 64.
+        };
+
+        // Adds battery data to build bloom filter.
+        bloomFilter1.add(concat(accountKey, salt1, batteryData));
+        bloomFilter2.add(concat(accountKey, salt2, batteryData));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("4A00F000"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("0101460A"));
+    }
+
+    // This test case has been on the spec,
+    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
+    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
+    // address) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasTwoAccountKeys() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+
+        // Adds the fixed elements to the filter.
+        bloomFilter1.add(concat(accountKey1, salt1));
+        bloomFilter1.add(concat(accountKey2, salt1));
+        bloomFilter2.add(concat(accountKey1, salt2));
+        bloomFilter2.add(concat(accountKey2, salt2));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("2FBA064200"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("844A62208B"));
+    }
+
+    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
+    // address, battery data) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasTwoAccountKeys_withBatteryData() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+        byte[] batteryData = {
+                0b00110011, // length = 3, show UI indication.
+                0b01000000, // Left bud: not charging, battery level = 64.
+                0b01000000, // Right bud: not charging, battery level = 64.
+                0b01000000 // Case: not charging, battery level = 64.
+        };
+
+        // Adds battery data to build bloom filter.
+        bloomFilter1.add(concat(accountKey1, salt1, batteryData));
+        bloomFilter1.add(concat(accountKey2, salt1, batteryData));
+        bloomFilter2.add(concat(accountKey1, salt2, batteryData));
+        bloomFilter2.add(concat(accountKey2, salt2, batteryData));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("102256C04D"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("461524D008"));
+    }
+
+    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
+    // address, battery data and battery remaining time) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasTwoAccountKeys_withBatteryLevelAndRemainingTime() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+        byte[] batteryData = {
+                0b00110011, // length = 3, show UI indication.
+                0b01000000, // Left bud: not charging, battery level = 64.
+                0b01000000, // Right bud: not charging, battery level = 64.
+                0b01000000 // Case: not charging, battery level = 64.
+        };
+        byte[] batteryRemainingTime = {
+                0b00010101, // length = 1, type = 0b0101 (remaining battery time).
+                0x1E, // remaining battery time (in minutes) =  30 minutes.
+        };
+
+        // Adds battery data to build bloom filter.
+        bloomFilter1.add(concat(accountKey1, salt1, batteryData, batteryRemainingTime));
+        bloomFilter1.add(concat(accountKey2, salt1, batteryData, batteryRemainingTime));
+        bloomFilter2.add(concat(accountKey1, salt2, batteryData, batteryRemainingTime));
+        bloomFilter2.add(concat(accountKey2, salt2, batteryData, batteryRemainingTime));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("32A086B41A"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("C2A042043E"));
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
index fb35092..0923b95 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
@@ -16,172 +16,29 @@
 
 package com.android.server.nearby.common.bloomfilter;
 
-import static com.google.common.io.BaseEncoding.base16;
-import static com.google.common.primitives.Bytes.concat;
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 import org.junit.Test;
 
-import java.util.Arrays;
+import java.nio.charset.Charset;
 
-public class FastPairBloomFilterHasherTest  extends BloomFilterTest{
-
-    @Override
-    public BloomFilter.Hasher newHasher() {
-        return new FastPairBloomFilterHasher();
+public class FastPairBloomFilterHasherTest {
+    private static final int BYTE_ARRAY_LENGTH = 100;
+    private static final Charset CHARSET = UTF_8;
+    private static FastPairBloomFilterHasher sFastPairBloomFilterHasher =
+            new FastPairBloomFilterHasher();
+    @Test
+    public void getHashes() {
+        int[] hashe1 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
+        int[] hashe2 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
+        int[] hashe3 = sFastPairBloomFilterHasher.getHashes(element(2).getBytes(CHARSET));
+        assertThat(hashe1).isEqualTo(hashe2);
+        assertThat(hashe1).isNotEqualTo(hashe3);
     }
 
-    @Test
-    public void specificBitPattern() throws Exception {
-        // Create a new BloomFilter along with a fixed set of elements
-        // and bit patterns to verify with.
-        BloomFilter bloomFilter = new BloomFilter(new byte[6], newHasher());
-        // Combine an account key and mac address.
-        byte[] element =
-                concat(
-                        base16().decode("11223344556677889900AABBCCDDEEFF"),
-                        base16().withSeparator(":", 2).decode("84:68:3E:00:02:11"));
-        byte[] expectedBitPattern = new byte[] {0x50, 0x00, 0x04, 0x15, 0x08, 0x01};
-
-        // Add the fixed elements to the filter.
-        bloomFilter.add(element);
-
-        // Verify that the resulting bytes match the expected one.
-        byte[] bloomFilterBytes = bloomFilter.asBytes();
-        assertWithMessage(
-                "Unexpected bit pattern. Expected %s, but got %s.",
-                base16().encode(expectedBitPattern), base16().encode(bloomFilterBytes))
-                .that(Arrays.equals(expectedBitPattern, bloomFilterBytes))
-                .isTrue();
-
-        // Verify that the expected bit pattern creates a BloomFilter containing all fixed elements.
-        bloomFilter = new BloomFilter(expectedBitPattern, newHasher());
-        assertThat(bloomFilter.possiblyContains(element)).isTrue();
-    }
-
-    // This test case has been on the spec,
-    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
-    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
-    // address) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasOneAccountKey() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
-        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-
-        // Add the fixed elements to the filter.
-        bloomFilter1.add(concat(accountKey, salt1));
-        bloomFilter2.add(concat(accountKey, salt2));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("0A428810"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("020C802A"));
-    }
-
-    // Adds this test case to spec. We can easily change the parameters (e.g. account key, ble
-    // address, battery data) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasOneAccountKey_withBatteryData() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
-        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-        byte[] batteryData = {
-                0b00110011, // length = 3, show UI indication.
-                0b01000000, // Left bud: not charging, battery level = 64.
-                0b01000000, // Right bud: not charging, battery level = 64.
-                0b01000000 // Case: not charging, battery level = 64.
-        };
-
-        // Adds battery data to build bloom filter.
-        bloomFilter1.add(concat(accountKey, salt1, batteryData));
-        bloomFilter2.add(concat(accountKey, salt2, batteryData));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("4A00F000"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("0101460A"));
-    }
-
-    // This test case has been on the spec,
-    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
-    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
-    // address) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasTwoAccountKeys() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
-        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-
-        // Adds the fixed elements to the filter.
-        bloomFilter1.add(concat(accountKey1, salt1));
-        bloomFilter1.add(concat(accountKey2, salt1));
-        bloomFilter2.add(concat(accountKey1, salt2));
-        bloomFilter2.add(concat(accountKey2, salt2));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("2FBA064200"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("844A62208B"));
-    }
-
-    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
-    // address, battery data) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasTwoAccountKeys_withBatteryData() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
-        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-        byte[] batteryData = {
-                0b00110011, // length = 3, show UI indication.
-                0b01000000, // Left bud: not charging, battery level = 64.
-                0b01000000, // Right bud: not charging, battery level = 64.
-                0b01000000 // Case: not charging, battery level = 64.
-        };
-
-        // Adds battery data to build bloom filter.
-        bloomFilter1.add(concat(accountKey1, salt1, batteryData));
-        bloomFilter1.add(concat(accountKey2, salt1, batteryData));
-        bloomFilter2.add(concat(accountKey1, salt2, batteryData));
-        bloomFilter2.add(concat(accountKey2, salt2, batteryData));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("102256C04D"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("461524D008"));
-    }
-
-    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
-    // address, battery data and battery remaining time) to clarify test results with partners.
-    @Test
-    public void specificBitPattern_hasTwoAccountKeys_withBatteryLevelAndRemainingTime() {
-        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
-        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
-        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
-        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
-        byte[] salt1 = base16().decode("C7");
-        byte[] salt2 = base16().decode("C7C8");
-        byte[] batteryData = {
-                0b00110011, // length = 3, show UI indication.
-                0b01000000, // Left bud: not charging, battery level = 64.
-                0b01000000, // Right bud: not charging, battery level = 64.
-                0b01000000 // Case: not charging, battery level = 64.
-        };
-        byte[] batteryRemainingTime = {
-                0b00010101, // length = 1, type = 0b0101 (remaining battery time).
-                0x1E, // remaining battery time (in minutes) =  30 minutes.
-        };
-
-        // Adds battery data to build bloom filter.
-        bloomFilter1.add(concat(accountKey1, salt1, batteryData, batteryRemainingTime));
-        bloomFilter1.add(concat(accountKey2, salt1, batteryData, batteryRemainingTime));
-        bloomFilter2.add(concat(accountKey1, salt2, batteryData, batteryRemainingTime));
-        bloomFilter2.add(concat(accountKey2, salt2, batteryData, batteryRemainingTime));
-
-        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("32A086B41A"));
-        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("C2A042043E"));
+    private String element(int index) {
+        return "ELEMENT_" + index;
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeNearbyItems.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
similarity index 73%
rename from nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeNearbyItems.java
rename to nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
index a3dfdcd..9cf65f4 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeNearbyItems.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
@@ -14,7 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.server.nearby.fastpair.testing;
+package com.android.server.nearby.fastpair;
 
-public class FakeNearbyItems {
+import org.junit.Test;
+
+public class FlagUtilsTest {
+
+    @Test
+    public void testGetPreferencesBuilder_notCrash() {
+        FlagUtils.getPreferencesBuilder().build();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
index 1aaed5d..5d4ea22 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
@@ -33,6 +33,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import service.proto.Cache;
+
 /** Unit tests for {@link DiscoveryItem} */
 public class DiscoveryItemTest {
     private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
@@ -45,6 +47,7 @@
             + "package=com.google.android.gms;"
             + "component=com.google.android.gms/"
             + ".nearby.discovery.service.DiscoveryService;end";
+    private static final String DISPLAY_URL = "DISPLAY_URL";
     private static final String TRIGGER_ID = "trigger.id";
     private static final String FAST_PAIR_ID = "id";
     private static final int RSSI = -80;
@@ -72,6 +75,7 @@
     public void testMultipleFields() {
         assertThat(mDiscoveryItem.getId()).isEqualTo(FAST_PAIR_ID);
         assertThat(mDiscoveryItem.getDescription()).isEqualTo(DEFAULT_DESCRIPITON);
+        assertThat(mDiscoveryItem.getDisplayUrl()).isEqualTo(DISPLAY_URL);
         assertThat(mDiscoveryItem.getTriggerId()).isEqualTo(TRIGGER_ID);
         assertThat(mDiscoveryItem.getMacAddress()).isEqualTo(DEFAULT_MAC_ADDRESS);
         assertThat(
@@ -82,10 +86,29 @@
         assertThat(mDiscoveryItem.getAppName()).isEqualTo(APP_NAME);
         assertThat(mDiscoveryItem.getRssi()).isEqualTo(RSSI);
         assertThat(mDiscoveryItem.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(mDiscoveryItem.getFastPairInformation()).isNull();
+        assertThat(mDiscoveryItem.getFastPairSecretKey()).isNull();
+        assertThat(mDiscoveryItem.getIcon()).isNull();
+        assertThat(mDiscoveryItem.getIconFifeUrl()).isNotNull();
+        assertThat(mDiscoveryItem.getState()).isNotNull();
+        assertThat(mDiscoveryItem.getTitle()).isNotNull();
+        assertThat(mDiscoveryItem.isApp()).isFalse();
+        assertThat(mDiscoveryItem.isDeletable(
+                100000L, 0L)).isTrue();
+        assertThat(mDiscoveryItem.isDeviceType(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
+        assertThat(mDiscoveryItem.isExpired(
+                100000L, 0L)).isTrue();
+        assertThat(mDiscoveryItem.isFastPair()).isTrue();
+        assertThat(mDiscoveryItem.isPendingAppInstallValid(5)).isTrue();
+        assertThat(mDiscoveryItem.isPendingAppInstallValid(5,
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  null,
+                TRIGGER_ID,  DEFAULT_MAC_ADDRESS,  "", RSSI, TX_POWER))).isTrue();
+        assertThat(mDiscoveryItem.isTypeEnabled(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
+        assertThat(mDiscoveryItem.toString()).isNotNull();
     }
 
     @Test
-    public void isMute() {
+    public void isMuted() {
         assertThat(mDiscoveryItem.isMuted()).isFalse();
     }
 
@@ -94,6 +117,7 @@
         assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
 
         // Null description should not show up.
+        mDiscoveryItem.setStoredItemForTest(DiscoveryItem.newStoredDiscoveryItem());
         mDiscoveryItem.setStoredItemForTest(
                 FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  null,
                         TRIGGER_ID,  DEFAULT_MAC_ADDRESS,  "", RSSI, TX_POWER));
@@ -177,6 +201,14 @@
     }
 
     @Test
+    public void testCompareTo() {
+        DiscoveryItem fastPairItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        assertThat(mDiscoveryItem.compareTo(fastPairItem)).isEqualTo(0);
+    }
+
+
+    @Test
     public void testCopyOfStoredItem() {
         DiscoveryItem fastPairItem =
                 FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
index adae97d..0f6fb19 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.le.ScanResult;
 import android.content.Context;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -43,6 +44,7 @@
     private static final ByteString ACCOUNT_KEY = ByteString.copyFromUtf8("axgs");
     private static final String MAC_ADDRESS_B = "00:11:22:44";
     private static final ByteString ACCOUNT_KEY_B = ByteString.copyFromUtf8("axgb");
+    private static final String ITEM_ID = "ITEM_ID";
 
     @Mock
     DiscoveryItem mDiscoveryItem;
@@ -50,6 +52,10 @@
     DiscoveryItem mDiscoveryItem2;
     @Mock
     Cache.StoredFastPairItem mStoredFastPairItem;
+    @Mock
+    ScanResult mScanResult;
+
+    Context mContext;
     Cache.StoredDiscoveryItem mStoredDiscoveryItem = Cache.StoredDiscoveryItem.newBuilder()
             .setTriggerId(MODEL_ID)
             .setAppName(APP_NAME).build();
@@ -60,12 +66,12 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mContext = ApplicationProvider.getApplicationContext();
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void notSaveRetrieveInfo() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
         when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
 
@@ -78,7 +84,6 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void saveRetrieveInfo() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
         when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
 
@@ -91,7 +96,6 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void getAllInfo() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
         when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
         when(mDiscoveryItem2.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem2);
@@ -105,12 +109,13 @@
         fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem2);
 
         assertThat(fastPairCacheManager.getAllSavedStoreDiscoveryItem()).hasSize(3);
+
+        fastPairCacheManager.cleanUp();
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void saveRetrieveInfoStoredFastPairItem() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
                 .setMacAddress(MAC_ADDRESS)
                 .setAccountKey(ACCOUNT_KEY)
@@ -128,7 +133,6 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void checkGetAllFastPairItems() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
                 .setMacAddress(MAC_ADDRESS)
                 .setAccountKey(ACCOUNT_KEY)
@@ -149,5 +153,15 @@
 
         assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
                 .isEqualTo(1);
+
+        fastPairCacheManager.cleanUp();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void getDeviceFromScanResult_notCrash() {
+        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+        fastPairCacheManager.getDeviceFromScanResult(mScanResult);
+
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
new file mode 100644
index 0000000..c5428f5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.cache;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteException;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairDbHelperTest {
+
+    Context mContext;
+    FastPairDbHelper mFastPairDbHelper;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mFastPairDbHelper = new FastPairDbHelper(mContext);
+    }
+
+    @After
+    public void teardown() {
+        mFastPairDbHelper.close();
+    }
+
+    @Test
+    public void testUpgrade_notCrash() {
+        mFastPairDbHelper
+                .onUpgrade(mFastPairDbHelper.getWritableDatabase(), 1, 2);
+    }
+
+    @Test
+    public void testDowngrade_throwsException()  {
+        assertThrows(
+                SQLiteException.class,
+                () -> mFastPairDbHelper.onDowngrade(
+                        mFastPairDbHelper.getWritableDatabase(), 2, 1));
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
index b7d8af2..b80cb55 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
@@ -20,7 +20,8 @@
 
 import static org.mockito.Mockito.when;
 
-import androidx.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
 
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
@@ -51,64 +52,49 @@
     @Mock
     FastPairCacheManager mFastPairCacheManager;
 
+    private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
     private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
     private static final int SUBSEQUENT_PAIR_START = 1310;
     private static final int SUBSEQUENT_PAIR_END = 1320;
+    private static HalfSheetPairingProgressHandler sHalfSheetPairingProgressHandler;
+    private static DiscoveryItem sDiscoveryItem;
+    private static BluetoothDevice sBluetoothDevice;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mContextWrapper.getLocator()).thenReturn(mLocator);
-        mLocator.overrideBindingForTest(FastPairCacheManager.class,
-                mFastPairCacheManager);
+        mLocator.overrideBindingForTest(FastPairCacheManager.class, mFastPairCacheManager);
         mLocator.overrideBindingForTest(Clock.class, mClock);
-    }
-
-    @Test
-    public void getPairEndEventCode() {
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
+        FastPairHalfSheetManager mfastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        mLocator.bind(FastPairHalfSheetManager.class, mfastPairHalfSheetManager);
+        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
                         .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+                        .setMacAddress(DEFAULT_MAC_ADDRESS)
                         .setFastPairInformation(
                                 Cache.FastPairInformation.newBuilder()
                                         .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
                         .build());
+        sHalfSheetPairingProgressHandler =
+                new HalfSheetPairingProgressHandler(mContextWrapper, sDiscoveryItem,
+                        sDiscoveryItem.getAppPackageName(), ACCOUNT_KEY);
 
-        HalfSheetPairingProgressHandler halfSheetPairingProgressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem);
-        assertThat(halfSheetPairingProgressHandler
+        sBluetoothDevice =
+                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+    }
+
+    @Test
+    public void getPairEndEventCode() {
+        assertThat(sHalfSheetPairingProgressHandler
                 .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
     }
 
     @Test
     public void getPairStartEventCode() {
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-
-        HalfSheetPairingProgressHandler halfSheetPairingProgressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem);
-        assertThat(halfSheetPairingProgressHandler
+        assertThat(sHalfSheetPairingProgressHandler
                 .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
     }
-
-    private HalfSheetPairingProgressHandler createProgressHandler(
-            @Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
-        FastPairHalfSheetManager fastPairHalfSheetManager =
-                new FastPairHalfSheetManager(mContextWrapper);
-        mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
-        HalfSheetPairingProgressHandler mHalfSheetPairingProgressHandler =
-                new HalfSheetPairingProgressHandler(
-                        mContextWrapper,
-                        fastPairItem,
-                        fastPairItem.getAppPackageName(),
-                        accountKey);
-        return mHalfSheetPairingProgressHandler;
-    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
index 423ef45..68d38b2 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
@@ -26,7 +26,6 @@
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
-import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
 import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
 import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
@@ -52,11 +51,12 @@
     Clock mClock;
     @Mock
     FastPairCacheManager mFastPairCacheManager;
-    @Mock
-    FootprintsDeviceManager mFootprintsDeviceManager;
+
     private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
     private static final int SUBSEQUENT_PAIR_START = 1310;
     private static final int SUBSEQUENT_PAIR_END = 1320;
+    private static DiscoveryItem sDiscoveryItem;
+    private static  NotificationPairingProgressHandler sNotificationPairingProgressHandler;
 
     @Before
     public void setup() {
@@ -65,42 +65,46 @@
         mLocator.overrideBindingForTest(FastPairCacheManager.class,
                 mFastPairCacheManager);
         mLocator.overrideBindingForTest(Clock.class, mClock);
-    }
-
-    @Test
-    public void getPairEndEventCode() {
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
+        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
                         .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
                         .setFastPairInformation(
                                 Cache.FastPairInformation.newBuilder()
                                         .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
                         .build());
+        sNotificationPairingProgressHandler = createProgressHandler(ACCOUNT_KEY, sDiscoveryItem);
+    }
 
-        NotificationPairingProgressHandler notificationPairingProgressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem);
-        assertThat(notificationPairingProgressHandler
+    @Test
+    public void getPairEndEventCode() {
+        assertThat(sNotificationPairingProgressHandler
                 .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
     }
 
     @Test
     public void getPairStartEventCode() {
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
-
-        NotificationPairingProgressHandler notificationPairingProgressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem);
-        assertThat(notificationPairingProgressHandler
+        assertThat(sNotificationPairingProgressHandler
                 .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
     }
 
+    @Test
+    public void onReadyToPair() {
+        sNotificationPairingProgressHandler.onReadyToPair();
+    }
+
+    @Test
+    public void  onPairingFailed() {
+        Throwable e = new Throwable("Pairing Failed");
+        sNotificationPairingProgressHandler.onPairingFailed(e);
+    }
+
+
+    @Test
+    public void onPairingSuccess() {
+        sNotificationPairingProgressHandler.onPairingSuccess(sDiscoveryItem.getMacAddress());
+    }
+
     private NotificationPairingProgressHandler createProgressHandler(
             @Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
         FastPairNotificationManager fastPairNotificationManager =
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
index 6e79297..b4b4f78 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
@@ -20,8 +20,13 @@
 
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
 import androidx.annotation.Nullable;
 
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -53,23 +58,41 @@
     FastPairCacheManager mFastPairCacheManager;
     @Mock
     FootprintsDeviceManager mFootprintsDeviceManager;
+    @Mock
+    FastPairConnection mFastPairConnection;
+
     private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+    private static final int PASSKEY = 1234;
+    private static DiscoveryItem sDiscoveryItem;
+    private static PairingProgressHandlerBase sPairingProgressHandlerBase;
+    private static BluetoothDevice sBluetoothDevice;
 
     @Before
     public void setup() {
-
         MockitoAnnotations.initMocks(this);
         when(mContextWrapper.getLocator()).thenReturn(mLocator);
         mLocator.overrideBindingForTest(FastPairCacheManager.class,
                 mFastPairCacheManager);
         mLocator.overrideBindingForTest(Clock.class, mClock);
+        sBluetoothDevice =
+                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
+                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+                        .setFastPairInformation(
+                                Cache.FastPairInformation.newBuilder()
+                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+                        .build());
+
+        sPairingProgressHandlerBase =
+                createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
     }
 
     @Test
     public void createHandler_halfSheetSubsequentPairing_notificationPairingHandlerCreated() {
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
                         .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
                         .setFastPairInformation(
                                 Cache.FastPairInformation.newBuilder()
@@ -77,7 +100,7 @@
                         .build());
 
         PairingProgressHandlerBase progressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem, /* isRetroactivePair= */ false);
+                createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
 
         assertThat(progressHandler).isInstanceOf(NotificationPairingProgressHandler.class);
     }
@@ -85,34 +108,86 @@
     @Test
     public void createHandler_halfSheetInitialPairing_halfSheetPairingHandlerCreated() {
         // No account key
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
                         .setFastPairInformation(
                                 Cache.FastPairInformation.newBuilder()
                                         .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
                         .build());
 
         PairingProgressHandlerBase progressHandler =
-                createProgressHandler(null, discoveryItem, /* isRetroactivePair= */ false);
+                createProgressHandler(null, sDiscoveryItem, /* isRetroactivePair= */ false);
 
         assertThat(progressHandler).isInstanceOf(HalfSheetPairingProgressHandler.class);
     }
 
     @Test
-    public void skipWaitingScreenUnlock() {
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
-                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
-                        .setFastPairInformation(
-                                Cache.FastPairInformation.newBuilder()
-                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
-                        .build());
+    public void onPairingStarted() {
+        sPairingProgressHandlerBase.onPairingStarted();
+    }
 
-        PairingProgressHandlerBase progressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem, /* isRetroactivePair= */ false);
-        assertThat(progressHandler.skipWaitingScreenUnlock()).isFalse();
+    @Test
+    public void onWaitForScreenUnlock() {
+        sPairingProgressHandlerBase.onWaitForScreenUnlock();
+    }
+
+    @Test
+    public void  onScreenUnlocked() {
+        sPairingProgressHandlerBase.onScreenUnlocked();
+    }
+
+    @Test
+    public void onReadyToPair() {
+        sPairingProgressHandlerBase.onReadyToPair();
+    }
+
+    @Test
+    public void  onSetupPreferencesBuilder() {
+        Preferences.Builder prefsBuilder =
+                Preferences.builder()
+                        .setEnableBrEdrHandover(false)
+                        .setIgnoreDiscoveryError(true);
+        sPairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
+    }
+
+    @Test
+    public void  onPairingSetupCompleted() {
+        sPairingProgressHandlerBase.onPairingSetupCompleted();
+    }
+
+    @Test
+    public void onHandlePasskeyConfirmation() {
+        sPairingProgressHandlerBase.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
+    }
+
+    @Test
+    public void getKeyForLocalCache() {
+        FastPairConnection.SharedSecret sharedSecret =
+                FastPairConnection.SharedSecret.create(ACCOUNT_KEY, sDiscoveryItem.getMacAddress());
+        sPairingProgressHandlerBase
+                .getKeyForLocalCache(ACCOUNT_KEY, mFastPairConnection, sharedSecret);
+    }
+
+    @Test
+    public void onPairingFailed() {
+        Throwable e = new Throwable("Pairing Failed");
+        sPairingProgressHandlerBase.onPairingFailed(e);
+    }
+
+    @Test
+    public void onPairingSuccess() {
+        sPairingProgressHandlerBase.onPairingSuccess(sDiscoveryItem.getMacAddress());
+    }
+
+    @Test
+    public void  optInFootprintsForInitialPairing() {
+        sPairingProgressHandlerBase.optInFootprintsForInitialPairing(
+                mFootprintsDeviceManager, sDiscoveryItem, ACCOUNT_KEY, null);
+    }
+
+    @Test
+    public void skipWaitingScreenUnlock() {
+        assertThat(sPairingProgressHandlerBase.skipWaitingScreenUnlock()).isFalse();
     }
 
     private PairingProgressHandlerBase createProgressHandler(
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
index e289543..cdec04d 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
@@ -31,6 +31,7 @@
                     + "package=com.google.android.gms;"
                     + "component=com.google.android.gms/"
                     + ".nearby.discovery.service.DiscoveryService;end";
+    private static final String DISPLAY_URL = "DISPLAY_URL";
     private static final String TRIGGER_ID = "trigger.id";
     private static final String FAST_PAIR_ID = "id";
     private static final int RSSI = -80;
@@ -56,6 +57,7 @@
         item.setAppName(APP_NAME);
         item.setRssi(RSSI);
         item.setTxPower(TX_POWER);
+        item.setDisplayUrl(DISPLAY_URL);
         return item.build();
     }
 
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 06c8179..2bf3ab9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -76,8 +76,10 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsService;
@@ -140,6 +142,7 @@
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FileRotator;
@@ -765,6 +768,11 @@
                 return null;
             }
         }
+
+        /** Gets whether the build is userdebug. */
+        public boolean isDebuggable() {
+            return Build.isDebuggable();
+        }
     }
 
     /**
@@ -927,18 +935,27 @@
         final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
         final int attempts;
         final int fallbacks;
+        final boolean runComparison;
         try {
             attempts = mImportLegacyAttemptsCounter.get();
+            // Fallbacks counter would be set to non-zero value to indicate the migration was
+            // not successful.
             fallbacks = mImportLegacyFallbacksCounter.get();
+            runComparison = shouldRunComparison();
         } catch (IOException e) {
             Log.wtf(TAG, "Failed to read counters, skip.", e);
             return;
         }
-        // If fallbacks is not zero, proceed with reading only to give signals from dogfooders.
-        // TODO(b/233752318): Remove fallbacks counter check before T formal release.
-        if (attempts >= targetAttempts && fallbacks == 0) return;
 
-        final boolean dryRunImportOnly = (attempts >= targetAttempts);
+        // If the target number of attempts are reached, don't import any data.
+        // However, if comparison is requested, still read the legacy data and compare
+        // it to the importer output. This allows OEMs to debug issues with the
+        // importer code and to collect signals from the field.
+        final boolean dryRunImportOnly =
+                fallbacks != 0 && runComparison && (attempts >= targetAttempts);
+        // Return if target attempts are reached and there is no need to dry run.
+        if (attempts >= targetAttempts && !dryRunImportOnly) return;
+
         if (dryRunImportOnly) {
             Log.i(TAG, "Starting import : only perform read");
         } else {
@@ -951,69 +968,54 @@
         };
 
         // Legacy directories will be created by recorders if they do not exist
-        final File legacyBaseDir = mDeps.getLegacyStatsDir();
-        final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
-                buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
-                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
-                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
-                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
-        };
+        final NetworkStatsRecorder[] legacyRecorders;
+        if (runComparison) {
+            final File legacyBaseDir = mDeps.getLegacyStatsDir();
+            legacyRecorders = new NetworkStatsRecorder[]{
+                    buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
+                    buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
+                    buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
+                    buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
+            };
+        } else {
+            legacyRecorders = null;
+        }
 
         long migrationEndTime = Long.MIN_VALUE;
-        boolean endedWithFallback = false;
         try {
             // First, read all legacy collections. This is OEM code and it can throw. Don't
             // commit any data to disk until all are read.
             for (int i = 0; i < migrations.length; i++) {
-                String errMsg = null;
-                Throwable exception = null;
                 final MigrationInfo migration = migrations[i];
 
-                // Read the collection from platform code, and using fallback method if throws.
+                // Read the collection from platform code, and set fallbacks counter if throws
+                // for better debugging.
                 try {
                     migration.collection = readPlatformCollectionForRecorder(migration.recorder);
                 } catch (Throwable e) {
-                    errMsg = "Failed to read stats from platform";
-                    exception = e;
-                }
-
-                // Also read the collection with legacy method
-                final NetworkStatsRecorder legacyRecorder = legacyRecorders[i];
-
-                final NetworkStatsCollection legacyStats;
-                try {
-                    legacyStats = legacyRecorder.getOrLoadCompleteLocked();
-                } catch (Throwable e) {
-                    Log.wtf(TAG, "Failed to read stats with legacy method for recorder " + i, e);
-                    if (exception != null) {
-                        throw exception;
+                    if (dryRunImportOnly) {
+                        Log.wtf(TAG, "Platform data read failed. ", e);
+                        return;
                     } else {
-                        // Use newer stats, since that's all that is available
-                        continue;
+                        // Data is not imported successfully, set fallbacks counter to non-zero
+                        // value to trigger dry run every later boot when the runComparison is
+                        // true, in order to make it easier to debug issues.
+                        tryIncrementLegacyFallbacksCounter();
+                        // Re-throw for error handling. This will increase attempts counter.
+                        throw e;
                     }
                 }
 
-                if (errMsg == null) {
-                    try {
-                        errMsg = compareStats(migration.collection, legacyStats);
-                    } catch (Throwable e) {
-                        errMsg = "Failed to compare migrated stats with all stats";
-                        exception = e;
+                if (runComparison) {
+                    final boolean success =
+                            compareImportedToLegacyStats(migration, legacyRecorders[i]);
+                    if (!success && !dryRunImportOnly) {
+                        tryIncrementLegacyFallbacksCounter();
                     }
                 }
-
-                if (errMsg != null) {
-                    Log.wtf(TAG, "NetworkStats import for migration " + i
-                            + " returned invalid data: " + errMsg, exception);
-                    // Fall back to legacy stats for this boot. The stats for old data will be
-                    // re-imported again on next boot until they succeed the import. This is fine
-                    // since every import clears the previous stats for the imported timespan.
-                    migration.collection = legacyStats;
-                    endedWithFallback = true;
-                }
             }
 
-            // For cases where the fallbacks is not zero but target attempts counts reached,
+            // For cases where the fallbacks are not zero but target attempts counts reached,
             // only perform reads above and return here.
             if (dryRunImportOnly) return;
 
@@ -1079,22 +1081,78 @@
         // Success ! No need to import again next time.
         try {
             mImportLegacyAttemptsCounter.set(targetAttempts);
-            if (endedWithFallback) {
-                Log.wtf(TAG, "Imported platform collections with legacy fallback");
-                final int fallbacksCount = mImportLegacyFallbacksCounter.get();
-                mImportLegacyFallbacksCounter.set(fallbacksCount + 1);
-            } else {
-                Log.i(TAG, "Successfully imported platform collections");
-                // The successes counter is only for debugging. Hence, the synchronization
-                // between successes counter and attempts counter are not very critical.
-                final int successCount = mImportLegacySuccessesCounter.get();
-                mImportLegacySuccessesCounter.set(successCount + 1);
-            }
+            Log.i(TAG, "Successfully imported platform collections");
+            // The successes counter is only for debugging. Hence, the synchronization
+            // between successes counter and attempts counter are not very critical.
+            final int successCount = mImportLegacySuccessesCounter.get();
+            mImportLegacySuccessesCounter.set(successCount + 1);
         } catch (IOException e) {
             Log.wtf(TAG, "Succeed but failed to update counters.", e);
         }
     }
 
+    void tryIncrementLegacyFallbacksCounter() {
+        try {
+            final int fallbacks = mImportLegacyFallbacksCounter.get();
+            mImportLegacyFallbacksCounter.set(fallbacks + 1);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to update fallback counter.", e);
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldRunComparison() {
+        final ConnectivityResources resources = new ConnectivityResources(mContext);
+        // 0 if id not found.
+        Boolean overlayValue = null;
+        try {
+            switch (resources.get().getInteger(R.integer.config_netstats_validate_import)) {
+                case 1:
+                    overlayValue = Boolean.TRUE;
+                    break;
+                case 0:
+                    overlayValue = Boolean.FALSE;
+                    break;
+            }
+        } catch (Resources.NotFoundException e) {
+            // Overlay value is not defined.
+        }
+        // TODO(b/233752318): For now it is always true to collect signal from beta users.
+        //  Should change to the default behavior (true if debuggable builds) before formal release.
+        return (overlayValue != null ? overlayValue : mDeps.isDebuggable()) || true;
+    }
+
+    /**
+     * Compare imported data with the data returned by legacy recorders.
+     *
+     * @return true if the data matches, false if the data does not match or throw with exceptions.
+     */
+    private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
+            @NonNull NetworkStatsRecorder legacyRecorder) {
+        final NetworkStatsCollection legacyStats;
+        try {
+            legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+        } catch (Throwable e) {
+            Log.wtf(TAG, "Failed to read stats with legacy method for recorder "
+                    + legacyRecorder.getCookie(), e);
+            // Cannot read data from legacy method, skip comparison.
+            return false;
+        }
+
+        // The result of comparison is only for logging.
+        try {
+            final String error = compareStats(migration.collection, legacyStats);
+            if (error != null) {
+                Log.wtf(TAG, "Unexpected comparison result for recorder "
+                        + legacyRecorder.getCookie() + ": " + error);
+            }
+        } catch (Throwable e) {
+            Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder "
+                    + legacyRecorder.getCookie(), e);
+        }
+        return true;
+    }
+
     private static String str(NetworkStatsCollection.Key key) {
         StringBuilder sb = new StringBuilder()
                 .append(key.ident.toString())
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 81782f9..bff6953 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -179,4 +179,13 @@
     Only supported up to S. On T+, the Wi-Fi code should use unregisterAfterReplacement in order
     to ensure that apps see the network disconnect and reconnect. -->
     <integer translatable="false" name="config_validationFailureAfterRoamIgnoreTimeMillis">-1</integer>
+
+    <!-- Whether the network stats service should run compare on the result of
+    {@link NetworkStatsDataMigrationUtils#readPlatformCollection} and the result
+    of reading from legacy recorders. Possible values are:
+      0 = never compare,
+      1 = always compare,
+      2 = compare on debuggable builds (default value)
+      -->
+    <integer translatable="false" name="config_netstats_validate_import">2</integer>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index b92dd08..3389d63 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -41,6 +41,7 @@
             <item type="array" name="config_ethernet_interfaces"/>
             <item type="string" name="config_ethernet_iface_regex"/>
             <item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
+            <item type="integer" name="config_netstats_validate_import" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index bc70c93..2780044 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -151,6 +151,12 @@
     return (jint)status.code();
 }
 
+static jint native_updateUidLockdownRule(JNIEnv* env, jobject self, jint uid, jboolean add) {
+    Status status = mTc.updateUidLockdownRule(uid, add);
+    CHECK_LOG(status);
+    return (jint)status.code();
+}
+
 static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
     Status status = mTc.swapActiveStatsMap();
     CHECK_LOG(status);
@@ -203,6 +209,8 @@
     (void*)native_addUidInterfaceRules},
     {"native_removeUidInterfaceRules", "([I)I",
     (void*)native_removeUidInterfaceRules},
+    {"native_updateUidLockdownRule", "(IZ)I",
+    (void*)native_updateUidLockdownRule},
     {"native_swapActiveStatsMap", "()I",
     (void*)native_swapActiveStatsMap},
     {"native_setPermissionForUids", "(I[I)V",
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 4efd0e1..9c7a761 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -51,7 +51,15 @@
     jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
 }
 
-static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) {
+// enable or disable  carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
+    uint32_t carrierOn = enabled;
+    if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+        throwException(env, errno, "set carrier", iface);
+    }
+}
+
+static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, const char* iface) {
     base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
     ifreq ifr{};
 
@@ -63,6 +71,11 @@
         return -1;
     }
 
+    if (!hasCarrier) {
+        // disable carrier before setting IFF_UP
+        setTunTapCarrierEnabledImpl(env, iface, tun.get(), hasCarrier);
+    }
+
     // Activate interface using an unconnected datagram socket.
     base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
     ifr.ifr_flags = IFF_UP;
@@ -79,23 +92,31 @@
 
 //------------------------------------------------------------------------------
 
-static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) {
+static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
+                                    jIface, jint tunFd, jboolean enabled) {
+    ScopedUtfChars iface(env, jIface);
+    if (!iface.c_str()) {
+        jniThrowNullPointerException(env, "iface");
+    }
+    setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
+                             jboolean hasCarrier, jstring jIface) {
     ScopedUtfChars iface(env, jIface);
     if (!iface.c_str()) {
         jniThrowNullPointerException(env, "iface");
         return -1;
     }
 
-    int tun = createTunTapInterface(env, isTun, iface.c_str());
-
-    // Any exceptions will be thrown from the createTunTapInterface call
-    return tun;
+    return createTunTapImpl(env, isTun, hasCarrier, iface.c_str());
 }
 
 //------------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
-    {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create},
+    {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
+    {"nativeCreateTunTap", "(ZZLjava/lang/String;)I", (void*)createTunTap},
 };
 
 int register_com_android_server_TestNetworkService(JNIEnv* env) {
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index bce3bb5..adc1925 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -340,8 +340,6 @@
             return ALLOWLIST;
         case LOW_POWER_STANDBY:
             return ALLOWLIST;
-        case LOCKDOWN:
-            return DENYLIST;
         case OEM_DENY_1:
             return DENYLIST;
         case OEM_DENY_2:
@@ -373,9 +371,6 @@
         case LOW_POWER_STANDBY:
             res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
             break;
-        case LOCKDOWN:
-            res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
-            break;
         case OEM_DENY_1:
             res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
             break;
@@ -447,6 +442,18 @@
     return netdutils::status::ok;
 }
 
+Status TrafficController::updateUidLockdownRule(const uid_t uid, const bool add) {
+    std::lock_guard guard(mMutex);
+
+    netdutils::Status result = add ? addRule(uid, LOCKDOWN_VPN_MATCH)
+                               : removeRule(uid, LOCKDOWN_VPN_MATCH);
+    if (!isOk(result)) {
+        ALOGW("%s Lockdown rule failed(%d): uid=%d",
+              (add ? "add": "remove"), result.code(), uid);
+    }
+    return result;
+}
+
 int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
                                           const std::vector<int32_t>& uids) {
     // FirewallRule rule = isAllowlist ? ALLOW : DENY;
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index f84a910..b77c465 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -218,7 +218,7 @@
         checkEachUidValue(uids, match);
     }
 
-    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule,
+    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint32_t expectedRule,
                                  uint32_t expectedIif) {
         for (uint32_t uid : appUids) {
             Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
@@ -407,7 +407,6 @@
     checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
     checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
     checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
-    checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
     checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
     checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
     checkUidOwnerRuleForChain(OEM_DENY_3, OEM_DENY_3_MATCH);
@@ -539,6 +538,21 @@
     expectMapEmpty(mFakeUidOwnerMap);
 }
 
+TEST_F(TrafficControllerTest, TestUpdateUidLockdownRule) {
+    // Add Lockdown rules
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, true /* add */)));
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, true /* add */)));
+    expectUidOwnerMapValues({1000, 1001}, LOCKDOWN_VPN_MATCH, 0);
+
+    // Remove one of Lockdown rules
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, false /* add */)));
+    expectUidOwnerMapValues({1001}, LOCKDOWN_VPN_MATCH, 0);
+
+    // Remove remaining Lockdown rule
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, false /* add */)));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
 TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
     // Set up existing PENALTY_BOX_MATCH rules
     ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
@@ -885,7 +899,6 @@
             {POWERSAVE, ALLOWLIST},
             {RESTRICTED, ALLOWLIST},
             {LOW_POWER_STANDBY, ALLOWLIST},
-            {LOCKDOWN, DENYLIST},
             {OEM_DENY_1, DENYLIST},
             {OEM_DENY_2, DENYLIST},
             {OEM_DENY_3, DENYLIST},
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index 2427aa9..3f28991 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -35,7 +35,6 @@
     POWERSAVE = 3,
     RESTRICTED = 4,
     LOW_POWER_STANDBY = 5,
-    LOCKDOWN = 6,
     OEM_DENY_1 = 7,
     OEM_DENY_2 = 8,
     OEM_DENY_3 = 9,
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index c019ce7..c921ff2 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -71,6 +71,8 @@
             EXCLUDES(mMutex);
     netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
 
+    netdutils::Status updateUidLockdownRule(const uid_t uid, const bool add) EXCLUDES(mMutex);
+
     netdutils::Status updateUidOwnerMap(const uint32_t uid,
                                         UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
 
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index c006bc6..151d0e3 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -216,6 +216,19 @@
     }
 
     /**
+     * Update lockdown rule for uid
+     *
+     * @param  uid          target uid to add/remove the rule
+     * @param  add          {@code true} to add the rule, {@code false} to remove the rule.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public void updateUidLockdownRule(final int uid, final boolean add) {
+        final int err = native_updateUidLockdownRule(uid, add);
+        maybeThrow(err, "Unable to update lockdown rule");
+    }
+
+    /**
      * Request netd to change the current active network stats map.
      *
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
@@ -271,6 +284,7 @@
     private native int native_setUidRule(int childChain, int uid, int firewallRule);
     private native int native_addUidInterfaceRules(String ifName, int[] uids);
     private native int native_removeUidInterfaceRules(int[] uids);
+    private native int native_updateUidLockdownRule(int uid, boolean add);
     private native int native_swapActiveStatsMap();
     private native void native_setPermissionForUids(int permissions, int[] uids);
     private native void native_dump(FileDescriptor fd, boolean verbose);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 3dbf678..d734029 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -7766,10 +7766,6 @@
         // when the old rules are removed and the time when new rules are added. To fix this,
         // make eBPF support two allowlisted interfaces so here new rules can be added before the
         // old rules are being removed.
-
-        // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
-        // packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
-        // by overriding the Lockdown blocking rule.
         if (wasFiltering) {
             mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
         }
@@ -8095,12 +8091,14 @@
      * Returns whether we need to set interface filtering rule or not
      */
     private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
-            String filterIface) {
-        // Only filter if lp has an interface.
-        if (lp == null || lp.getInterfaceName() == null) return false;
-        // Before T, allow rules are only needed if VPN isolation is enabled.
-        // T and After T, allow rules are needed for all VPNs.
-        return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
+            String isolationIface) {
+        // Allow rules are always needed if VPN isolation is enabled.
+        if (isolationIface != null) return true;
+
+        // On T and above, allow rules are needed for all VPNs. Allow rule with null iface is a
+        // wildcard to allow apps to receive packets on all interfaces. This is required to accept
+        // incoming traffic in Lockdown mode by overriding the Lockdown blocking rule.
+        return SdkLevel.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
     }
 
     private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8243,10 +8241,6 @@
             // above, where the addition of new ranges happens before the removal of old ranges.
             // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
             // to be removed will never overlap with the new range to be added.
-
-            // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
-            // receive packets on all interfaces. This is required to accept incoming traffic in
-            // Lockdown mode by overriding the Lockdown blocking rule.
             if (wasFiltering && !prevRanges.isEmpty()) {
                 mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
                         prevNc.getOwnerUid());
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index e12190c..1209579 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -50,6 +50,7 @@
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
 
+import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -76,7 +77,11 @@
     @NonNull private final NetworkProvider mNetworkProvider;
 
     // Native method stubs
-    private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+    private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
+            @NonNull String iface);
+
+    private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+            boolean enabled);
 
     @VisibleForTesting
     protected TestNetworkService(@NonNull Context context) {
@@ -114,7 +119,7 @@
      * interface.
      */
     @Override
-    public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
+    public TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
             LinkAddress[] linkAddrs, @Nullable String iface) {
         enforceTestNetworkPermissions(mContext);
 
@@ -130,8 +135,8 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            ParcelFileDescriptor tunIntf =
-                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, interfaceName));
+            ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
+                    nativeCreateTunTap(isTun, hasCarrier, interfaceName));
             for (LinkAddress addr : linkAddrs) {
                 mNetd.interfaceAddAddress(
                         interfaceName,
@@ -375,4 +380,20 @@
     public static void enforceTestNetworkPermissions(@NonNull Context context) {
         context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
     }
+
+    /** Enable / disable TestNetworkInterface carrier */
+    @Override
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        enforceTestNetworkPermissions(mContext);
+        nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
+                enabled);
+        // Explicitly close fd after use to prevent StrictMode from complaining.
+        // Also, explicitly referencing iface guarantees that the object is not garbage collected
+        // before nativeSetTunTapCarrierEnabled() executes.
+        try {
+            iface.getFileDescriptor().close();
+        } catch (IOException e) {
+            // if the close fails, there is not much that can be done -- move on.
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index e4a2c20..dedeb38 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -23,9 +23,6 @@
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
-import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -684,8 +681,12 @@
     }
 
     private synchronized void updateLockdownUid(int uid, boolean add) {
-        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
-                && !hasRestrictedNetworksPermission(uid)) {
+        // Apps that can use restricted networks can always bypass VPNs.
+        if (hasRestrictedNetworksPermission(uid)) {
+            return;
+        }
+
+        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)) {
             updateLockdownUidRule(uid, add);
         }
     }
@@ -1079,11 +1080,7 @@
 
     private void updateLockdownUidRule(int uid, boolean add) {
         try {
-            if (add) {
-                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
-            } else {
-                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
-            }
+            mBpfNetMaps.updateUidLockdownRule(uid, add);
         } catch (ServiceSpecificException e) {
             loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
         }
@@ -1259,7 +1256,7 @@
         pw.println("Lockdown filtering rules:");
         pw.increaseIndent();
         for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
-            pw.println("UIDs: " + range.toString());
+            pw.println("UIDs: " + range);
         }
         pw.decreaseIndent();
 
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 7725bb0..03e1cc4 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -53,7 +53,6 @@
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
@@ -9520,38 +9519,28 @@
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testLockdownSetFirewallUidRule() throws Exception {
-        // For ConnectivityService#setAlwaysOnVpnPackage.
-        mServiceContext.setPermission(
-                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
-        // Needed to call Vpn#setAlwaysOnPackage.
-        mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
-        // Needed to call Vpn#isAlwaysOnPackageSupported.
-        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
-
+        final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
         // Enable Lockdown
-        final ArrayList<String> allowList = new ArrayList<>();
-        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
-                true /* lockdown */, allowList);
+        mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
         waitForIdle();
 
         // Lockdown rule is set to apps uids
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
 
         reset(mBpfNetMaps);
 
         // Disable lockdown
-        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
-                allowList);
+        mCm.setRequireVpnForUids(false /* requireVPN */, lockdownRange);
         waitForIdle();
 
         // Lockdown rule is removed from apps uids
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
 
         // Interface rules are not changed by Lockdown mode enable/disable
         verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
@@ -10532,27 +10521,28 @@
         assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
     }
 
-    @Test
-    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName("tun0");
-        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
-        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+    private void checkInterfaceFilteringRuleWithNullInterface(final LinkProperties lp,
+            final int uid) throws Exception {
         // The uid range needs to cover the test app so the network is visible to it.
         final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
-        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
-        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
+        mMockVpn.establish(lp, uid, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, uid);
 
         if (SdkLevel.isAtLeastT()) {
-            // On T and above, A connected Legacy VPN should have interface rules with null
-            // interface. Null Interface is a wildcard and this accepts traffic from all the
-            // interfaces. There are two expected invocations, one during the VPN initial
+            // On T and above, VPN should have rules for null interface. Null Interface is a
+            // wildcard and this accepts traffic from all the interfaces.
+            // There are two expected invocations, one during the VPN initial
             // connection, one during the VPN LinkProperties update.
             ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
             verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
                     eq(null) /* iface */, uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
-            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            if (uid == VPN_UID) {
+                assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
+                assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
+            } else {
+                assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+                assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            }
             assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
                     vpnRange);
 
@@ -10561,50 +10551,37 @@
 
             // Disconnected VPN should have interface rules removed
             verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            if (uid == VPN_UID) {
+                assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+            } else {
+                assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            }
             assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
         } else {
-            // Before T, Legacy VPN should not have interface rules.
+            // Before T, rules are not configured for null interface.
             verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
         }
     }
 
     @Test
+    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        // Legacy VPN should have interface filtering with null interface.
+        checkInterfaceFilteringRuleWithNullInterface(lp, Process.SYSTEM_UID);
+    }
+
+    @Test
     public void testLocalIpv4OnlyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
-        // The uid range needs to cover the test app so the network is visible to it.
-        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
-        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
-        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
-
-        if (SdkLevel.isAtLeastT()) {
-            // IPv6 unreachable route should not be misinterpreted as a default route
-            // On T and above, A connected VPN that does not provide a default route should have
-            // interface rules with null interface. Null Interface is a wildcard and this accepts
-            // traffic from all the interfaces. There are two expected invocations, one during the
-            // VPN initial connection, one during the VPN LinkProperties update.
-            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
-            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
-                    eq(null) /* iface */, uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
-            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
-            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
-                    vpnRange);
-
-            mMockVpn.disconnect();
-            waitForIdle();
-
-            // Disconnected VPN should have interface rules removed
-            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
-            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
-        } else {
-            // Before T, VPN with IPv6 unreachable route should not have interface rules.
-            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
-        }
+        // VPN that does not provide a default route should have interface filtering with null
+        // interface.
+        checkInterfaceFilteringRuleWithNullInterface(lp, VPN_UID);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index ecd17ba..354e79a 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -30,9 +30,6 @@
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
-import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -698,7 +695,8 @@
         mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1},
                 SYSTEM_APPID1);
 
-        final List<PackageInfo> pkgs = List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
+        final List<PackageInfo> pkgs = List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, SYSTEM_APP_UID21, CHANGE_NETWORK_STATE));
         doReturn(pkgs).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
@@ -764,9 +762,10 @@
                 MOCK_APPID1);
     }
 
-    private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+    private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
             throws Exception {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
@@ -774,7 +773,7 @@
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11);
         mPermissionMonitor.startMonitoring();
-        // Every app on user 0 except MOCK_UID12 are under VPN.
+        // Every app on user 0 except MOCK_UID12 is subject to the VPN.
         final Set<UidRange> vpnRange1 = Set.of(
                 new UidRange(0, MOCK_UID12 - 1),
                 new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1));
@@ -811,18 +810,19 @@
 
     @Test
     public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
-        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+        doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
     }
 
     @Test
     public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
             throws Exception {
-        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+        doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
     }
 
     private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
             Exception {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
@@ -857,155 +857,149 @@
 
     @Test
     public void testLockdownUidFilteringWithLockdownEnableDisable() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
-        // Every app on user 0 except MOCK_UID12 are under VPN.
-        final UidRange[] vpnRange1 = {
+        // Every app on user 0 except MOCK_UID12 is subject to the VPN.
+        final UidRange[] lockdownRange = {
                 new UidRange(0, MOCK_UID12 - 1),
                 new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
         };
 
-        // Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
-        verify(mBpfNetMaps)
-                .setUidRule(
-                        eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
+        // Add Lockdown uid range, expect a rule to be set up for MOCK_UID11 and VPN_UID
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range, expect rules to be torn down
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
         assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
     }
 
     @Test
     public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
-        // MOCK_UID11 is under VPN.
+        // MOCK_UID11 is subject to the VPN.
         final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
-        final UidRange[] vpnRange = {range};
+        final UidRange[] lockdownRange = {range};
 
         // Add Lockdown uid range at 1st time, expect a rule to be set up
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
         // already has the rule
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
-        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
         // the range 2 times.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
         // twice and we removed twice.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
         assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
     }
 
     @Test
     public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
-        // MOCK_UID11 is under VPN.
+        // MOCK_UID11 is subject to the VPN.
         final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
-        final UidRange[] vpnRangeDuplicates = {range, range};
-        final UidRange[] vpnRange = {range};
+        final UidRange[] lockdownRangeDuplicates = {range, range};
+        final UidRange[] lockdownRange = {range};
 
         // Add Lockdown uid ranges which contains duplicated uid ranges
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRangeDuplicates);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
         // ranges we added contains duplicated uid ranges.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(), anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
         assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
     }
 
     @Test
     public void testLockdownUidFilteringWithInstallAndUnInstall() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
 
         mPermissionMonitor.startMonitoring();
-        final UidRange[] vpnRange = {
+        final UidRange[] lockdownRange = {
                 UidRange.createForUser(MOCK_USER1),
                 UidRange.createForUser(MOCK_USER2)
         };
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+
+        reset(mBpfNetMaps);
 
         // Installing package should add Lockdown rules
         addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
-                        eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID21, true /* add */);
 
         reset(mBpfNetMaps);
 
         // Uninstalling package should remove Lockdown rules
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
-        verify(mBpfNetMaps, never())
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
-                        eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
     }
 
     // Normal package add/remove operations will trigger multiple intent for uids corresponding to
@@ -1329,7 +1323,8 @@
     public void testOnExternalApplicationsAvailable() throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // and have different uids. There has no permission for both uids.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
@@ -1387,7 +1382,8 @@
             throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // storage and shared on MOCK_UID11. There has no permission for MOCK_UID11.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
@@ -1413,7 +1409,8 @@
         // Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
         // MOCK_PACKAGE2 is installed on device. These two packages are shared on MOCK_UID11.
         // MOCK_UID11 has NETWORK and INTERNET permissions.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11, CHANGE_NETWORK_STATE, INTERNET)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 5a4ad87..acdc48a 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -95,13 +95,16 @@
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.net.ConnectivityResources;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsSession;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
 import android.net.NetworkStateSnapshot;
 import android.net.NetworkStats;
 import android.net.NetworkStatsCollection;
@@ -128,6 +131,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.net.module.util.IBpfMap;
@@ -165,6 +169,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -247,6 +252,8 @@
     private @Mock PersistentInt mImportLegacyAttemptsCounter;
     private @Mock PersistentInt mImportLegacySuccessesCounter;
     private @Mock PersistentInt mImportLegacyFallbacksCounter;
+    private @Mock Resources mResources;
+    private Boolean mIsDebuggable;
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -307,6 +314,12 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+
+        // Setup mock resources.
+        final Context mockResContext = mock(Context.class);
+        doReturn(mResources).when(mockResContext).getResources();
+        ConnectivityResources.setResourcesContextForTest(mockResContext);
+
         final Context context = InstrumentationRegistry.getContext();
         mServiceContext = new MockContext(context);
         when(mLocationPermissionChecker.checkCallersLocationPermission(
@@ -462,6 +475,11 @@
             public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
                 return mAppUidStatsMap;
             }
+
+            @Override
+            public boolean isDebuggable() {
+                return mIsDebuggable == Boolean.TRUE;
+            }
         };
     }
 
@@ -1898,6 +1916,99 @@
         //  will decrease the retry counter by 1.
     }
 
+    @Test
+    public void testDataMigration_differentFromFallback() throws Exception {
+        assertStatsFilesExist(false);
+        expectDefaultSettings();
+
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+        forcePollAndWaitForIdle();
+        // Simulate shutdown to force persisting data
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(true);
+
+        // Move the files to the legacy directory to simulate an import from old data
+        for (File f : mStatsDir.listFiles()) {
+            Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+        }
+        assertStatsFilesExist(false);
+
+        // Prepare some unexpected data.
+        final NetworkIdentity testWifiIdent = new NetworkIdentity.Builder().setType(TYPE_WIFI)
+                .setWifiNetworkKey(TEST_WIFI_NETWORK_KEY).build();
+        final NetworkStatsCollection.Key unexpectedUidAllkey = new NetworkStatsCollection.Key(
+                Set.of(testWifiIdent), UID_ALL, SET_DEFAULT, 0);
+        final NetworkStatsCollection.Key unexpectedUidBluekey = new NetworkStatsCollection.Key(
+                Set.of(testWifiIdent), UID_BLUE, SET_DEFAULT, 0);
+        final NetworkStatsHistory unexpectedHistory = new NetworkStatsHistory
+                .Builder(965L /* bucketDuration */, 1)
+                .addEntry(new NetworkStatsHistory.Entry(TEST_START, 3L, 55L, 4L, 31L, 10L, 5L))
+                .build();
+
+        // Simulate the platform stats collection somehow is different from what is read from
+        // the fallback method. The service should read them as is. This usually happens when an
+        // OEM has changed the implementation of NetworkStatsDataMigrationUtils inside the platform.
+        final NetworkStatsCollection summaryCollection =
+                getLegacyCollection(PREFIX_XT, false /* includeTags */);
+        summaryCollection.recordHistory(unexpectedUidAllkey, unexpectedHistory);
+        final NetworkStatsCollection uidCollection =
+                getLegacyCollection(PREFIX_UID, false /* includeTags */);
+        uidCollection.recordHistory(unexpectedUidBluekey, unexpectedHistory);
+        mPlatformNetworkStatsCollection.put(PREFIX_DEV, summaryCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_XT, summaryCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_UID, uidCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+                getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+        // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+        assertStatsFilesExist(false);
+
+        // Set the flag and reboot, verify the imported data is not there until next boot.
+        mStoreFilesInApexData = true;
+        mImportLegacyTargetAttempts = 3;
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(false);
+
+        // Boot through systemReady() again.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+
+        // Verify the result read from public API matches the result returned from the importer.
+        assertNetworkTotal(sTemplateWifi, 1024L + 55L, 8L + 4L, 2048L + 31L, 16L + 10L, 0 + 5);
+        assertUidTotal(sTemplateWifi, UID_BLUE,
+                128L + 55L, 1L + 4L, 128L + 31L, 1L + 10L, 0 + 5);
+        assertStatsFilesExist(true);
+        verify(mImportLegacyAttemptsCounter).set(3);
+        verify(mImportLegacySuccessesCounter).set(1);
+    }
+
+    @Test
+    public void testShouldRunComparison() {
+        // TODO(b/233752318): For now it should always true to collect signal from beta users.
+        //  Should change to the default behavior (true if userdebug rom) before formal release.
+        for (int testValue : Set.of(-1, 0, 1, 2)) {
+            doReturn(testValue).when(mResources)
+                    .getInteger(R.integer.config_netstats_validate_import);
+            assertEquals(true, mService.shouldRunComparison());
+        }
+    }
+
     private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
             boolean includeTags) {
         final NetworkStats.NonMonotonicObserver observer =