Merge " Add unit tests" into tm-mainline-prod
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 17c18c9..44f76de 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -53,15 +53,18 @@
 
 // For maps netd does not need to access
 #define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0060)
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
 
 // For maps netd only needs read only access to
 #define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0460)
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
 
 // For maps netd needs to be able to read and write
 #define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
-    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0660)
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0660)
 
 // Bpf map arrays on creation are preinitialized to 0 and do not support deletion of a key,
 // see: kernel/bpf/arraymap.c array_map_delete_elem() returns -EINVAL (from both syscall and ebpf)
@@ -81,6 +84,20 @@
 /* never actually used from ebpf */
 DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
 
+// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
+#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+
+// programs that need to be usable by netd, but not by netutils_wrappers
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+                        KVER_NONE, KVER_INF, false, "fs_bpf_netd_readonly", "")
+
+// programs that only need to be usable by the system server
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+                        KVER_NONE, KVER_INF, false, "fs_bpf_net_shared", "")
+
 static __always_inline int is_system_uid(uint32_t uid) {
     // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
     // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
@@ -313,18 +330,18 @@
     return match;
 }
 
-DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
+DEFINE_NETD_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, BPF_INGRESS);
 }
 
-DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
+DEFINE_NETD_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, BPF_EGRESS);
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon does not generate new traffic, all its traffic is accounted for already
     // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -344,7 +361,7 @@
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
     // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -356,7 +373,8 @@
     return BPF_MATCH;
 }
 
-DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+                    tc_bpf_ingress_account_prog)
 (struct __sk_buff* skb) {
     if (is_received_skb(skb)) {
         // Account for ingress traffic before tc drops it.
@@ -367,7 +385,7 @@
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     if (is_system_uid(sock_uid)) return BPF_MATCH;
@@ -385,7 +403,7 @@
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
-DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -393,7 +411,7 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
+DEFINE_NETD_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
 (struct bpf_sock* sk) {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
new file mode 100644
index 0000000..1311765
--- /dev/null
+++ b/framework/jarjar-excludes.txt
@@ -0,0 +1,25 @@
+# INetworkStatsProvider / INetworkStatsProviderCallback are referenced from net-tests-utils, which
+# may be used by tests that do not apply connectivity jarjar rules.
+# TODO: move files to a known internal package (like android.net.connectivity.visiblefortesting)
+# so that they do not need jarjar
+android\.net\.netstats\.provider\.INetworkStatsProvider(\$.+)?
+android\.net\.netstats\.provider\.INetworkStatsProviderCallback(\$.+)?
+
+# INetworkAgent / INetworkAgentRegistry are used in NetworkAgentTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.INetworkAgent(\$.+)?
+android\.net\.INetworkAgentRegistry(\$.+)?
+
+# IConnectivityDiagnosticsCallback used in ConnectivityDiagnosticsManagerTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+
+
+# KeepaliveUtils is used by ConnectivityManager CTS
+# TODO: move into service-connectivity so framework-connectivity stops using
+# ServiceConnectivityResources (callers need high permissions to find/query the resource apk anyway)
+# and have a ConnectivityManager test API instead
+android\.net\.util\.KeepaliveUtils(\$.+)?
+
+# TODO (b/217115866): add jarjar rules for Nearby
+android\.nearby\..+
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
index 2ecce47..d459329 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -31,10 +31,7 @@
 import com.android.server.nearby.common.ble.decode.FastPairDecoder;
 import com.android.server.nearby.common.ble.util.RangingUtils;
 import com.android.server.nearby.common.bloomfilter.BloomFilter;
-import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
 import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 import com.android.server.nearby.util.DataUtils;
@@ -120,7 +117,7 @@
                 Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
             }
         } else {
-            // Start to process bloom filter
+            // Start to process bloom filter. Yet to finish.
             try {
                 List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
                 byte[] bloomFilterByteArray = FastPairDecoder
@@ -130,73 +127,9 @@
                 if (bloomFilterByteArray == null || bloomFilterByteArray.length == 0) {
                     return;
                 }
-                for (Account account : accountList) {
-                    List<Data.FastPairDeviceWithAccountKey> listDevices =
-                            mPairDataProvider.loadFastPairDeviceWithAccountKey(account);
-                    Data.FastPairDeviceWithAccountKey recognizedDevice =
-                            findRecognizedDevice(listDevices,
-                                    new BloomFilter(bloomFilterByteArray,
-                                            new FastPairBloomFilterHasher()), bloomFilterSalt);
-
-                    if (recognizedDevice != null) {
-                        Log.d(TAG, "find matched device show notification to remind"
-                                + " user to pair");
-                        // Check the distance of the device if the distance is larger than the
-                        // threshold
-                        // do not show half sheet.
-                        if (!isNearby(fastPairDevice.getRssi(),
-                                recognizedDevice.getDiscoveryItem().getTxPower() == 0
-                                        ? fastPairDevice.getTxPower()
-                                        : recognizedDevice.getDiscoveryItem().getTxPower())) {
-                            return;
-                        }
-                        // Check if the device is already paired
-                        List<Cache.StoredFastPairItem> storedFastPairItemList =
-                                Locator.get(mContext, FastPairCacheManager.class)
-                                        .getAllSavedStoredFastPairItem();
-                        Cache.StoredFastPairItem recognizedStoredFastPairItem =
-                                findRecognizedDeviceFromCachedItem(storedFastPairItemList,
-                                        new BloomFilter(bloomFilterByteArray,
-                                                new FastPairBloomFilterHasher()), bloomFilterSalt);
-                        if (recognizedStoredFastPairItem != null) {
-                            // The bloomfilter is recognized in the cache so the device is paired
-                            // before
-                            Log.d(TAG, "bloom filter is recognized in the cache");
-                            continue;
-                        } else {
-                            Log.d(TAG, "bloom filter is recognized not paired before should"
-                                    + "show subsequent pairing notification");
-                            if (mIsFirst) {
-                                mIsFirst = false;
-                                // Get full info from api the initial request will only return
-                                // part of the info due to size limit.
-                                List<Data.FastPairDeviceWithAccountKey> resList =
-                                        mPairDataProvider.loadFastPairDeviceWithAccountKey(account,
-                                                List.of(recognizedDevice.getAccountKey()
-                                                        .toByteArray()));
-                                if (resList != null && resList.size() > 0) {
-                                    //Saved device from footprint does not have ble address so
-                                    // fill ble address with current scan result.
-                                    Cache.StoredDiscoveryItem storedDiscoveryItem =
-                                            resList.get(0).getDiscoveryItem().toBuilder()
-                                                    .setMacAddress(
-                                                            fastPairDevice.getBluetoothAddress())
-                                                    .build();
-                                    Locator.get(mContext, FastPairController.class).pair(
-                                            new DiscoveryItem(mContext, storedDiscoveryItem),
-                                            resList.get(0).getAccountKey().toByteArray(),
-                                            /** companionApp=*/null);
-                                }
-                            }
-                        }
-
-                        return;
-                    }
-                }
             } catch (IllegalStateException e) {
                 Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
             }
-
         }
     }
 
@@ -249,5 +182,4 @@
     boolean isNearby(int rssi, int txPower) {
         return RangingUtils.distanceFromRssiAndTxPower(rssi, txPower) < NEARBY_DISTANCE_THRESHOLD;
     }
-
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index aacb6d8..a2da967 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -42,7 +42,6 @@
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         CredentialElement element = new CredentialElement(KEY, VALUE);
-
         assertThat(element.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
     }
@@ -58,9 +57,31 @@
         CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
                 parcel);
         parcel.recycle();
-
         assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        CredentialElement element = new CredentialElement(KEY, VALUE);
+        assertThat(element.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEqual() {
+        CredentialElement element1 = new CredentialElement(KEY, VALUE);
+        CredentialElement element2 = new CredentialElement(KEY, VALUE);
+        assertThat(element1.equals(element2)).isTrue();
+        assertThat(element1.hashCode()).isEqualTo(element2.hashCode());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        CredentialElement [] elements =
+                CredentialElement.CREATOR.newArray(2);
+        assertThat(elements.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
index dd9cbb0..d9323b0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
@@ -42,8 +42,11 @@
 
     private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
     private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+    private static final byte[] SALT = new byte[] {1, 2, 3, 4};
     private static final String FAST_PAIR_MODEL_ID = "1234";
     private static final int RSSI = -60;
+    private static final int TX_POWER = -10;
+    private static final int ACTION = 1;
 
     private NearbyDeviceParcelable.Builder mBuilder;
 
@@ -66,11 +69,11 @@
     public void testToString() {
         PublicCredential publicCredential =
                 new PublicCredential.Builder(
-                                new byte[] {1},
-                                new byte[] {2},
-                                new byte[] {3},
-                                new byte[] {4},
-                                new byte[] {5})
+                        new byte[] {1},
+                        new byte[] {2},
+                        new byte[] {3},
+                        new byte[] {4},
+                        new byte[] {5})
                         .build();
         NearbyDeviceParcelable nearbyDeviceParcelable =
                 mBuilder.setFastPairModelId(null)
@@ -88,20 +91,36 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 33, codeName = "T")
-    public void test_defaultNullFields() {
+    public void testNullFields() {
+        PublicCredential publicCredential =
+                new PublicCredential.Builder(
+                        new byte[] {1},
+                        new byte[] {2},
+                        new byte[] {3},
+                        new byte[] {4},
+                        new byte[] {5})
+                        .build();
         NearbyDeviceParcelable nearbyDeviceParcelable =
                 new NearbyDeviceParcelable.Builder()
                         .setMedium(NearbyDevice.Medium.BLE)
+                        .setPublicCredential(publicCredential)
+                        .setAction(ACTION)
                         .setRssi(RSSI)
+                        .setTxPower(TX_POWER)
+                        .setSalt(SALT)
                         .build();
 
         assertThat(nearbyDeviceParcelable.getName()).isNull();
         assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
         assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
         assertThat(nearbyDeviceParcelable.getData()).isNull();
-
         assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
         assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+        assertThat(nearbyDeviceParcelable.getAction()).isEqualTo(ACTION);
+        assertThat(nearbyDeviceParcelable.getPublicCredential()).isEqualTo(publicCredential);
+        assertThat(nearbyDeviceParcelable.getSalt()).isEqualTo(SALT);
+        assertThat(nearbyDeviceParcelable.getScanType()).isEqualTo(SCAN_TYPE_NEARBY_PRESENCE);
+        assertThat(nearbyDeviceParcelable.getTxPower()).isEqualTo(TX_POWER);
     }
 
     @Test
@@ -141,7 +160,6 @@
     @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testWriteParcel_nullBluetoothAddress() {
         NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
-
         Parcel parcel = Parcel.obtain();
         nearbyDeviceParcelable.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
@@ -151,4 +169,28 @@
 
         assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void describeContents() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+        assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testEqual() {
+        NearbyDeviceParcelable nearbyDeviceParcelable1 = mBuilder.setBluetoothAddress(null).build();
+        NearbyDeviceParcelable nearbyDeviceParcelable2 = mBuilder.setBluetoothAddress(null).build();
+        assertThat(nearbyDeviceParcelable1.equals(nearbyDeviceParcelable2)).isTrue();
+        assertThat(nearbyDeviceParcelable1.hashCode())
+                .isEqualTo(nearbyDeviceParcelable2.hashCode());
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        NearbyDeviceParcelable[] nearbyDeviceParcelables =
+                NearbyDeviceParcelable.CREATOR.newArray(2);
+        assertThat(nearbyDeviceParcelables.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
index f37800a..675969a 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -56,4 +56,30 @@
         assertThat(fastPairDevice.getMediums()).contains(1);
         assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEqual() {
+        FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+                .addMedium(NearbyDevice.Medium.BLE)
+                .setRssi(-60)
+                .build();
+        FastPairDevice fastPairDevice2 = new FastPairDevice.Builder()
+                .addMedium(NearbyDevice.Medium.BLE)
+                .setRssi(-60)
+                .build();
+        assertThat(fastPairDevice1.equals(fastPairDevice2)).isTrue();
+        assertThat(fastPairDevice1.hashCode()).isEqualTo(fastPairDevice2.hashCode());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testToString() {
+        FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+                .addMedium(NearbyDevice.Medium.BLE)
+                .setRssi(-60)
+                .build();
+
+        assertThat(fastPairDevice1.toString()).isEqualTo("");
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 7696a61..462d05a 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -158,6 +158,15 @@
         mNearbyManager.stopBroadcast(callback);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void setFastPairScanEnabled() {
+        mNearbyManager.setFastPairScanEnabled(mContext, true);
+        assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isTrue();
+        mNearbyManager.setFastPairScanEnabled(mContext, false);
+        assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isFalse();
+    }
+
     private void enableBluetooth() {
         BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
         BluetoothAdapter bluetoothAdapter = manager.getAdapter();
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index 1daa410..be0c4b7 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -114,4 +114,18 @@
         assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
 
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+        assertThat(broadcastRequest.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PresenceBroadcastRequest[] presenceBroadcastRequests =
+                PresenceBroadcastRequest.CREATOR.newArray(2);
+        assertThat(presenceBroadcastRequests.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index 5fefc68..ab0b7b4 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -104,4 +104,24 @@
         assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
         assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceDevice device =
+                new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+                        .addExtendedProperty(new DataElement(KEY, VALUE))
+                        .setRssi(RSSI)
+                        .addMedium(MEDIUM)
+                        .setName(DEVICE_NAME)
+                        .build();
+        assertThat(device.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PresenceDevice[] devices =
+                PresenceDevice.CREATOR.newArray(2);
+        assertThat(devices.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index b7fe40a..806e7c0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -91,4 +91,19 @@
         assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
         assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceScanFilter filter = mBuilder.build();
+        assertThat(filter.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        PresenceScanFilter[] filters =
+                PresenceScanFilter.CREATOR.newArray(2);
+        assertThat(filters.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index f05f65f..fa8c954 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -99,4 +99,19 @@
         assertThat(credentialElement.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void describeContents() {
+        PrivateCredential credential = mBuilder.build();
+        assertThat(credential.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testCreatorNewArray() {
+        PrivateCredential[]  credentials =
+                PrivateCredential.CREATOR.newArray(2);
+        assertThat(credentials.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index 11bbacc..05a4598 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -161,4 +161,19 @@
                         .build();
         assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PublicCredential credential = mBuilder.build();
+        assertThat(credential.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        PublicCredential[] credentials  =
+        PublicCredential.CREATOR.newArray(2);
+        assertThat(credentials.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index 21f3d28..de4b1c3 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -171,6 +171,23 @@
         assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(RSSI);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        ScanRequest request = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_FAST_PAIR)
+                .build();
+        assertThat(request.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        ScanRequest[] requests =
+                ScanRequest.CREATOR.newArray(2);
+        assertThat(requests.length).isEqualTo(2);
+    }
+
     private static PresenceScanFilter getPresenceScanFilter() {
         final byte[] secretId = new byte[]{1, 2, 3, 4};
         final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
index b8ada31..c4a9729 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
@@ -24,6 +24,7 @@
 import android.bluetooth.BluetoothAssignedNumbers;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.le.ScanFilter;
+import android.os.Parcel;
 import android.os.ParcelUuid;
 import android.util.SparseArray;
 
@@ -547,6 +548,54 @@
         assertFilterValuesEqual(nearbyFilter, osFilter);
     }
 
+    @Test
+    public void describeContents() {
+        BleFilter nearbyFilter = createTestFilter();
+        assertThat(nearbyFilter.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testHashCode() {
+        BleFilter nearbyFilter = createTestFilter();
+        BleFilter compareFilter = new BleFilter("BERT", "00:11:22:33:AA:BB",
+                new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+                new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")),
+                ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+                new byte[] {0x51, 0x64}, new byte[] {0x00, (byte) 0xFF},
+                BluetoothAssignedNumbers.GOOGLE, new byte[] {0x00, 0x10},
+                new byte[] {0x00, (byte) 0xFF});
+        assertThat(nearbyFilter.hashCode()).isEqualTo(compareFilter.hashCode());
+    }
+
+    @Test
+    public void testToString() {
+        BleFilter nearbyFilter = createTestFilter();
+        assertThat(nearbyFilter.toString()).isEqualTo("BleFilter [deviceName=BERT,"
+                + " deviceAddress=00:11:22:33:AA:BB, uuid=0000fea0-0000-1000-8000-00805f9b34fb,"
+                + " uuidMask=0fffffff-ffff-0fff-ffff-ffffffffffff,"
+                + " serviceDataUuid=0000110b-0000-1000-8000-00805f9b34fb,"
+                + " serviceData=[81, 100], serviceDataMask=[0, -1],"
+                + " manufacturerId=224, manufacturerData=[0, 16], manufacturerDataMask=[0, -1]]");
+    }
+
+    @Test
+    public void testParcel() {
+        BleFilter nearbyFilter = createTestFilter();
+        Parcel parcel = Parcel.obtain();
+        nearbyFilter.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        BleFilter compareFilter = BleFilter.CREATOR.createFromParcel(
+                parcel);
+        parcel.recycle();
+        assertThat(compareFilter.getDeviceName()).isEqualTo("BERT");
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        BleFilter[] nearbyFilters  = BleFilter.CREATOR.newArray(2);
+        assertThat(nearbyFilters.length).isEqualTo(2);
+    }
+
     private static boolean matches(
             BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecord) {
         return filter.matches(new BleSighting(device,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
index aa52e26..3f9a259 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
@@ -34,6 +34,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
 import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
@@ -245,5 +248,48 @@
         assertThat(record).isNotEqualTo(record2);
         assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
     }
+
+    @Test
+    public void testFields() {
+        BleRecord record = BleRecord.parseFromBytes(BEACON);
+        assertThat(byteString(record.getManufacturerSpecificData()))
+                .isEqualTo("  0215F7826DA64FA24E988024BC5B71E0893E44D02522B3");
+        assertThat(
+                byteString(record.getServiceData(
+                        ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB"))))
+                .isEqualTo("[null]");
+        assertThat(record.getTxPowerLevel()).isEqualTo(-12);
+        assertThat(record.toString()).isEqualTo(
+                "BleRecord [advertiseFlags=6, serviceUuids=[], "
+                        + "manufacturerSpecificData={76=[2, 21, -9, -126, 109, -90, 79, -94, 78,"
+                        + " -104, -128, 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]},"
+                        + " serviceData={0000d00d-0000-1000-8000-00805f9b34fb"
+                        + "=[116, 109, 77, 107, 50, 54, 100]},"
+                        + " txPowerLevel=-12, deviceName=Kontakt]");
+    }
+
+    private static String byteString(SparseArray<byte[]> bytesArray) {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < bytesArray.size(); i++) {
+            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
+            builder.append(byteString(bytesArray.valueAt(i)));
+        }
+        return builder.toString();
+    }
+
+    private static String byteString(byte[] bytes) {
+        if (bytes == null) {
+            return "[null]";
+        } else {
+            final char[] hexArray = "0123456789ABCDEF".toCharArray();
+            char[] hexChars = new char[bytes.length * 2];
+            for (int i = 0; i < bytes.length; i++) {
+                int v = bytes[i] & 0xFF;
+                hexChars[i * 2] = hexArray[v >>> 4];
+                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+            }
+            return new String(hexChars);
+        }
+    }
 }
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
index 05dab09..b318842 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
@@ -22,6 +22,7 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
 
 import org.junit.Test;
 
@@ -81,7 +82,61 @@
         assertThat(sighting.getNormalizedRSSI()).isEqualTo(RSSI + defaultRssiOffset);
     }
 
-    /** Builds a BleSighting instance which will correctly match filters by device name. */
+    @Test
+    public void testFields() {
+        BleSighting sighting =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        assertThat(byteString(sighting.getBleRecordBytes()))
+                .isEqualTo("080964657669636531");
+        assertThat(sighting.getRssi()).isEqualTo(RSSI);
+        assertThat(sighting.getTimestampMillis()).isEqualTo(TIME_EPOCH_MILLIS);
+        assertThat(sighting.getTimestampNanos())
+                .isEqualTo(TimeUnit.MILLISECONDS.toNanos(TIME_EPOCH_MILLIS));
+        assertThat(sighting.toString()).isEqualTo(
+                "BleSighting{device=00:11:22:33:44:55,"
+                        + " bleRecord=BleRecord [advertiseFlags=-1,"
+                        + " serviceUuids=[],"
+                        + " manufacturerSpecificData={}, serviceData={},"
+                        + " txPowerLevel=-2147483648,"
+                        + " deviceName=device1],"
+                        + " rssi=1,"
+                        + " timestampNanos=123456000000}");
+    }
+
+    @Test
+    public void testParcelable() {
+        BleSighting sighting =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        Parcel dest = Parcel.obtain();
+        sighting.writeToParcel(dest, 0);
+        dest.setDataPosition(0);
+        BleSighting compareSighting = BleSighting.CREATOR.createFromParcel(dest);
+        assertThat(sighting.getRssi()).isEqualTo(RSSI);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        BleSighting[]  sightings =
+                BleSighting.CREATOR.newArray(2);
+        assertThat(sightings.length).isEqualTo(2);
+    }
+
+    private static String byteString(byte[] bytes) {
+        if (bytes == null) {
+            return "[null]";
+        } else {
+            final char[] hexArray = "0123456789ABCDEF".toCharArray();
+            char[] hexChars = new char[bytes.length * 2];
+            for (int i = 0; i < bytes.length; i++) {
+                int v = bytes[i] & 0xFF;
+                hexChars[i * 2] = hexArray[v >>> 4];
+                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+            }
+            return new String(hexChars);
+        }
+    }
+
+        /** Builds a BleSighting instance which will correctly match filters by device name. */
     private static BleSighting buildBleSighting(
             BluetoothDevice bluetoothDevice, String deviceName, long timeEpochMillis, int rssi) {
         byte[] nameBytes = deviceName.getBytes(UTF_8);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
new file mode 100644
index 0000000..9a9181d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.decode;
+
+import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class BeaconDecoderTest {
+    private BeaconDecoder mBeaconDecoder = new FastPairDecoder();;
+
+    @Test
+    public void testFields() {
+        assertThat(mBeaconDecoder.getTelemetry(parseFromBytes(getFastPairRecord()))).isNull();
+        assertThat(mBeaconDecoder.getUrl(parseFromBytes(getFastPairRecord()))).isNull();
+        assertThat(mBeaconDecoder.supportsBeaconIdAndTxPower(parseFromBytes(getFastPairRecord())))
+                .isTrue();
+        assertThat(mBeaconDecoder.supportsTxPower()).isTrue();
+    }
+}
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 d6a846d..30df81f 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
@@ -21,8 +21,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import junit.framework.TestCase;
-
 import org.junit.Test;
 
 import java.util.Arrays;
@@ -30,7 +28,7 @@
 /**
  * Unit-tests for the {@link BloomFilter} class.
  */
-public class BloomFilterTest extends TestCase {
+public class BloomFilterTest {
     private static final int BYTE_ARRAY_LENGTH = 100;
 
     private final BloomFilter mBloomFilter =
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
index 346a961..01f2f46 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
@@ -16,24 +16,33 @@
 
 package com.android.server.nearby.fastpair;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.accounts.Account;
 import android.content.Context;
 import android.nearby.FastPairDevice;
 
+import com.android.server.nearby.common.bloomfilter.BloomFilter;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
 import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import service.proto.Cache;
+import service.proto.Data;
 import service.proto.Rpcs;
 
 public class FastPairAdvHandlerTest {
@@ -45,11 +54,22 @@
     private FastPairHalfSheetManager mFastPairHalfSheetManager;
     @Mock
     private FastPairNotificationManager mFastPairNotificationManager;
+    @Mock
+    private BloomFilter mBloomFilter;
+    @Mock
+    Cache.StoredDiscoveryItem mStoredDiscoveryItem;
+    @Mock
+    Cache.StoredFastPairItem mStoredFastPairItem;
+    @Mock
+    Data.FastPairDeviceWithAccountKey mFastPairDeviceWithAccountKey;
+    private static final byte[] ACCOUNT_KEY = new byte[] {0, 1, 2};
     private static final String BLUETOOTH_ADDRESS = "AA:BB:CC:DD";
     private static final int CLOSE_RSSI = -80;
     private static final int FAR_AWAY_RSSI = -120;
     private static final int TX_POWER = -70;
     private static final byte[] INITIAL_BYTE_ARRAY = new byte[]{0x01, 0x02, 0x03};
+    private static final byte[] SALT = new byte[]{0x01};
+    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
 
     LocatorContextWrapper mLocatorContextWrapper;
     FastPairAdvHandler mFastPairAdvHandler;
@@ -99,7 +119,7 @@
     }
 
     @Test
-    public void testSubsequentBroadcast() {
+    public void testSubsequentBroadcast_notShowHalfSheet() {
         byte[] fastPairRecordWithBloomFilter =
                 new byte[]{
                         (byte) 0x02,
@@ -132,4 +152,44 @@
 
         verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
     }
+
+    @Test
+    public void testFindRecognizedDevice_bloomFilterNotContains_notFound() {
+        when(mFastPairDeviceWithAccountKey.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
+
+        assertThat(FastPairAdvHandler.findRecognizedDevice(
+                ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNull();
+    }
+
+    @Test
+    public void testFindRecognizedDevice_bloomFilterContains_found() {
+        when(mFastPairDeviceWithAccountKey.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
+
+        assertThat(FastPairAdvHandler.findRecognizedDevice(
+                ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNotNull();
+    }
+
+    @Test
+    public void testFindRecognizedDeviceFromCachedItem_bloomFilterNotContains_notFound() {
+        when(mStoredFastPairItem.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
+
+        assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
+                ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNull();
+    }
+
+    @Test
+    public void testFindRecognizedDeviceFromCachedItem_bloomFilterContains_found() {
+        when(mStoredFastPairItem.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
+
+        assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
+                ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNotNull();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
index 58e4c47..b51a295 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.fastpair.halfsheet;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +27,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
@@ -32,6 +35,8 @@
 import android.content.pm.ResolveInfo;
 import android.os.UserHandle;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.FastPairController;
@@ -50,13 +55,13 @@
 public class FastPairHalfSheetManagerTest {
     private static final String BLEADDRESS = "11:22:44:66";
     private static final String NAME = "device_name";
+    private static final int PASSKEY = 1234;
     private FastPairHalfSheetManager mFastPairHalfSheetManager;
     private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+    @Mock private Context mContext;
     @Mock
     LocatorContextWrapper mContextWrapper;
     @Mock
-    ResolveInfo mResolveInfo;
-    @Mock
     PackageManager mPackageManager;
     @Mock
     Locator mLocator;
@@ -66,7 +71,8 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-
+        when(mContext.getContentResolver()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
         mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
                 .setAddress(BLEADDRESS)
                 .setDeviceName(NAME)
@@ -133,4 +139,24 @@
         verify(mContextWrapper, never())
                 .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
     }
+
+    @Test
+    public void getHalfSheetForegroundState() {
+        mFastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        assertThat(mFastPairHalfSheetManager.getHalfSheetForegroundState()).isTrue();
+    }
+
+    @Test
+    public void testEmptyMethods() {
+        mFastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        mFastPairHalfSheetManager.destroyBluetoothPairController();
+        mFastPairHalfSheetManager.disableDismissRunnable();
+        mFastPairHalfSheetManager.notifyPairingProcessDone(true, BLEADDRESS, null);
+        mFastPairHalfSheetManager.showPairingFailed();
+        mFastPairHalfSheetManager.showPairingHalfSheet(null);
+        mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLEADDRESS);
+        mFastPairHalfSheetManager.showPasskeyConfirmation(null, PASSKEY);
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
new file mode 100644
index 0000000..4fb6b37
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.fastpair.notification;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairNotificationManagerTest {
+
+    @Mock private Context mContext;
+    private static final boolean USE_LARGE_ICON = true;
+    private static final int NOTIFICATION_ID = 1;
+    private static final String COMPANION_APP = "companionApp";
+    private static final int BATTERY_LEVEL = 1;
+    private static final String DEVICE_NAME = "deviceName";
+    private static final String ADDRESS = "address";
+    private FastPairNotificationManager mFastPairNotificationManager;
+    private LocatorContextWrapper mLocatorContextWrapper;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+        when(mContext.getContentResolver()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+        mFastPairNotificationManager =
+                new FastPairNotificationManager(mLocatorContextWrapper, null,
+                        USE_LARGE_ICON, NOTIFICATION_ID);
+    }
+
+    @Test
+    public void  notifyPairingProcessDone() {
+        mFastPairNotificationManager.notifyPairingProcessDone(true, true,
+                "privateAddress", "publicAddress");
+    }
+
+    @Test
+    public void  showConnectingNotification() {
+        mFastPairNotificationManager.showConnectingNotification();
+    }
+
+    @Test
+    public void   showPairingFailedNotification() {
+        mFastPairNotificationManager.showPairingFailedNotification(new byte[]{1});
+    }
+
+    @Test
+    public void  showPairingSucceededNotification() {
+        mFastPairNotificationManager.showPairingSucceededNotification(COMPANION_APP,
+                BATTERY_LEVEL, DEVICE_NAME, ADDRESS);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
index 5e0ccbe..8e3e068 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -75,6 +75,15 @@
         assertThat(originalAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V0);
         assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+        assertThat(originalAdvertisement.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(originalAdvertisement.toString())
+                .isEqualTo("FastAdvertisement:<VERSION: 0, length: 19,"
+                        + " ltvFieldCount: 4,"
+                        + " identityType: 1,"
+                        + " identity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],"
+                        + " salt: [2, 3],"
+                        + " actions: [123],"
+                        + " txPower: 4");
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
new file mode 100644
index 0000000..e167cf4
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.util;
+
+import org.junit.Test;
+
+public class EnvironmentTest {
+
+    @Test
+    public void getNearbyDirectory() {
+        Environment.getNearbyDirectory();
+        Environment.getNearbyDirectory(1);
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 79802fb..c4ea9ae 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -488,8 +488,6 @@
             if (null != capabilities) {
                 setCapabilities(capabilities);
             }
-            // Send an abort callback if a request is filed before the previous one has completed.
-            maybeSendNetworkManagementCallbackForAbort();
             // TODO: Update this logic to only do a restart if required. Although a restart may
             //  be required due to the capabilities or ipConfiguration values, not all
             //  capabilities changes require a restart.
@@ -651,6 +649,8 @@
                 mIpClientCallback.awaitIpClientShutdown();
                 mIpClient = null;
             }
+            // Send an abort callback if an updateInterface request was in progress.
+            maybeSendNetworkManagementCallbackForAbort();
             mIpClientCallback = null;
 
             if (mNetworkAgent != null) {
@@ -662,7 +662,6 @@
 
         public void destroy() {
             mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
-            maybeSendNetworkManagementCallbackForAbort();
             stop();
             mRequests.clear();
         }
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 7cc19a0..31caabc 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -270,6 +270,8 @@
                     + ", ipConfig: " + ipConfig);
         }
 
+        // TODO: do the right thing if the interface was in server mode: either fail this operation,
+        // or take the interface out of server mode.
         final IpConfiguration localIpConfig = ipConfig == null
                 ? null : new IpConfiguration(ipConfig);
         if (ipConfig != null) {
@@ -580,8 +582,8 @@
         }
         if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
 
-        // TODO: avoid making an interface default if it has configured NetworkCapabilities.
-        if (mDefaultInterface == null) {
+        // Do not make an interface default if it has configured NetworkCapabilities.
+        if (mDefaultInterface == null && !mNetworkCapabilities.containsKey(iface)) {
             mDefaultInterface = iface;
         }
 
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index d99e164..7c801d7 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -78,6 +78,7 @@
 
     private final long mBucketDuration;
     private final boolean mOnlyTags;
+    private final boolean mWipeOnError;
 
     private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
     private NetworkStats mLastSnapshot;
@@ -102,6 +103,7 @@
         // slack to avoid overflow
         mBucketDuration = YEAR_IN_MILLIS;
         mOnlyTags = false;
+        mWipeOnError = true;
 
         mPending = null;
         mSinceBoot = new NetworkStatsCollection(mBucketDuration);
@@ -113,7 +115,8 @@
      * Persisted recorder.
      */
     public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
-            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
+            boolean wipeOnError) {
         mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
         mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
         mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -121,6 +124,7 @@
 
         mBucketDuration = bucketDuration;
         mOnlyTags = onlyTags;
+        mWipeOnError = wipeOnError;
 
         mPending = new NetworkStatsCollection(bucketDuration);
         mSinceBoot = new NetworkStatsCollection(bucketDuration);
@@ -552,7 +556,9 @@
             }
             mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
         }
-
-        mRotator.deleteAll();
+        // Delete all files if this recorder is set wipe on error.
+        if (mWipeOnError) {
+            mRotator.deleteAll();
+        }
     }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 2bf3ab9..424dcd9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -800,11 +800,14 @@
             mSystemReady = true;
 
             // create data recorders along with historical rotators
-            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir);
-            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir);
-            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir);
+            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
             mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
-                    mStatsDir);
+                    mStatsDir, true /* wipeOnError */);
 
             updatePersistThresholdsLocked();
 
@@ -869,12 +872,13 @@
 
     private NetworkStatsRecorder buildRecorder(
             String prefix, NetworkStatsSettings.Config config, boolean includeTags,
-            File baseDir) {
+            File baseDir, boolean wipeOnError) {
         final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
                 Context.DROPBOX_SERVICE);
         return new NetworkStatsRecorder(new FileRotator(
                 baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
-                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
+                wipeOnError);
     }
 
     @GuardedBy("mStatsLock")
@@ -971,12 +975,17 @@
         final NetworkStatsRecorder[] legacyRecorders;
         if (runComparison) {
             final File legacyBaseDir = mDeps.getLegacyStatsDir();
+            // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+            // failed and calling deleteAll.
             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)
-            };
+                buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
+                        false /* wipeOnError */)};
         } else {
             legacyRecorders = null;
         }
@@ -1117,9 +1126,7 @@
         } 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;
+        return overlayValue != null ? overlayValue : mDeps.isDebuggable();
     }
 
     /**
@@ -1145,10 +1152,12 @@
             if (error != null) {
                 Log.wtf(TAG, "Unexpected comparison result for recorder "
                         + legacyRecorder.getCookie() + ": " + error);
+                return false;
             }
         } catch (Throwable e) {
             Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder "
                     + legacyRecorder.getCookie(), e);
+            return false;
         }
         return true;
     }
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
new file mode 100644
index 0000000..b0d6763
--- /dev/null
+++ b/service/jarjar-excludes.txt
@@ -0,0 +1,9 @@
+# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
+com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
+
+# Do not jarjar com.android.server, as several unit tests fail because they lose
+# package-private visibility between jarjared and non-jarjared classes.
+# TODO: fix the tests and also jarjar com.android.server, or at least only exclude a package that
+# is specific to the module like com.android.server.connectivity
+com\.android\.server\..+
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index adc1925..4dc056d 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -56,7 +56,6 @@
 using bpf::BpfMap;
 using bpf::synchronizeKernelRCU;
 using netdutils::DumpWriter;
-using netdutils::getIfaceList;
 using netdutils::NetlinkListener;
 using netdutils::NetlinkListenerInterface;
 using netdutils::ScopedIndent;
@@ -111,14 +110,6 @@
     return matchType;
 }
 
-bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) {
-    // This implementation is the same logic as method ActivityManager#checkComponentPermission.
-    // It implies that the calling uid can never be the same as PER_USER_RANGE.
-    uint32_t appId = uid % PER_USER_RANGE;
-    return ((appId == AID_ROOT) || (appId == AID_SYSTEM) ||
-            mPrivilegedUser.find(appId) != mPrivilegedUser.end());
-}
-
 const std::string UidPermissionTypeToString(int permission) {
     if (permission == INetd::PERMISSION_NONE) {
         return "PERMISSION_NONE";
@@ -198,16 +189,6 @@
 Status TrafficController::start() {
     RETURN_IF_NOT_OK(initMaps());
 
-    // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is
-    // already running, so it will call addInterface() when any new interface appears.
-    // TODO: Clean-up addInterface() after interface monitoring is in
-    // NetworkStatsService.
-    std::map<std::string, uint32_t> ifacePairs;
-    ASSIGN_OR_RETURN(ifacePairs, getIfaceList());
-    for (const auto& ifacePair:ifacePairs) {
-        addInterface(ifacePair.first.c_str(), ifacePair.second);
-    }
-
     auto result = makeSkDestroyListener();
     if (!isOk(result)) {
         ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
@@ -245,22 +226,6 @@
     return netdutils::status::ok;
 }
 
-int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) {
-    IfaceValue iface;
-    if (ifaceIndex == 0) {
-        ALOGE("Unknown interface %s(%d)", name, ifaceIndex);
-        return -1;
-    }
-
-    strlcpy(iface.name, name, sizeof(IfaceValue));
-    Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY);
-    if (!isOk(res)) {
-        ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code()));
-        return -res.code();
-    }
-    return 0;
-}
-
 Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
                                               FirewallType type) {
     std::lock_guard guard(mMutex);
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index c921ff2..8512929 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -45,11 +45,6 @@
      */
     netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
 
-    /*
-     * Add the interface name and index pair into the eBPF map.
-     */
-    int addInterface(const char* name, uint32_t ifaceIndex);
-
     int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
 
     int removeUidOwnerRule(const uid_t uid);
@@ -187,8 +182,6 @@
     // need to call back to system server for permission check.
     std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
 
-    bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex);
-
     // For testing
     friend class TrafficControllerTest;
 };
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index cc07fd1..d0567ae 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -34,8 +34,6 @@
 
 import java.io.FileNotFoundException;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
         IBuildReceiver {
@@ -171,18 +169,19 @@
         }
     }
 
-    private static final Pattern UID_PATTERN =
-            Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE);
-
     protected int getUid(String packageName) throws DeviceNotAvailableException {
-        final String output = runCommand("dumpsys package " + packageName);
-        final Matcher matcher = UID_PATTERN.matcher(output);
-        while (matcher.find()) {
-            final String match = matcher.group(1);
-            return Integer.parseInt(match);
+        final int currentUser = getDevice().getCurrentUser();
+        final String uidLines = runCommand(
+                "cmd package list packages -U --user " + currentUser + " " + packageName);
+        for (String uidLine : uidLines.split("\n")) {
+            if (uidLine.startsWith("package:" + packageName + " uid:")) {
+                final String[] uidLineParts = uidLine.split(":");
+                // 3rd entry is package uid
+                return Integer.parseInt(uidLineParts[2].trim());
+            }
         }
-        throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n"
-                + output);
+        throw new IllegalStateException("Failed to find the test app on the device; pkg="
+                + packageName + ", u=" + currentUser);
     }
 
     protected String runCommand(String command) throws DeviceNotAvailableException {
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 97ecb9e..e52533b 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -31,8 +31,13 @@
   std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
   ASSERT_TRUE(elfFile.is_open());
 
-  EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
-  EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  if (android::modules::sdklevel::IsAtLeastT()) {
+    EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  } else {
+    EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  }
 }
 
 TEST(BpfTest, bpfStructSizeTestPreT) {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 0344604..1b77d5f 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -33,7 +33,6 @@
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
 import android.net.Uri
-import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
@@ -60,6 +59,8 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
 import org.junit.runner.RunWith
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
@@ -99,34 +100,42 @@
 
     private val server = TestHttpServer("localhost")
 
+    @get:Rule
+    val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
+
+    companion object {
+        @JvmStatic @BeforeClass
+        fun setUpClass() {
+            runAsShell(READ_DEVICE_CONFIG) {
+                // Verify that the test URLs are not normally set on the device, but do not fail if
+                // the test URLs are set to what this test uses (URLs on localhost), in case the
+                // test was interrupted manually and rerun.
+                assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+                assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+            }
+            NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
+        }
+
+        private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+            val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+            assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+                    "$urlKey must not be set in production scenarios (current value: $url)")
+        }
+    }
+
     @Before
     fun setUp() {
-        runAsShell(READ_DEVICE_CONFIG) {
-            // Verify that the test URLs are not normally set on the device, but do not fail if the
-            // test URLs are set to what this test uses (URLs on localhost), in case the test was
-            // interrupted manually and rerun.
-            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
-            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
-        }
-        clearValidationTestUrlsDeviceConfig()
         server.start()
     }
 
     @After
     fun tearDown() {
-        clearValidationTestUrlsDeviceConfig()
         if (pm.hasSystemFeature(FEATURE_WIFI)) {
-            reconnectWifi()
+            deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
         }
         server.stop()
     }
 
-    private fun assertEmptyOrLocalhostUrl(urlKey: String) {
-        val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
-        assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
-                "$urlKey must not be set in production scenarios (current value: $url)")
-    }
-
     @Test
     fun testCaptivePortalIsNotDefaultNetwork() {
         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
@@ -154,12 +163,13 @@
         server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
         val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
         server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
-        setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
-        setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+        setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
+        setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
         Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
         // URL expiration needs to be in the next 10 minutes
         assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
-        setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
+        setUrlExpirationDeviceConfig(deviceConfigRule,
+                System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
 
         // Wait for a captive portal to be detected on the network
         val wifiNetworkFuture = CompletableFuture<Network>()
@@ -215,4 +225,4 @@
         utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
         utils.ensureWifiConnected()
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 08cf0d7..da2e594 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -249,6 +249,10 @@
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
+    @Rule
+    public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
+            5 /* retryCountBeforeSIfConfigChanged */);
+
     private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
 
     public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
@@ -2765,9 +2769,8 @@
             // Accept partial connectivity network should result in a validated network
             expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
         } finally {
-            resetValidationConfig();
-            // Reconnect wifi to reset the wifi status
-            reconnectWifi();
+            mHttpServer.stop();
+            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
         }
     }
 
@@ -2792,11 +2795,13 @@
             // Reject partial connectivity network should cause the network being torn down
             assertEquals(network, cb.waitForLost());
         } finally {
-            resetValidationConfig();
+            mHttpServer.stop();
             // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
             // apply here. Thus, turn off wifi first and restart to restore.
-            runShellCommand("svc wifi disable");
-            mCtsNetUtils.ensureWifiConnected();
+            mTestValidationConfigRule.runAfterNextCleanup(() -> {
+                runShellCommand("svc wifi disable");
+                mCtsNetUtils.ensureWifiConnected();
+            });
         }
     }
 
@@ -2832,11 +2837,13 @@
             });
             waitForLost(wifiCb);
         } finally {
-            resetValidationConfig();
+            mHttpServer.stop();
             /// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
             // apply here. Thus, turn off wifi first and restart to restore.
-            runShellCommand("svc wifi disable");
-            mCtsNetUtils.ensureWifiConnected();
+            mTestValidationConfigRule.runAfterNextCleanup(() -> {
+                runShellCommand("svc wifi disable");
+                mCtsNetUtils.ensureWifiConnected();
+            });
         }
     }
 
@@ -2896,9 +2903,8 @@
             wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
         } finally {
             resetAvoidBadWifi(previousAvoidBadWifi);
-            resetValidationConfig();
-            // Reconnect wifi to reset the wifi status
-            reconnectWifi();
+            mHttpServer.stop();
+            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
         }
     }
 
@@ -2942,11 +2948,6 @@
         return future.get(timeout, TimeUnit.MILLISECONDS);
     }
 
-    private void resetValidationConfig() {
-        NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
-        mHttpServer.stop();
-    }
-
     private void prepareHttpServer() throws Exception {
         runAsShell(READ_DEVICE_CONFIG, () -> {
             // Verify that the test URLs are not normally set on the device, but do not fail if the
@@ -3019,9 +3020,11 @@
         mHttpServer.addResponse(new TestHttpServer.Request(
                 TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
                 httpStatusCode, null /* locationHeader */, "" /* content */);
-        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH));
-        NetworkValidationTestUtil.setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH));
-        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(
+        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(mTestValidationConfigRule,
+                makeUrl(TEST_HTTPS_URL_PATH));
+        NetworkValidationTestUtil.setHttpUrlDeviceConfig(mTestValidationConfigRule,
+                makeUrl(TEST_HTTP_URL_PATH));
+        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(mTestValidationConfigRule,
                 System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
     }
 
diff --git a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
new file mode 100644
index 0000000..d31a4e0
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ *        the configs that were set through this rule were changed, and retry the test
+ *        up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+    val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+    // Maps (namespace, key) -> value
+    private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+    private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+    /**
+     * Actions to be run after cleanup of the config, for the current test only.
+     */
+    private val currentTestCleanupActions = mutableListOf<Runnable>()
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return TestValidationUrlStatement(base, description)
+    }
+
+    private inner class TestValidationUrlStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+            while (retryCount > 0) {
+                retryCount--
+                tryTest {
+                    base.evaluate()
+                    // Can't use break/return out of a loop here because this is a tryTest lambda,
+                    // so set retryCount to exit instead
+                    retryCount = 0
+                }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+                    if (retryCount == 0) throw e
+                    usedConfig.forEach { (key, value) ->
+                        val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+                            DeviceConfig.getProperty(key.first, key.second)
+                        }
+                        if (currentValue != value) {
+                            Log.w(TAG, "Test failed with unexpected device config change, retrying")
+                            return@catch
+                        }
+                    }
+                    throw e
+                } cleanupStep {
+                    runAsShell(WRITE_DEVICE_CONFIG) {
+                        originalConfig.forEach { (key, value) ->
+                            DeviceConfig.setProperty(
+                                    key.first, key.second, value, false /* makeDefault */)
+                        }
+                    }
+                } cleanupStep {
+                    originalConfig.clear()
+                    usedConfig.clear()
+                } cleanup {
+                    currentTestCleanupActions.forEach { it.run() }
+                    currentTestCleanupActions.clear()
+                }
+            }
+        }
+    }
+
+    /**
+     * Set a configuration key/value. After the test case ends, it will be restored to the value it
+     * had when this method was first called.
+     */
+    fun setConfig(namespace: String, key: String, value: String?) {
+        runAsShell(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) {
+            val keyPair = Pair(namespace, key)
+            if (!originalConfig.containsKey(keyPair)) {
+                originalConfig[keyPair] = DeviceConfig.getProperty(namespace, key)
+            }
+            usedConfig[keyPair] = value
+            DeviceConfig.setProperty(namespace, key, value, false /* makeDefault */)
+        }
+    }
+
+    /**
+     * Add an action to be run after config cleanup when the current test case ends.
+     */
+    fun runAfterNextCleanup(action: Runnable) {
+        currentTestCleanupActions.add(action)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index db24b44..54d6818 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -30,11 +30,14 @@
 import android.net.EthernetManager.STATE_LINK_UP
 import android.net.EthernetManager.TetheredInterfaceCallback
 import android.net.EthernetManager.TetheredInterfaceRequest
+import android.net.EthernetNetworkManagementException
 import android.net.EthernetNetworkSpecifier
+import android.net.EthernetNetworkUpdateRequest
 import android.net.InetAddresses
 import android.net.IpConfiguration
 import android.net.MacAddress
 import android.net.Network
+import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_TEST
@@ -44,8 +47,8 @@
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
 import android.os.Build
 import android.os.Handler
-import android.os.HandlerExecutor
 import android.os.Looper
+import android.os.OutcomeReceiver
 import android.os.SystemProperties
 import android.platform.test.annotations.AppModeFull
 import android.util.ArraySet
@@ -53,6 +56,7 @@
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
 import com.android.testutils.anyNetwork
+import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
@@ -94,35 +98,42 @@
 @AppModeFull(reason = "Instant apps can't access EthernetManager")
 // EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
 @RunWith(DevSdkIgnoreRunner::class)
+// This test depends on behavior introduced post-T as part of connectivity module updates
+@ConnectivityModuleTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class EthernetManagerTest {
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val em by lazy { context.getSystemService(EthernetManager::class.java) }
     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val handler by lazy { Handler(Looper.getMainLooper()) }
 
     private val ifaceListener = EthernetStateListener()
     private val createdIfaces = ArrayList<EthernetTestInterface>()
     private val addedListeners = ArrayList<EthernetStateListener>()
-    private val networkRequests = ArrayList<TestableNetworkCallback>()
+    private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
 
     private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
 
     private class EthernetTestInterface(
         context: Context,
-        private val handler: Handler
+        private val handler: Handler,
+        hasCarrier: Boolean
     ) {
         private val tapInterface: TestNetworkInterface
         private val packetReader: TapPacketReader
         private val raResponder: RouterAdvertisementResponder
-        val interfaceName get() = tapInterface.interfaceName
+        private val tnm: TestNetworkManager
+        val name get() = tapInterface.interfaceName
 
         init {
-            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
-                val tnm = context.getSystemService(TestNetworkManager::class.java)
-                tnm.createTapInterface(false /* bringUp */)
+            tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+                context.getSystemService(TestNetworkManager::class.java)
             }
-            val mtu = 1500
+            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
+                tnm.createTapInterface(hasCarrier, false /* bringUp */)
+            }
+            val mtu = tapInterface.mtu
             packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
             raResponder = RouterAdvertisementResponder(packetReader)
             raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
@@ -132,6 +143,12 @@
             raResponder.start()
         }
 
+        fun setCarrierEnabled(enabled: Boolean) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                tnm.setCarrierEnabled(tapInterface, enabled)
+            }
+        }
+
         fun destroy() {
             raResponder.stop()
             handler.post({ packetReader.stop() })
@@ -172,7 +189,7 @@
         }
 
         fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
-            expectCallback(createChangeEvent(iface.interfaceName, state, role))
+            expectCallback(createChangeEvent(iface.name, state, role))
         }
 
         fun createChangeEvent(iface: String, state: Int, role: Int) =
@@ -190,7 +207,7 @@
         }
 
         fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
-            eventuallyExpect(iface.interfaceName, state, role)
+            eventuallyExpect(iface.name, state, role)
         }
 
         fun assertNoCallback() {
@@ -227,6 +244,35 @@
         }
     }
 
+    private class EthernetOutcomeReceiver :
+        OutcomeReceiver<String, EthernetNetworkManagementException> {
+        private val result = CompletableFuture<String>()
+
+        override fun onResult(iface: String) {
+            result.complete(iface)
+        }
+
+        override fun onError(e: EthernetNetworkManagementException) {
+            result.completeExceptionally(e)
+        }
+
+        fun expectResult(expected: String) {
+            assertEquals(expected, result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS))
+        }
+
+        fun expectError() {
+            // Assert that the future fails with EthernetNetworkManagementException from the
+            // completeExceptionally() call inside onUnavailable.
+            assertFailsWith(EthernetNetworkManagementException::class) {
+                try {
+                    result.get()
+                } catch (e: ExecutionException) {
+                    throw e.cause!!
+                }
+            }
+        }
+    }
+
     @Before
     fun setUp() {
         setIncludeTestInterfaces(true)
@@ -243,26 +289,28 @@
         for (listener in addedListeners) {
             em.removeInterfaceStateListener(listener)
         }
-        networkRequests.forEach { cm.unregisterNetworkCallback(it) }
+        registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
         releaseTetheredInterface()
     }
 
     private fun addInterfaceStateListener(listener: EthernetStateListener) {
         runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
-            em.addInterfaceStateListener(HandlerExecutor(Handler(Looper.getMainLooper())), listener)
+            em.addInterfaceStateListener(handler::post, listener)
         }
         addedListeners.add(listener)
     }
 
-    private fun createInterface(): EthernetTestInterface {
+    private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
         val iface = EthernetTestInterface(
             context,
-            Handler(Looper.getMainLooper())
+            handler,
+            hasCarrier
         ).also { createdIfaces.add(it) }
-        with(ifaceListener) {
-            // when an interface comes up, we should always see a down cb before an up cb.
-            eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
-            expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        // when an interface comes up, we should always see a down cb before an up cb.
+        ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+        if (hasCarrier) {
+            ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
         }
         return iface
     }
@@ -282,18 +330,20 @@
     private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback {
         return TestableNetworkCallback().also {
             cm.requestNetwork(request, it)
-            networkRequests.add(it)
+            registeredCallbacks.add(it)
         }
     }
 
-    private fun releaseNetwork(cb: TestableNetworkCallback) {
-        cm.unregisterNetworkCallback(cb)
-        networkRequests.remove(cb)
+    private fun registerNetworkListener(request: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.registerNetworkCallback(request, it)
+            registeredCallbacks.add(it)
+        }
     }
 
     private fun requestTetheredInterface() = TetheredInterfaceListener().also {
         tetheredInterfaceRequest = runAsShell(NETWORK_SETTINGS) {
-            em.requestTetheredInterface(HandlerExecutor(Handler(Looper.getMainLooper())), it)
+            em.requestTetheredInterface(handler::post, it)
         }
     }
 
@@ -304,22 +354,65 @@
         }
     }
 
+    private fun releaseRequest(cb: TestableNetworkCallback) {
+        cm.unregisterNetworkCallback(cb)
+        registeredCallbacks.remove(cb)
+    }
+
+    private fun disableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.disableInterface(iface.name, handler::post, it)
+        }
+    }
+
+    private fun enableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.enableInterface(iface.name, handler::post, it)
+        }
+    }
+
+    private fun updateConfiguration(
+        iface: EthernetTestInterface,
+        ipConfig: IpConfiguration? = null,
+        capabilities: NetworkCapabilities? = null
+    ) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.updateConfiguration(
+                iface.name,
+                EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(ipConfig)
+                    .setNetworkCapabilities(capabilities).build(),
+                handler::post,
+                it)
+        }
+    }
+
+    // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to
+    // keep ETH_REQUEST as it is, a defensive copy is created here.
     private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
         NetworkRequest.Builder(NetworkRequest(ETH_REQUEST))
             .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build()
 
     // It can take multiple seconds for the network to become available.
     private fun TestableNetworkCallback.expectAvailable() =
-        expectCallback<Available>(anyNetwork(), 5000/*ms timeout*/).network
+        expectCallback<Available>(anyNetwork(), 5000 /* ms timeout */).network
 
     // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
     // eventuallyExpect(Lost::class) instead.
     private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) =
         eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
 
-    private fun TestableNetworkCallback.assertNotLost(n: Network? = null) =
+    private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) =
         assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
 
+    private fun TestableNetworkCallback.assertNeverAvailable(n: Network? = null) =
+        assertNoCallbackThat() { it is Available && (n?.equals(it.network) ?: true) }
+
+    private fun TestableNetworkCallback.expectCapabilitiesWithInterfaceName(name: String) =
+        expectCapabilitiesThat(anyNetwork()) {
+            it.networkSpecifier == EthernetNetworkSpecifier(name)
+        }
+
     @Test
     fun testCallbacks() {
         // If an interface exists when the callback is registered, it is reported on registration.
@@ -398,11 +491,8 @@
             assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned")
             assertTrue(ifaces.contains(iface), "Untracked interface $iface returned")
             // If the event's iface was created in the test, additional criteria can be validated.
-            createdIfaces.find { it.interfaceName.equals(iface) }?.let {
-                assertEquals(event,
-                    listener.createChangeEvent(it.interfaceName,
-                                                        STATE_LINK_UP,
-                                                        ROLE_CLIENT))
+            createdIfaces.find { it.name.equals(iface) }?.let {
+                assertEquals(event, listener.createChangeEvent(it.name, STATE_LINK_UP, ROLE_CLIENT))
             }
         }
         // Assert all callbacks are accounted for.
@@ -411,87 +501,75 @@
 
     @Test
     fun testGetInterfaceList() {
-        setIncludeTestInterfaces(true)
-
         // Create two test interfaces and check the return list contains the interface names.
         val iface1 = createInterface()
         val iface2 = createInterface()
         var ifaces = em.getInterfaceList()
         assertTrue(ifaces.size > 0)
-        assertTrue(ifaces.contains(iface1.interfaceName))
-        assertTrue(ifaces.contains(iface2.interfaceName))
+        assertTrue(ifaces.contains(iface1.name))
+        assertTrue(ifaces.contains(iface2.name))
 
         // Remove one existing test interface and check the return list doesn't contain the
         // removed interface name.
         removeInterface(iface1)
         ifaces = em.getInterfaceList()
-        assertFalse(ifaces.contains(iface1.interfaceName))
-        assertTrue(ifaces.contains(iface2.interfaceName))
+        assertFalse(ifaces.contains(iface1.name))
+        assertTrue(ifaces.contains(iface2.name))
 
         removeInterface(iface2)
     }
 
     @Test
     fun testNetworkRequest_withSingleExistingInterface() {
-        setIncludeTestInterfaces(true)
         createInterface()
 
         // install a listener which will later be used to verify the Lost callback
-        val listenerCb = TestableNetworkCallback()
-        cm.registerNetworkCallback(ETH_REQUEST, listenerCb)
-        networkRequests.add(listenerCb)
+        val listenerCb = registerNetworkListener(ETH_REQUEST)
 
         val cb = requestNetwork(ETH_REQUEST)
         val network = cb.expectAvailable()
 
-        cb.assertNotLost()
-        releaseNetwork(cb)
+        cb.assertNeverLost()
+        releaseRequest(cb)
         listenerCb.eventuallyExpectLost(network)
     }
 
     @Test
     fun testNetworkRequest_beforeSingleInterfaceIsUp() {
-        setIncludeTestInterfaces(true)
-
         val cb = requestNetwork(ETH_REQUEST)
 
-        // bring up interface after network has been requested
+        // bring up interface after network has been requested.
+        // Note: there is no guarantee that the NetworkRequest has been processed before the
+        // interface is actually created. That being said, it takes a few seconds between calling
+        // createInterface and the interface actually being properly registered with the ethernet
+        // module, so it is extremely unlikely that the CS handler thread has not run until then.
         val iface = createInterface()
         val network = cb.expectAvailable()
 
         // remove interface before network request has been removed
-        cb.assertNotLost()
+        cb.assertNeverLost()
         removeInterface(iface)
         cb.eventuallyExpectLost()
-
-        releaseNetwork(cb)
     }
 
     @Test
     fun testNetworkRequest_withMultipleInterfaces() {
-        setIncludeTestInterfaces(true)
-
         val iface1 = createInterface()
         val iface2 = createInterface()
 
-        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
 
         val network = cb.expectAvailable()
-        cb.expectCapabilitiesThat(network) {
-            it.networkSpecifier == EthernetNetworkSpecifier(iface2.interfaceName)
-        }
+        cb.expectCapabilitiesWithInterfaceName(iface2.name)
 
         removeInterface(iface1)
-        cb.assertNotLost()
+        cb.assertNeverLost()
         removeInterface(iface2)
         cb.eventuallyExpectLost()
-
-        releaseNetwork(cb)
     }
 
     @Test
     fun testNetworkRequest_withInterfaceBeingReplaced() {
-        setIncludeTestInterfaces(true)
         val iface1 = createInterface()
 
         val cb = requestNetwork(ETH_REQUEST)
@@ -499,36 +577,67 @@
 
         // create another network and verify the request sticks to the current network
         val iface2 = createInterface()
-        cb.assertNotLost()
+        cb.assertNeverLost()
 
         // remove iface1 and verify the request brings up iface2
         removeInterface(iface1)
         cb.eventuallyExpectLost(network)
         val network2 = cb.expectAvailable()
-
-        releaseNetwork(cb)
     }
 
     @Test
     fun testNetworkRequest_withMultipleInterfacesAndRequests() {
-        setIncludeTestInterfaces(true)
         val iface1 = createInterface()
         val iface2 = createInterface()
 
-        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.interfaceName))
-        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.name))
+        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
         val cb3 = requestNetwork(ETH_REQUEST)
 
         cb1.expectAvailable()
+        cb1.expectCapabilitiesWithInterfaceName(iface1.name)
         cb2.expectAvailable()
+        cb2.expectCapabilitiesWithInterfaceName(iface2.name)
+        // this request can be matched by either network.
         cb3.expectAvailable()
 
-        cb1.assertNotLost()
-        cb2.assertNotLost()
-        cb3.assertNotLost()
+        cb1.assertNeverLost()
+        cb2.assertNeverLost()
+        cb3.assertNeverLost()
+    }
 
-        releaseNetwork(cb1)
-        releaseNetwork(cb2)
-        releaseNetwork(cb3)
+    @Test
+    fun testNetworkRequest_ensureProperRefcounting() {
+        // create first request before interface is up / exists; create another request after it has
+        // been created; release one of them and check that the network stays up.
+        val listener = registerNetworkListener(ETH_REQUEST)
+        val cb1 = requestNetwork(ETH_REQUEST)
+
+        val iface = createInterface()
+        val network = cb1.expectAvailable()
+
+        val cb2 = requestNetwork(ETH_REQUEST)
+        cb2.expectAvailable()
+
+        // release the first request; this used to trigger b/197548738
+        releaseRequest(cb1)
+
+        cb2.assertNeverLost()
+        releaseRequest(cb2)
+        listener.eventuallyExpectLost(network)
+    }
+
+    @Test
+    fun testNetworkRequest_forInterfaceWhileTogglingCarrier() {
+        val iface = createInterface(false /* hasCarrier */)
+
+        val cb = requestNetwork(ETH_REQUEST)
+        cb.assertNeverAvailable()
+
+        iface.setCarrierEnabled(true)
+        cb.expectAvailable()
+
+        iface.setCarrierEnabled(false)
+        cb.eventuallyExpectLost()
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index 391d03a..462c8a3 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -16,16 +16,11 @@
 
 package android.net.cts
 
-import android.Manifest
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
 import android.net.util.NetworkStackUtils
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
-import android.util.Log
 import com.android.testutils.runAsShell
-import com.android.testutils.tryTest
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
 
 /**
  * Collection of utility methods for configuring network validation.
@@ -38,9 +33,14 @@
      * Clear the test network validation URLs.
      */
     @JvmStatic fun clearValidationTestUrlsDeviceConfig() {
-        setHttpsUrlDeviceConfig(null)
-        setHttpUrlDeviceConfig(null)
-        setUrlExpirationDeviceConfig(null)
+        runAsShell(WRITE_DEVICE_CONFIG) {
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_URL_EXPIRATION_TIME, null, false)
+        }
     }
 
     /**
@@ -48,71 +48,28 @@
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
      */
-    @JvmStatic fun setHttpsUrlDeviceConfig(url: String?) =
-            setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+    @JvmStatic
+    fun setHttpsUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
 
     /**
      * Set the test validation HTTP URL.
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
      */
-    @JvmStatic fun setHttpUrlDeviceConfig(url: String?) =
-            setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+    @JvmStatic
+    fun setHttpUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
 
     /**
      * Set the test validation URL expiration.
      *
      * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
      */
-    @JvmStatic fun setUrlExpirationDeviceConfig(timestamp: Long?) =
-            setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
-
-    private fun setConfig(configKey: String, value: String?): String? {
-        Log.i(TAG, "Setting config \"$configKey\" to \"$value\"")
-        val readWritePermissions = arrayOf(
-                Manifest.permission.READ_DEVICE_CONFIG,
-                Manifest.permission.WRITE_DEVICE_CONFIG)
-
-        val existingValue = runAsShell(*readWritePermissions) {
-            DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, configKey)
-        }
-        if (existingValue == value) {
-            // Already the correct value. There may be a race if a change is already in flight,
-            // but if multiple threads update the config there is no way to fix that anyway.
-            Log.i(TAG, "\$configKey\" already had value \"$value\"")
-            return value
-        }
-
-        val future = CompletableFuture<String>()
-        val listener = DeviceConfig.OnPropertiesChangedListener {
-            // The listener receives updates for any change to any key, so don't react to
-            // changes that do not affect the relevant key
-            if (!it.keyset.contains(configKey)) return@OnPropertiesChangedListener
-            if (it.getString(configKey, null) == value) {
-                future.complete(value)
-            }
-        }
-
-        return tryTest {
-            runAsShell(*readWritePermissions) {
-                DeviceConfig.addOnPropertiesChangedListener(
-                        NAMESPACE_CONNECTIVITY,
-                        inlineExecutor,
-                        listener)
-                DeviceConfig.setProperty(
-                        NAMESPACE_CONNECTIVITY,
-                        configKey,
-                        value,
-                        false /* makeDefault */)
-                // Don't drop the permission until the config is applied, just in case
-                future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-            }.also {
-                Log.i(TAG, "Config \"$configKey\" successfully set to \"$value\"")
-            }
-        } cleanup {
-            DeviceConfig.removeOnPropertiesChangedListener(listener)
-        }
-    }
-
-    private val inlineExecutor get() = Executor { r -> r.run() }
+    @JvmStatic
+    fun setUrlExpirationDeviceConfig(rule: DeviceConfigRule, timestamp: Long?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
 }
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
new file mode 100644
index 0000000..fad11a3
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *i
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkStats;
+import android.os.DropBoxManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.FileRotator;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public final class NetworkStatsRecorderTest {
+    private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
+
+    private static final String TEST_PREFIX = "test";
+
+    @Mock private DropBoxManager mDropBox;
+    @Mock private NetworkStats.NonMonotonicObserver mObserver;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
+        return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
+                    HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+    }
+
+    @Test
+    public void testWipeOnError() throws Exception {
+        final FileRotator rotator = mock(FileRotator.class);
+        final NetworkStatsRecorder wipeOnErrorRecorder = buildRecorder(rotator, true);
+
+        // Assuming that the rotator gets an exception happened when read data.
+        doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+        wipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+        // Verify that the files will be deleted.
+        verify(rotator, times(1)).deleteAll();
+        reset(rotator);
+
+        final NetworkStatsRecorder noWipeOnErrorRecorder = buildRecorder(rotator, false);
+        doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+        noWipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+        // Verify that the rotator won't delete files.
+        verify(rotator, never()).deleteAll();
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index cdfa190..5899fd0 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -27,7 +27,9 @@
 import static android.net.ConnectivityManager.NetworkCallback;
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.VpnManager.TYPE_VPN_PLATFORM;
+import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
 import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.UserHandle.PER_USER_RANGE;
 
@@ -59,6 +61,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -83,7 +86,9 @@
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpPrefix;
+import android.net.IpSecConfig;
 import android.net.IpSecManager;
+import android.net.IpSecTransform;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -100,7 +105,12 @@
 import android.net.VpnProfileState;
 import android.net.VpnService;
 import android.net.VpnTransportInfo;
+import android.net.ipsec.ike.ChildSessionCallback;
+import android.net.ipsec.ike.ChildSessionConfiguration;
 import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.IkeTrafficSelector;
 import android.net.ipsec.ike.exceptions.IkeException;
 import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
 import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
@@ -121,6 +131,7 @@
 import android.security.Credentials;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Range;
 
 import androidx.test.filters.SmallTest;
@@ -155,6 +166,7 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
@@ -165,6 +177,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 
@@ -198,11 +211,37 @@
     static final Network EGRESS_NETWORK = new Network(101);
     static final String EGRESS_IFACE = "wlan0";
     static final String TEST_VPN_PKG = "com.testvpn.vpn";
+    private static final String TEST_VPN_CLIENT = "2.4.6.8";
     private static final String TEST_VPN_SERVER = "1.2.3.4";
     private static final String TEST_VPN_IDENTITY = "identity";
     private static final byte[] TEST_VPN_PSK = "psk".getBytes();
 
+    private static final int IP4_PREFIX_LEN = 32;
+    private static final int MIN_PORT = 0;
+    private static final int MAX_PORT = 65535;
+
+    private static final InetAddress TEST_VPN_CLIENT_IP =
+            InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
+    private static final InetAddress TEST_VPN_SERVER_IP =
+            InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
+    private static final InetAddress TEST_VPN_CLIENT_IP_2 =
+            InetAddresses.parseNumericAddress("192.0.2.200");
+    private static final InetAddress TEST_VPN_SERVER_IP_2 =
+            InetAddresses.parseNumericAddress("192.0.2.201");
+    private static final InetAddress TEST_VPN_INTERNAL_IP =
+            InetAddresses.parseNumericAddress("198.51.100.10");
+    private static final InetAddress TEST_VPN_INTERNAL_DNS =
+            InetAddresses.parseNumericAddress("8.8.8.8");
+
+    private static final IkeTrafficSelector IN_TS =
+            new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
+    private static final IkeTrafficSelector OUT_TS =
+            new IkeTrafficSelector(MIN_PORT, MAX_PORT,
+                    InetAddresses.parseNumericAddress("0.0.0.0"),
+                    InetAddresses.parseNumericAddress("255.255.255.255"));
+
     private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
+    private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
     private static final String TEST_IFACE_NAME = "TEST_IFACE";
     private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
     private static final long TEST_TIMEOUT_MS = 500L;
@@ -234,7 +273,9 @@
     @Mock private AppOpsManager mAppOps;
     @Mock private NotificationManager mNotificationManager;
     @Mock private Vpn.SystemServices mSystemServices;
+    @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
     @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
+    @Mock private NetworkAgent mMockNetworkAgent;
     @Mock private ConnectivityManager mConnectivityManager;
     @Mock private IpSecService mIpSecService;
     @Mock private VpnProfileStore mVpnProfileStore;
@@ -243,6 +284,8 @@
 
     private IpSecManager mIpSecManager;
 
+    private TestDeps mTestDeps;
+
     public VpnTest() throws Exception {
         // Build an actual VPN profile that is capable of being converted to and from an
         // Ikev2VpnProfile
@@ -257,6 +300,7 @@
         MockitoAnnotations.initMocks(this);
 
         mIpSecManager = new IpSecManager(mContext, mIpSecService);
+        mTestDeps = spy(new TestDeps());
 
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         setMockedPackages(mPackages);
@@ -297,6 +341,14 @@
         // itself, so set the default value of Context#checkCallingOrSelfPermission to
         // PERMISSION_DENIED.
         doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
+
+        resetIkev2SessionCreator(mIkeSessionWrapper);
+    }
+
+    private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
+        reset(mIkev2SessionCreator);
+        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
+                .thenReturn(ikeSession);
     }
 
     @After
@@ -1320,6 +1372,10 @@
         final ArgumentCaptor<IkeSessionCallback> captor =
                 ArgumentCaptor.forClass(IkeSessionCallback.class);
 
+        // This test depends on a real ScheduledThreadPoolExecutor
+        doReturn(new ScheduledThreadPoolExecutor(1)).when(mTestDeps)
+                .newScheduledThreadPoolExecutor();
+
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
         when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
                 .thenReturn(mVpnProfile.encode());
@@ -1491,14 +1547,217 @@
         return vpn;
     }
 
+    private IkeSessionConnectionInfo createIkeConnectInfo() {
+        return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
+    }
+
+    private IkeSessionConnectionInfo createIkeConnectInfo_2() {
+        return new IkeSessionConnectionInfo(
+                TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
+    }
+
+    private IkeSessionConfiguration createIkeConfig(
+            IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
+        final IkeSessionConfiguration.Builder builder =
+                new IkeSessionConfiguration.Builder(ikeConnectInfo);
+
+        if (isMobikeEnabled) {
+            builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
+        }
+
+        return builder.build();
+    }
+
+    private ChildSessionConfiguration createChildConfig() {
+        return new ChildSessionConfiguration.Builder(Arrays.asList(IN_TS), Arrays.asList(OUT_TS))
+                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
+                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
+                .build();
+    }
+
+    private IpSecTransform createIpSecTransform() {
+        return new IpSecTransform(mContext, new IpSecConfig());
+    }
+
+    private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
+        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
+                anyInt(), anyString());
+        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
+                anyInt(), anyString());
+    }
+
+    private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
+            throws Exception {
+        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+        final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
+                ArgumentCaptor.forClass(ChildSessionCallback.class);
+
+        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
+                any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
+
+        return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
+    }
+
+    private static class PlatformVpnSnapshot {
+        public final Vpn vpn;
+        public final NetworkCallback nwCb;
+        public final IkeSessionCallback ikeCb;
+        public final ChildSessionCallback childCb;
+
+        PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
+                IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
+            this.vpn = vpn;
+            this.nwCb = nwCb;
+            this.ikeCb = ikeCb;
+            this.childCb = childCb;
+        }
+    }
+
+    private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
+            throws Exception {
+        doReturn(mMockNetworkAgent).when(mTestDeps)
+                .newNetworkAgent(
+                        any(), any(), anyString(), any(), any(), any(), any(), any());
+
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        final NetworkCallback nwCb = triggerOnAvailableAndGetCallback();
+
+        // Mock the setup procedure by firing callbacks
+        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+                verifyCreateIkeAndCaptureCbs();
+        final IkeSessionCallback ikeCb = cbPair.first;
+        final ChildSessionCallback childCb = cbPair.second;
+
+        ikeCb.onOpened(ikeConfig);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+        childCb.onOpened(createChildConfig());
+
+        // Verification VPN setup
+        verifyApplyTunnelModeTransforms(1);
+
+        ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
+        ArgumentCaptor<NetworkCapabilities> ncCaptor =
+                ArgumentCaptor.forClass(NetworkCapabilities.class);
+        verify(mTestDeps).newNetworkAgent(
+                any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
+                any(), any(), any());
+
+        // Check LinkProperties
+        final LinkProperties lp = lpCaptor.getValue();
+        final List<RouteInfo> expectedRoutes = Arrays.asList(
+                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+                        TEST_IFACE_NAME, RouteInfo.RTN_UNICAST),
+                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+                        TEST_IFACE_NAME, RTN_UNREACHABLE));
+        assertEquals(expectedRoutes, lp.getRoutes());
+
+        // Check internal addresses
+        final List<LinkAddress> expectedAddresses =
+                Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN));
+        assertEquals(expectedAddresses, lp.getLinkAddresses());
+
+        // Check internal DNS
+        assertEquals(Arrays.asList(TEST_VPN_INTERNAL_DNS), lp.getDnsServers());
+
+        // Check NetworkCapabilities
+        assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
+
+        return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
+    }
+
     @Test
     public void testStartPlatformVpn() throws Exception {
-        startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
-        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
-        // a subsequent patch.
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
     }
 
     @Test
+    public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+
+        // Mock network switch
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+
+        // Verify MOBIKE is triggered
+        verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2);
+
+        // Mock the MOBIKE procedure
+        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
+        vpnSnapShot.childCb.onIpSecTransformsMigrated(
+                createIpSecTransform(), createIpSecTransform());
+
+        verify(mIpSecService).setNetworkForTunnelInterface(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
+
+        // Expect 2 times: one for initial setup and one for MOBIKE
+        verifyApplyTunnelModeTransforms(2);
+
+        // Verify mNetworkCapabilities and mNetworkAgent are updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    @Test
+    public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the first IKE creation to be prepared to capture callbacks of the second
+        // IKE session
+        resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
+
+        // Mock network switch
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+
+        // Verify the old IKE Session is killed
+        verify(mIkeSessionWrapper).kill();
+
+        // Capture callbacks of the new IKE Session
+        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+                verifyCreateIkeAndCaptureCbs();
+        final IkeSessionCallback ikeCb = cbPair.first;
+        final ChildSessionCallback childCb = cbPair.second;
+
+        // Mock the IKE Session setup
+        ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
+
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+        childCb.onOpened(createChildConfig());
+
+        // Expect 2 times since there have been two Session setups
+        verifyApplyTunnelModeTransforms(2);
+
+        // Verify mNetworkCapabilities and mNetworkAgent are updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    // TODO: Add a test for network loss without mobility
+
+    @Test
     public void testStartRacoonNumericAddress() throws Exception {
         startRacoon("1.2.3.4", "1.2.3.4");
     }
@@ -1578,7 +1837,8 @@
         }
     }
 
-    private final class TestDeps extends Vpn.Dependencies {
+    // Make it public and un-final so as to spy it
+    public class TestDeps extends Vpn.Dependencies {
         public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
         public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
         public final File mStateFile;
@@ -1713,10 +1973,25 @@
             return mDeviceIdleInternal;
         }
 
+        @Override
         public long getNextRetryDelaySeconds(int retryCount) {
             // Simply return retryCount as the delay seconds for retrying.
             return retryCount;
         }
+
+        @Override
+        public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
+            final ScheduledThreadPoolExecutor mockExecutor =
+                    mock(ScheduledThreadPoolExecutor.class);
+            doAnswer(
+                    (invocation) -> {
+                        ((Runnable) invocation.getArgument(0)).run();
+                        return null;
+                    })
+                .when(mockExecutor)
+                .execute(any());
+            return mockExecutor;
+        }
     }
 
     /**
@@ -1728,7 +2003,7 @@
         when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
                 .thenReturn(asUserContext);
         final TestLooper testLooper = new TestLooper();
-        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService,
+        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
                 mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
         verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
                 provider -> provider.getName().contains("VpnNetworkProvider")
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 4f849d2..2178b33 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -92,6 +92,8 @@
     private Handler mHandler;
     private EthernetNetworkFactory mNetFactory = null;
     private IpClientCallbacks mIpClientCallbacks;
+    private NetworkOfferCallback mNetworkOfferCallback;
+    private NetworkRequest mRequestToKeepNetworkUp;
     @Mock private Context mContext;
     @Mock private Resources mResources;
     @Mock private EthernetNetworkFactory.Dependencies mDeps;
@@ -244,7 +246,9 @@
         ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
                 NetworkOfferCallback.class);
         verify(mNetworkProvider).registerNetworkOffer(any(), any(), any(), captor.capture());
-        captor.getValue().onNetworkNeeded(createDefaultRequest());
+        mRequestToKeepNetworkUp = createDefaultRequest();
+        mNetworkOfferCallback = captor.getValue();
+        mNetworkOfferCallback.onNetworkNeeded(mRequestToKeepNetworkUp);
 
         verifyStart(ipConfig);
         clearInvocations(mDeps);
@@ -625,6 +629,14 @@
     }
 
     @Test
+    public void testUpdateInterfaceAbortsOnNetworkUneededRemovesAllRequests() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetworkOfferCallback.onNetworkUnneeded(mRequestToKeepNetworkUp));
+    }
+
+    @Test
     public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
         initEthernetNetworkFactory();
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index acdc48a..f9cbb10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -2000,28 +2000,43 @@
 
     @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)
+        for (Boolean isDebuggable : Set.of(Boolean.TRUE, Boolean.FALSE)) {
+            mIsDebuggable = isDebuggable;
+            // Verify return false regardless of the device is debuggable.
+            doReturn(0).when(mResources)
                     .getInteger(R.integer.config_netstats_validate_import);
-            assertEquals(true, mService.shouldRunComparison());
+            assertShouldRunComparison(false, isDebuggable);
+            // Verify return true regardless of the device is debuggable.
+            doReturn(1).when(mResources)
+                    .getInteger(R.integer.config_netstats_validate_import);
+            assertShouldRunComparison(true, isDebuggable);
+            // Verify return true iff the device is debuggable.
+            for (int testValue : Set.of(-1, 2)) {
+                doReturn(testValue).when(mResources)
+                        .getInteger(R.integer.config_netstats_validate_import);
+                assertShouldRunComparison(isDebuggable, isDebuggable);
+            }
         }
     }
 
+    private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
+        assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
+                expected, mService.shouldRunComparison());
+    }
+
     private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
-            boolean includeTags) {
+            boolean includeTags, boolean wipeOnError) {
         final NetworkStats.NonMonotonicObserver observer =
                 mock(NetworkStats.NonMonotonicObserver.class);
         final DropBoxManager dropBox = mock(DropBoxManager.class);
         return new NetworkStatsRecorder(new FileRotator(
                 directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
-                observer, dropBox, prefix, config.bucketDuration, includeTags);
+                observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
     }
 
     private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
         final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, prefix,
-                mSettings.getDevConfig(), includeTags);
+                mSettings.getDevConfig(), includeTags, false);
         return recorder.getOrLoadCompleteLocked();
     }
 
diff --git a/tools/Android.bp b/tools/Android.bp
new file mode 100644
index 0000000..1fa93bb
--- /dev/null
+++ b/tools/Android.bp
@@ -0,0 +1,91 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Build tool used to generate jarjar rules for all classes in a jar, except those that are
+// API, UnsupportedAppUsage or otherwise excluded.
+python_binary_host {
+    name: "jarjar-rules-generator",
+    srcs: [
+        "gen_jarjar.py",
+    ],
+    main: "gen_jarjar.py",
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+genrule_defaults {
+    name: "jarjar-rules-combine-defaults",
+    // Concat files with a line break in the middle
+    cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
+    defaults_visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_library {
+    name: "jarjar-rules-generator-testjavalib",
+    srcs: ["testdata/java/**/*.java"],
+    visibility: ["//visibility:private"],
+}
+
+// TODO(b/233723405) - Remove this workaround.
+// Temporary work around of b/233723405. Using the module_lib stub directly
+// in the test causes it to sometimes get the dex jar and sometimes get the
+// classes jar due to b/233111644. Statically including it here instead
+// ensures that it will always get the classes jar.
+java_library {
+    name: "framework-connectivity.stubs.module_lib-for-test",
+    visibility: ["//visibility:private"],
+    static_libs: [
+        "framework-connectivity.stubs.module_lib",
+    ],
+    // Not strictly necessary but specified as this MUST not have generate
+    // a dex jar as that will break the tests.
+    compile_dex: false,
+}
+
+python_test_host {
+    name: "jarjar-rules-generator-test",
+    srcs: [
+        "gen_jarjar.py",
+        "gen_jarjar_test.py",
+    ],
+    data: [
+        "testdata/test-jarjar-excludes.txt",
+        "testdata/test-unsupportedappusage.txt",
+        ":framework-connectivity.stubs.module_lib-for-test",
+        ":jarjar-rules-generator-testjavalib",
+    ],
+    main: "gen_jarjar_test.py",
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+}
diff --git a/tools/gen_jarjar.py b/tools/gen_jarjar.py
new file mode 100755
index 0000000..4c2cf54
--- /dev/null
+++ b/tools/gen_jarjar.py
@@ -0,0 +1,133 @@
+#
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" This script generates jarjar rule files to add a jarjar prefix to all classes, except those
+that are API, unsupported API or otherwise excluded."""
+
+import argparse
+import io
+import re
+import subprocess
+from xml import sax
+from xml.sax.handler import ContentHandler
+from zipfile import ZipFile
+
+
+def parse_arguments(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        '--jars', nargs='+',
+        help='Path to pre-jarjar JAR. Can be followed by multiple space-separated paths.')
+    parser.add_argument(
+        '--prefix', required=True,
+        help='Package prefix to use for jarjared classes, '
+             'for example "com.android.connectivity" (does not end with a dot).')
+    parser.add_argument(
+        '--output', required=True, help='Path to output jarjar rules file.')
+    parser.add_argument(
+        '--apistubs', nargs='*', default=[],
+        help='Path to API stubs jar. Classes that are API will not be jarjared. Can be followed by '
+             'multiple space-separated paths.')
+    parser.add_argument(
+        '--unsupportedapi', nargs='*', default=[],
+        help='Path to UnsupportedAppUsage hidden API .txt lists. '
+             'Classes that have UnsupportedAppUsage API will not be jarjared. Can be followed by '
+             'multiple space-separated paths.')
+    parser.add_argument(
+        '--excludes', nargs='*', default=[],
+        help='Path to files listing classes that should not be jarjared. Can be followed by '
+             'multiple space-separated paths. '
+             'Each file should contain one full-match regex per line. Empty lines or lines '
+             'starting with "#" are ignored.')
+    return parser.parse_args(argv)
+
+
+def _list_toplevel_jar_classes(jar):
+    """List all classes in a .class .jar file that are not inner classes."""
+    return {_get_toplevel_class(c) for c in _list_jar_classes(jar)}
+
+def _list_jar_classes(jar):
+    with ZipFile(jar, 'r') as zip:
+        files = zip.namelist()
+        assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \
+                                           'expected an intermediate zip of .class files'
+        class_len = len('.class')
+        return [f.replace('/', '.')[:-class_len] for f in files
+                if f.endswith('.class') and not f.endswith('/package-info.class')]
+
+
+def _list_hiddenapi_classes(txt_file):
+    out = set()
+    with open(txt_file, 'r') as f:
+        for line in f:
+            if not line.strip():
+                continue
+            assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}'
+            clazz = line.replace('/', '.').split(';')[0][1:]
+            out.add(_get_toplevel_class(clazz))
+    return out
+
+
+def _get_toplevel_class(clazz):
+    """Return the name of the toplevel (not an inner class) enclosing class of the given class."""
+    if '$' not in clazz:
+        return clazz
+    return clazz.split('$')[0]
+
+
+def _get_excludes(path):
+    out = []
+    with open(path, 'r') as f:
+        for line in f:
+            stripped = line.strip()
+            if not stripped or stripped.startswith('#'):
+                continue
+            out.append(re.compile(stripped))
+    return out
+
+
+def make_jarjar_rules(args):
+    excluded_classes = set()
+    for apistubs_file in args.apistubs:
+        excluded_classes.update(_list_toplevel_jar_classes(apistubs_file))
+
+    for unsupportedapi_file in args.unsupportedapi:
+        excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file))
+
+    exclude_regexes = []
+    for exclude_file in args.excludes:
+        exclude_regexes.extend(_get_excludes(exclude_file))
+
+    with open(args.output, 'w') as outfile:
+        for jar in args.jars:
+            jar_classes = _list_jar_classes(jar)
+            jar_classes.sort()
+            for clazz in jar_classes:
+                if (_get_toplevel_class(clazz) not in excluded_classes and
+                        not any(r.fullmatch(clazz) for r in exclude_regexes)):
+                    outfile.write(f'rule {clazz} {args.prefix}.@0\n')
+                    # Also include jarjar rules for unit tests of the class, so the package matches
+                    outfile.write(f'rule {clazz}Test {args.prefix}.@0\n')
+                    outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n')
+
+
+def _main():
+    # Pass in None to use argv
+    args = parse_arguments(None)
+    make_jarjar_rules(args)
+
+
+if __name__ == '__main__':
+    _main()
diff --git a/tools/gen_jarjar_test.py b/tools/gen_jarjar_test.py
new file mode 100644
index 0000000..8d8e82b
--- /dev/null
+++ b/tools/gen_jarjar_test.py
@@ -0,0 +1,57 @@
+#  Copyright (C) 2022 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+import gen_jarjar
+import unittest
+
+
+class TestGenJarjar(unittest.TestCase):
+    def test_gen_rules(self):
+        args = gen_jarjar.parse_arguments([
+            "--jars", "jarjar-rules-generator-testjavalib.jar",
+            "--prefix", "jarjar.prefix",
+            "--output", "test-output-rules.txt",
+            "--apistubs", "framework-connectivity.stubs.module_lib.jar",
+            "--unsupportedapi", "testdata/test-unsupportedappusage.txt",
+            "--excludes", "testdata/test-jarjar-excludes.txt",
+        ])
+        gen_jarjar.make_jarjar_rules(args)
+
+        with open(args.output) as out:
+            lines = out.readlines()
+
+        self.assertListEqual([
+            'rule test.utils.TestUtilClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClassTest$* jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+
+
+if __name__ == '__main__':
+    # Need verbosity=2 for the test results parser to find results
+    unittest.main(verbosity=2)
diff --git a/tools/testdata/java/android/net/LinkProperties.java b/tools/testdata/java/android/net/LinkProperties.java
new file mode 100644
index 0000000..bdca377
--- /dev/null
+++ b/tools/testdata/java/android/net/LinkProperties.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Test class with a name matching a public API.
+ */
+public class LinkProperties {
+}
diff --git a/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
new file mode 100644
index 0000000..7e3bee1
--- /dev/null
+++ b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.jarjarexcluded;
+
+/**
+ * Test class that is excluded from jarjar.
+ */
+public class JarjarExcludedClass {
+}
diff --git a/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
new file mode 100644
index 0000000..9d32296
--- /dev/null
+++ b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.unsupportedappusage;
+
+public class TestUnsupportedAppUsageClass {
+    public void testMethod() {}
+}
diff --git a/tools/testdata/java/test/utils/TestUtilClass.java b/tools/testdata/java/test/utils/TestUtilClass.java
new file mode 100644
index 0000000..2162e45
--- /dev/null
+++ b/tools/testdata/java/test/utils/TestUtilClass.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package test.utils;
+
+/**
+ * Sample class to test jarjar rules.
+ */
+public class TestUtilClass {
+    public static class TestInnerClass {}
+}
diff --git a/tools/testdata/test-jarjar-excludes.txt b/tools/testdata/test-jarjar-excludes.txt
new file mode 100644
index 0000000..35d97a2
--- /dev/null
+++ b/tools/testdata/test-jarjar-excludes.txt
@@ -0,0 +1,3 @@
+# Test file for excluded classes
+test\.jarj.rexcluded\.JarjarExcludedCla.s
+test\.jarjarexcluded\.JarjarExcludedClass\$TestInnerCl.ss
diff --git a/tools/testdata/test-unsupportedappusage.txt b/tools/testdata/test-unsupportedappusage.txt
new file mode 100644
index 0000000..331eff9
--- /dev/null
+++ b/tools/testdata/test-unsupportedappusage.txt
@@ -0,0 +1 @@
+Ltest/unsupportedappusage/TestUnsupportedAppUsageClass;->testMethod()V
\ No newline at end of file