Merge changes I9fb420ec,If00a7dc5,I8025cc2e into main

* changes:
  Make Tethering public API for DO/Carrier apps
  Add stopTethering(TetheringRequest) stubs
  Introduce interfaceName field into TetheringRequestParcel
diff --git a/bpf/loader/NetBpfLoad.cpp b/bpf/loader/NetBpfLoad.cpp
index c2a1d6e..4834b09 100644
--- a/bpf/loader/NetBpfLoad.cpp
+++ b/bpf/loader/NetBpfLoad.cpp
@@ -1544,15 +1544,14 @@
      *
      * Additionally the 32-bit kernel jit support is poor,
      * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
+     * Note, however, that TV and Wear devices will continue to support 32-bit userspace
+     * on ARM64.
      */
     if (isUserspace32bit() && isAtLeastKernelVersion(6, 2, 0)) {
         // Stuff won't work reliably, but...
-        if (isTV()) {
-            // exempt TVs... they don't really need functional advanced networking
-            ALOGW("[TV] 32-bit userspace unsupported on 6.2+ kernels.");
-        } else if (isWear() && isArm()) {
-            // exempt Arm Wear devices (arm32 ABI is far less problematic than x86-32)
-            ALOGW("[Arm Wear] 32-bit userspace unsupported on 6.2+ kernels.");
+        if (isArm() && (isTV() || isWear())) {
+            // exempt Arm TV or Wear devices (arm32 ABI is far less problematic than x86-32)
+            ALOGW("[Arm TV/Wear] 32-bit userspace unsupported on 6.2+ kernels.");
         } else if (first_api_level <= __ANDROID_API_T__ && isArm()) {
             // also exempt Arm devices upgrading with major kernel rev from T-
             // might possibly be better for them to run with a newer kernel...
@@ -1566,8 +1565,8 @@
         }
     }
 
-    // Note: 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
-    if (isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
+    // On handheld, 6.6 is highest version supported by Android V (sdk=35), so this is for sdk=36+
+    if (!isArm() && isUserspace32bit() && isAtLeastKernelVersion(6, 7, 0)) {
         ALOGE("64-bit userspace required on 6.7+ kernels.");
         return 1;
     }
diff --git a/framework-t/src/android/net/TrafficStats.java b/framework-t/src/android/net/TrafficStats.java
index caf3152..81f2cf9 100644
--- a/framework-t/src/android/net/TrafficStats.java
+++ b/framework-t/src/android/net/TrafficStats.java
@@ -184,7 +184,9 @@
 
     @GuardedBy("TrafficStats.class")
     private static INetworkStatsService sStatsService;
-    @GuardedBy("TrafficStats.class")
+
+    // The variable will only be accessed in the test, which is effectively
+    // single-threaded.
     private static INetworkStatsService sStatsServiceForTest = null;
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
@@ -209,9 +211,7 @@
      */
     @VisibleForTesting(visibility = PRIVATE)
     public static void setServiceForTest(INetworkStatsService statsService) {
-        synchronized (TrafficStats.class) {
-            sStatsServiceForTest = statsService;
-        }
+        sStatsServiceForTest = statsService;
     }
 
     /**
diff --git a/framework/Android.bp b/framework/Android.bp
index a5a7d61..a93a532 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -158,6 +158,7 @@
 java_defaults {
     name: "CronetJavaDefaults",
     srcs: [":httpclient_api_sources"],
+    static_libs: ["com.android.net.http.flags-aconfig-java"],
     libs: [
         "androidx.annotation_annotation",
     ],
@@ -218,6 +219,7 @@
     },
     aconfig_declarations: [
         "com.android.net.flags-aconfig",
+        "com.android.net.http.flags-aconfig",
         "com.android.networksecurity.flags-aconfig",
     ],
 }
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index cd7307f..0129e5c 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -247,3 +247,11 @@
 
 }
 
+package android.net.http {
+
+  public abstract class HttpEngine {
+    method @FlaggedApi("android.net.http.preload_httpengine_in_zygote") public static void preload();
+  }
+
+}
+
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index bd8f7b9..56a5ee5 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -75,34 +75,31 @@
         }
     }
 
-    void startPublicKeyDownload(String publicKeyUrl) {
-        long downloadId = download(publicKeyUrl);
-        if (downloadId == -1) {
-            Log.e(TAG, "Metadata download request failed for " + publicKeyUrl);
-            return;
+    long startPublicKeyDownload() {
+        long downloadId = download(mDataStore.getProperty(Config.PUBLIC_KEY_URL));
+        if (downloadId != -1) {
+            mDataStore.setPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, downloadId);
+            mDataStore.store();
         }
-        mDataStore.setPropertyLong(Config.PUBLIC_KEY_URL_KEY, downloadId);
-        mDataStore.store();
+        return downloadId;
     }
 
-    void startMetadataDownload(String metadataUrl) {
-        long downloadId = download(metadataUrl);
-        if (downloadId == -1) {
-            Log.e(TAG, "Metadata download request failed for " + metadataUrl);
-            return;
+    long startMetadataDownload() {
+        long downloadId = download(mDataStore.getProperty(Config.METADATA_URL));
+        if (downloadId != -1) {
+            mDataStore.setPropertyLong(Config.METADATA_DOWNLOAD_ID, downloadId);
+            mDataStore.store();
         }
-        mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
-        mDataStore.store();
+        return downloadId;
     }
 
-    void startContentDownload(String contentUrl) {
-        long downloadId = download(contentUrl);
-        if (downloadId == -1) {
-            Log.e(TAG, "Content download request failed for " + contentUrl);
-            return;
+    long startContentDownload() {
+        long downloadId = download(mDataStore.getProperty(Config.CONTENT_URL));
+        if (downloadId != -1) {
+            mDataStore.setPropertyLong(Config.CONTENT_DOWNLOAD_ID, downloadId);
+            mDataStore.store();
         }
-        mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
-        mDataStore.store();
+        return downloadId;
     }
 
     @Override
@@ -157,7 +154,11 @@
             return;
         }
 
-        startMetadataDownload(mDataStore.getProperty(Config.METADATA_URL_PENDING));
+        if (startMetadataDownload() == -1) {
+            Log.e(TAG, "Metadata download not started.");
+        } else if (Config.DEBUG) {
+            Log.d(TAG, "Metadata download started successfully.");
+        }
     }
 
     private void handleMetadataDownloadCompleted(long downloadId) {
@@ -166,7 +167,11 @@
             handleDownloadFailed(status);
             return;
         }
-        startContentDownload(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
+        if (startContentDownload() == -1) {
+            Log.e(TAG, "Content download not started.");
+        } else if (Config.DEBUG) {
+            Log.d(TAG, "Content download started successfully.");
+        }
     }
 
     private void handleContentDownloadCompleted(long downloadId) {
@@ -204,8 +209,6 @@
             return;
         }
 
-        String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
-        String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
         try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
             success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version);
         } catch (IOException e) {
@@ -216,8 +219,6 @@
         if (success) {
             // Update information about the stored version on successful install.
             mDataStore.setProperty(Config.VERSION, version);
-            mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
-            mDataStore.setProperty(Config.METADATA_URL, metadataUrl);
             mDataStore.store();
         }
     }
@@ -237,29 +238,59 @@
     }
 
     @VisibleForTesting
+    long getPublicKeyDownloadId() {
+        return mDataStore.getPropertyLong(Config.PUBLIC_KEY_DOWNLOAD_ID, -1);
+    }
+
+    @VisibleForTesting
+    long getMetadataDownloadId() {
+        return mDataStore.getPropertyLong(Config.METADATA_DOWNLOAD_ID, -1);
+    }
+
+    @VisibleForTesting
+    long getContentDownloadId() {
+        return mDataStore.getPropertyLong(Config.CONTENT_DOWNLOAD_ID, -1);
+    }
+
+    @VisibleForTesting
+    boolean hasPublicKeyDownloadId() {
+        return getPublicKeyDownloadId() != -1;
+    }
+
+    @VisibleForTesting
+    boolean hasMetadataDownloadId() {
+        return getMetadataDownloadId() != -1;
+    }
+
+    @VisibleForTesting
+    boolean hasContentDownloadId() {
+        return getContentDownloadId() != -1;
+    }
+
+    @VisibleForTesting
     boolean isPublicKeyDownloadId(long downloadId) {
-        return mDataStore.getPropertyLong(Config.PUBLIC_KEY_URL_KEY, -1) == downloadId;
+        return getPublicKeyDownloadId() == downloadId;
     }
 
     @VisibleForTesting
     boolean isMetadataDownloadId(long downloadId) {
-        return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+        return getMetadataDownloadId() == downloadId;
     }
 
     @VisibleForTesting
     boolean isContentDownloadId(long downloadId) {
-        return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+        return getContentDownloadId() == downloadId;
     }
 
     private Uri getPublicKeyDownloadUri() {
-        return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.PUBLIC_KEY_URL_KEY, -1));
+        return mDownloadHelper.getUri(getPublicKeyDownloadId());
     }
 
     private Uri getMetadataDownloadUri() {
-        return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+        return mDownloadHelper.getUri(getMetadataDownloadId());
     }
 
     private Uri getContentDownloadUri() {
-        return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+        return mDownloadHelper.getUri(getContentDownloadId());
     }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index f359a2a..3138ea7 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -115,11 +115,14 @@
 
         // TODO: handle the case where there is already a pending download.
 
-        mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
-        mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
-        mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
+        mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
+        mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
         mDataStore.store();
 
-        mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
+        if (mCertificateTransparencyDownloader.startMetadataDownload() == -1) {
+            Log.e(TAG, "Metadata download not started.");
+        } else if (Config.DEBUG) {
+            Log.d(TAG, "Metadata download started successfully.");
+        }
     }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
index c5d0413..bf23cb0 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyJob.java
@@ -78,11 +78,15 @@
             Log.d(TAG, "Starting CT daily job.");
         }
 
-        mDataStore.setProperty(Config.CONTENT_URL_PENDING, Config.URL_LOG_LIST);
-        mDataStore.setProperty(Config.METADATA_URL_PENDING, Config.URL_SIGNATURE);
-        mDataStore.setProperty(Config.PUBLIC_KEY_URL_PENDING, Config.URL_PUBLIC_KEY);
+        mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
+        mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
+        mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
         mDataStore.store();
 
-        mCertificateTransparencyDownloader.startPublicKeyDownload(Config.URL_PUBLIC_KEY);
+        if (mCertificateTransparencyDownloader.startPublicKeyDownload() == -1) {
+            Log.e(TAG, "Public key download not started.");
+        } else if (Config.DEBUG) {
+            Log.d(TAG, "Public key download started successfully.");
+        }
     }
 }
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index ae30f3a..70d8e42 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -47,21 +47,17 @@
     static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
 
     // properties
-    static final String VERSION_PENDING = "version_pending";
     static final String VERSION = "version";
-    static final String CONTENT_URL_PENDING = "content_url_pending";
     static final String CONTENT_URL = "content_url";
-    static final String CONTENT_URL_KEY = "content_url_key";
-    static final String METADATA_URL_PENDING = "metadata_url_pending";
+    static final String CONTENT_DOWNLOAD_ID = "content_download_id";
     static final String METADATA_URL = "metadata_url";
-    static final String METADATA_URL_KEY = "metadata_url_key";
-    static final String PUBLIC_KEY_URL_PENDING = "public_key_url_pending";
+    static final String METADATA_DOWNLOAD_ID = "metadata_download_id";
     static final String PUBLIC_KEY_URL = "public_key_url";
-    static final String PUBLIC_KEY_URL_KEY = "public_key_url_key";
+    static final String PUBLIC_KEY_DOWNLOAD_ID = "public_key_download_id";
 
     // URLs
-    static final String URL_BASE = "https://www.gstatic.com/android/certificate_transparency/";
-    static final String URL_LOG_LIST = URL_BASE + "log_list.json";
-    static final String URL_SIGNATURE = URL_BASE + "log_list.sig";
-    static final String URL_PUBLIC_KEY = URL_BASE + "log_list.pub";
+    static final String URL_PREFIX = "https://www.gstatic.com/android/certificate_transparency/";
+    static final String URL_LOG_LIST = URL_PREFIX + "log_list.json";
+    static final String URL_SIGNATURE = URL_PREFIX + "log_list.sig";
+    static final String URL_PUBLIC_KEY = URL_PREFIX + "log_list.pub";
 }
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index 87d75e6..ffa1283 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -27,14 +27,16 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
 import android.content.Context;
 import android.content.Intent;
+import android.database.Cursor;
+import android.database.MatrixCursor;
 import android.net.Uri;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.server.net.ct.DownloadHelper.DownloadStatus;
-
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.junit.After;
@@ -63,7 +65,7 @@
 @RunWith(JUnit4.class)
 public class CertificateTransparencyDownloaderTest {
 
-    @Mock private DownloadHelper mDownloadHelper;
+    @Mock private DownloadManager mDownloadManager;
     @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
 
     private PrivateKey mPrivateKey;
@@ -79,7 +81,6 @@
     @Before
     public void setUp() throws IOException, GeneralSecurityException {
         MockitoAnnotations.initMocks(this);
-
         KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
         KeyPair keyPair = instance.generateKeyPair();
         mPrivateKey = keyPair.getPrivate();
@@ -88,16 +89,17 @@
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mTempFile = File.createTempFile("datastore-test", ".properties");
         mDataStore = new DataStore(mTempFile);
-        mDataStore.load();
         mSignatureVerifier = new SignatureVerifier(mContext);
-
         mCertificateTransparencyDownloader =
                 new CertificateTransparencyDownloader(
                         mContext,
                         mDataStore,
-                        mDownloadHelper,
+                        new DownloadHelper(mDownloadManager),
                         mSignatureVerifier,
                         mCertificateTransparencyInstaller);
+
+        prepareDataStore();
+        prepareDownloadManager();
     }
 
     @After
@@ -108,116 +110,108 @@
 
     @Test
     public void testDownloader_startPublicKeyDownload() {
-        String publicKeyUrl = "http://test-public-key.org";
-        long downloadId = preparePublicKeyDownload(publicKeyUrl);
+        assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isFalse();
+        long downloadId = mCertificateTransparencyDownloader.startPublicKeyDownload();
 
-        assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isFalse();
-        mCertificateTransparencyDownloader.startPublicKeyDownload(publicKeyUrl);
+        assertThat(mCertificateTransparencyDownloader.hasPublicKeyDownloadId()).isTrue();
         assertThat(mCertificateTransparencyDownloader.isPublicKeyDownloadId(downloadId)).isTrue();
     }
 
     @Test
     public void testDownloader_startMetadataDownload() {
-        String metadataUrl = "http://test-metadata.org";
-        long downloadId = prepareMetadataDownload(metadataUrl);
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
+        long downloadId = mCertificateTransparencyDownloader.startMetadataDownload();
 
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
-        mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
         assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
     }
 
     @Test
     public void testDownloader_startContentDownload() {
-        String contentUrl = "http://test-content.org";
-        long downloadId = prepareContentDownload(contentUrl);
+        assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
+        long downloadId = mCertificateTransparencyDownloader.startContentDownload();
 
-        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
-        mCertificateTransparencyDownloader.startContentDownload(contentUrl);
+        assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
         assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
     }
 
     @Test
     public void testDownloader_publicKeyDownloadSuccess_updatePublicKey_startMetadataDownload()
             throws Exception {
-        long publicKeyId = prepareSuccessfulPublicKeyDownload(writePublicKeyToFile(mPublicKey));
-        long metadataId = prepareMetadataDownload("http://test-metadata.org");
+        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+        setSuccessfulDownload(publicKeyId, writePublicKeyToFile(mPublicKey));
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(metadataId)).isFalse();
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(publicKeyId));
 
         assertThat(mSignatureVerifier.getPublicKey()).hasValue(mPublicKey);
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(metadataId)).isTrue();
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isTrue();
     }
 
     @Test
     public void
             testDownloader_publicKeyDownloadSuccess_updatePublicKeyFail_doNotStartMetadataDownload()
                     throws Exception {
-        long publicKeyId =
-                prepareSuccessfulPublicKeyDownload(
-                        writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
-        long metadataId = prepareMetadataDownload("http://test-metadata.org");
+        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+        setSuccessfulDownload(
+                publicKeyId, writeToFile("i_am_not_a_base64_encoded_public_key".getBytes()));
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(metadataId)).isFalse();
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(publicKeyId));
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(metadataId)).isFalse();
-        verify(mDownloadHelper, never()).startDownload(anyString());
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
     }
 
     @Test
     public void testDownloader_publicKeyDownloadFail_doNotUpdatePublicKey() throws Exception {
-        long publicKeyId =
-                prepareFailedPublicKeyDownload(
-                        // Failure cases where we give up on the download.
-                        DownloadManager.ERROR_INSUFFICIENT_SPACE,
-                        DownloadManager.ERROR_HTTP_DATA_ERROR);
+        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
+        setFailedDownload(
+                publicKeyId, // Failure cases where we give up on the download.
+                DownloadManager.ERROR_INSUFFICIENT_SPACE,
+                DownloadManager.ERROR_HTTP_DATA_ERROR);
         Intent downloadCompleteIntent = makeDownloadCompleteIntent(publicKeyId);
-        long metadataId = prepareMetadataDownload("http://test-metadata.org");
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(metadataId)).isFalse();
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
         mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
 
         assertThat(mSignatureVerifier.getPublicKey()).isEmpty();
-        assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(metadataId)).isFalse();
-        verify(mDownloadHelper, never()).startDownload(anyString());
+        assertThat(mCertificateTransparencyDownloader.hasMetadataDownloadId()).isFalse();
     }
 
     @Test
     public void testDownloader_metadataDownloadSuccess_startContentDownload() {
-        long metadataId = prepareSuccessfulMetadataDownload(new File("log_list.sig"));
-        long contentId = prepareContentDownload("http://test-content.org");
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, new File("log_list.sig"));
 
-        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isFalse();
+        assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(metadataId));
 
-        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
+        assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isTrue();
     }
 
     @Test
     public void testDownloader_metadataDownloadFail_doNotStartContentDownload() {
-        long metadataId =
-                prepareFailedMetadataDownload(
-                        // Failure cases where we give up on the download.
-                        DownloadManager.ERROR_INSUFFICIENT_SPACE,
-                        DownloadManager.ERROR_HTTP_DATA_ERROR);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setFailedDownload(
+                metadataId,
+                // Failure cases where we give up on the download.
+                DownloadManager.ERROR_INSUFFICIENT_SPACE,
+                DownloadManager.ERROR_HTTP_DATA_ERROR);
         Intent downloadCompleteIntent = makeDownloadCompleteIntent(metadataId);
-        long contentId = prepareContentDownload("http://test-content.org");
 
-        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isFalse();
+        assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
         mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
         mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
 
-        assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isFalse();
-        verify(mDownloadHelper, never()).startDownload(anyString());
+        assertThat(mCertificateTransparencyDownloader.hasContentDownloadId()).isFalse();
     }
 
     @Test
@@ -227,8 +221,10 @@
         File logListFile = makeLogListFile(newVersion);
         File metadataFile = sign(logListFile);
         mSignatureVerifier.setPublicKey(mPublicKey);
-        prepareSuccessfulMetadataDownload(metadataFile);
-        long contentId = prepareSuccessfulContentDownload(logListFile);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
         when(mCertificateTransparencyInstaller.install(
                         eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
                 .thenReturn(true);
@@ -242,11 +238,12 @@
 
     @Test
     public void testDownloader_contentDownloadFail_doNotInstall() throws Exception {
-        long contentId =
-                prepareFailedContentDownload(
-                        // Failure cases where we give up on the download.
-                        DownloadManager.ERROR_INSUFFICIENT_SPACE,
-                        DownloadManager.ERROR_HTTP_DATA_ERROR);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setFailedDownload(
+                contentId,
+                // Failure cases where we give up on the download.
+                DownloadManager.ERROR_INSUFFICIENT_SPACE,
+                DownloadManager.ERROR_HTTP_DATA_ERROR);
         Intent downloadCompleteIntent = makeDownloadCompleteIntent(contentId);
 
         mCertificateTransparencyDownloader.onReceive(mContext, downloadCompleteIntent);
@@ -262,8 +259,10 @@
         File logListFile = makeLogListFile("456");
         File metadataFile = sign(logListFile);
         mSignatureVerifier.setPublicKey(mPublicKey);
-        prepareSuccessfulMetadataDownload(metadataFile);
-        long contentId = prepareSuccessfulContentDownload(logListFile);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
         when(mCertificateTransparencyInstaller.install(
                         eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
                 .thenReturn(false);
@@ -281,8 +280,10 @@
         File logListFile = makeLogListFile("456");
         File metadataFile = File.createTempFile("log_list-wrong_metadata", "sig");
         mSignatureVerifier.setPublicKey(mPublicKey);
-        prepareSuccessfulMetadataDownload(metadataFile);
-        long contentId = prepareSuccessfulContentDownload(logListFile);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
 
         assertNoVersionIsInstalled();
         mCertificateTransparencyDownloader.onReceive(
@@ -299,8 +300,10 @@
         File logListFile = makeLogListFile("456");
         File metadataFile = sign(logListFile);
         mSignatureVerifier.resetPublicKey();
-        prepareSuccessfulMetadataDownload(metadataFile);
-        long contentId = prepareSuccessfulContentDownload(logListFile);
+        long metadataId = mCertificateTransparencyDownloader.startMetadataDownload();
+        setSuccessfulDownload(metadataId, metadataFile);
+        long contentId = mCertificateTransparencyDownloader.startContentDownload();
+        setSuccessfulDownload(contentId, logListFile);
 
         assertNoVersionIsInstalled();
         mCertificateTransparencyDownloader.onReceive(
@@ -321,26 +324,23 @@
         assertNoVersionIsInstalled();
 
         // 1. Start download of public key.
-        String publicKeyUrl = "http://test-public-key.org";
-        long publicKeyId = preparePublicKeyDownload(publicKeyUrl);
-
-        mCertificateTransparencyDownloader.startPublicKeyDownload(publicKeyUrl);
+        long publicKeyId = mCertificateTransparencyDownloader.startPublicKeyDownload();
 
         // 2. On successful public key download, set the key and start the metatadata download.
         setSuccessfulDownload(publicKeyId, publicKeyFile);
-        long metadataId = prepareMetadataDownload("http://test-metadata.org");
 
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(publicKeyId));
 
         // 3. On successful metadata download, start the content download.
+        long metadataId = mCertificateTransparencyDownloader.getMetadataDownloadId();
         setSuccessfulDownload(metadataId, metadataFile);
-        long contentId = prepareContentDownload("http://test-content.org");
 
         mCertificateTransparencyDownloader.onReceive(
                 mContext, makeDownloadCompleteIntent(metadataId));
 
         // 4. On successful content download, verify the signature and install the new version.
+        long contentId = mCertificateTransparencyDownloader.getContentDownloadId();
         setSuccessfulDownload(contentId, logListFile);
         when(mCertificateTransparencyInstaller.install(
                         eq(Config.COMPATIBILITY_VERSION), any(), anyString()))
@@ -354,16 +354,10 @@
 
     private void assertNoVersionIsInstalled() {
         assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
-        assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
-        assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
     }
 
     private void assertInstallSuccessful(String version) {
         assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
-        assertThat(mDataStore.getProperty(Config.CONTENT_URL))
-                .isEqualTo(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
-        assertThat(mDataStore.getProperty(Config.METADATA_URL))
-                .isEqualTo(mDataStore.getProperty(Config.METADATA_URL_PENDING));
     }
 
     private Intent makeDownloadCompleteIntent(long downloadId) {
@@ -371,110 +365,51 @@
                 .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
     }
 
-    private long prepareDownloadId(String url) {
-        long downloadId = mNextDownloadId++;
-        when(mDownloadHelper.startDownload(url)).thenReturn(downloadId);
-        return downloadId;
+    private void prepareDataStore() {
+        mDataStore.load();
+        mDataStore.setProperty(Config.CONTENT_URL, Config.URL_LOG_LIST);
+        mDataStore.setProperty(Config.METADATA_URL, Config.URL_SIGNATURE);
+        mDataStore.setProperty(Config.PUBLIC_KEY_URL, Config.URL_PUBLIC_KEY);
     }
 
-    private long preparePublicKeyDownload(String url) {
-        long downloadId = prepareDownloadId(url);
-        mDataStore.setProperty(Config.PUBLIC_KEY_URL_PENDING, url);
-        return downloadId;
+    private void prepareDownloadManager() {
+        when(mDownloadManager.enqueue(any(Request.class)))
+                .thenAnswer(invocation -> mNextDownloadId++);
     }
 
-    private long prepareMetadataDownload(String url) {
-        long downloadId = prepareDownloadId(url);
-        mDataStore.setProperty(Config.METADATA_URL_PENDING, url);
-        return downloadId;
-    }
-
-    private long prepareContentDownload(String url) {
-        long downloadId = prepareDownloadId(url);
-        mDataStore.setProperty(Config.CONTENT_URL_PENDING, url);
-        return downloadId;
-    }
-
-    private long prepareSuccessfulDownload(String propertyKey) {
-        long downloadId = mNextDownloadId++;
-        mDataStore.setPropertyLong(propertyKey, downloadId);
-        when(mDownloadHelper.getDownloadStatus(downloadId))
-                .thenReturn(makeSuccessfulDownloadStatus(downloadId));
-        return downloadId;
-    }
-
-    private long prepareSuccessfulDownload(String propertyKey, File file) {
-        long downloadId = prepareSuccessfulDownload(propertyKey);
-        when(mDownloadHelper.getUri(downloadId)).thenReturn(Uri.fromFile(file));
-        return downloadId;
-    }
-
-    private long prepareSuccessfulPublicKeyDownload(File file) {
-        long downloadId = prepareSuccessfulDownload(Config.PUBLIC_KEY_URL_KEY, file);
-        mDataStore.setProperty(
-                Config.METADATA_URL_PENDING, "http://public-key-was-downloaded-here.org");
-        return downloadId;
-    }
-
-    private long prepareSuccessfulMetadataDownload(File file) {
-        long downloadId = prepareSuccessfulDownload(Config.METADATA_URL_KEY, file);
-        mDataStore.setProperty(
-                Config.METADATA_URL_PENDING, "http://metadata-was-downloaded-here.org");
-        return downloadId;
-    }
-
-    private long prepareSuccessfulContentDownload(File file) {
-        long downloadId = prepareSuccessfulDownload(Config.CONTENT_URL_KEY, file);
-        mDataStore.setProperty(
-                Config.CONTENT_URL_PENDING, "http://content-was-downloaded-here.org");
-        return downloadId;
+    private Cursor makeSuccessfulDownloadCursor() {
+        MatrixCursor cursor =
+                new MatrixCursor(
+                        new String[] {
+                            DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_REASON
+                        });
+        cursor.addRow(new Object[] {DownloadManager.STATUS_SUCCESSFUL, -1});
+        return cursor;
     }
 
     private void setSuccessfulDownload(long downloadId, File file) {
-        when(mDownloadHelper.getDownloadStatus(downloadId))
-                .thenReturn(makeSuccessfulDownloadStatus(downloadId));
-        when(mDownloadHelper.getUri(downloadId)).thenReturn(Uri.fromFile(file));
+        when(mDownloadManager.query(any(Query.class))).thenReturn(makeSuccessfulDownloadCursor());
+        when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(Uri.fromFile(file));
     }
 
-    private long prepareFailedDownload(String propertyKey, int... downloadManagerErrors) {
-        long downloadId = mNextDownloadId++;
-        mDataStore.setPropertyLong(propertyKey, downloadId);
-        DownloadStatus firstError =
-                DownloadStatus.builder()
-                        .setDownloadId(downloadId)
-                        .setStatus(DownloadManager.STATUS_FAILED)
-                        .setReason(downloadManagerErrors[0])
-                        .build();
-        DownloadStatus[] otherErrors = new DownloadStatus[downloadManagerErrors.length - 1];
+    private Cursor makeFailedDownloadCursor(int error) {
+        MatrixCursor cursor =
+                new MatrixCursor(
+                        new String[] {
+                            DownloadManager.COLUMN_STATUS, DownloadManager.COLUMN_REASON
+                        });
+        cursor.addRow(new Object[] {DownloadManager.STATUS_FAILED, error});
+        return cursor;
+    }
+
+    private void setFailedDownload(long downloadId, int... downloadManagerErrors) {
+        Cursor first = makeFailedDownloadCursor(downloadManagerErrors[0]);
+        Cursor[] others = new Cursor[downloadManagerErrors.length - 1];
         for (int i = 1; i < downloadManagerErrors.length; i++) {
-            otherErrors[i - 1] =
-                    DownloadStatus.builder()
-                            .setDownloadId(downloadId)
-                            .setStatus(DownloadManager.STATUS_FAILED)
-                            .setReason(downloadManagerErrors[i])
-                            .build();
+            others[i - 1] = makeFailedDownloadCursor(downloadManagerErrors[i]);
         }
-        when(mDownloadHelper.getDownloadStatus(downloadId)).thenReturn(firstError, otherErrors);
-        return downloadId;
-    }
-
-    private long prepareFailedPublicKeyDownload(int... downloadManagerErrors) {
-        return prepareFailedDownload(Config.PUBLIC_KEY_URL_KEY, downloadManagerErrors);
-    }
-
-    private long prepareFailedMetadataDownload(int... downloadManagerErrors) {
-        return prepareFailedDownload(Config.METADATA_URL_KEY, downloadManagerErrors);
-    }
-
-    private long prepareFailedContentDownload(int... downloadManagerErrors) {
-        return prepareFailedDownload(Config.CONTENT_URL_KEY, downloadManagerErrors);
-    }
-
-    private DownloadStatus makeSuccessfulDownloadStatus(long downloadId) {
-        return DownloadStatus.builder()
-                .setDownloadId(downloadId)
-                .setStatus(DownloadManager.STATUS_SUCCESSFUL)
-                .build();
+        when(mDownloadManager.query(any())).thenReturn(first, others);
+        when(mDownloadManager.getUriForDownloadedFile(downloadId)).thenReturn(null);
     }
 
     private File writePublicKeyToFile(PublicKey publicKey)
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 0d0f6fc..f3b97bc 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -121,7 +121,6 @@
 import static android.net.connectivity.ConnectivityCompatChanges.NETWORK_BLOCKED_WITHOUT_INTERNET_PERMISSION;
 import static android.os.Process.INVALID_UID;
 import static android.os.Process.VPN_UID;
-import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
 import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -149,6 +148,7 @@
 import static com.android.server.connectivity.ConnectivityFlags.CELLULAR_DATA_INACTIVITY_TIMEOUT;
 import static com.android.server.connectivity.ConnectivityFlags.DELAY_DESTROY_SOCKETS;
 import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
+import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHERING_BOOT;
 import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS;
 import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT;
@@ -1615,13 +1615,13 @@
 
         /** Returns the data inactivity timeout to be used for cellular networks */
         public int getDefaultCellularDataInactivityTimeout() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
                     CELLULAR_DATA_INACTIVITY_TIMEOUT, 10);
         }
 
         /** Returns the data inactivity timeout to be used for WiFi networks */
         public int getDefaultWifiDataInactivityTimeout() {
-            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING,
+            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_TETHERING_BOOT,
                     WIFI_DATA_INACTIVITY_TIMEOUT, 15);
         }
 
diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java
index 93335f1..136ea81 100644
--- a/service/src/com/android/server/connectivity/ConnectivityFlags.java
+++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java
@@ -26,6 +26,11 @@
  */
 public final class ConnectivityFlags {
     /**
+     * Boot namespace for this module. Values from this should only be read at boot.
+     */
+    public static final String NAMESPACE_TETHERING_BOOT = "tethering_boot";
+
+    /**
      * Minimum module version at which to avoid rematching all requests when a network request is
      * registered, and rematch only the registered requests instead.
      */
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 781a04e..dfb2053 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -150,6 +150,8 @@
                 return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWNDUSEROPT:
                 return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWPREFIX:
+                return (NetlinkMessage) RtNetlinkPrefixMessage.parse(nlmsghdr, byteBuffer);
             default: return null;
         }
     }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
new file mode 100644
index 0000000..30c63fb
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkPrefixMessage.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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.net.module.util.netlink;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink address messages.
+ *
+ * RtNetlinkPrefixMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkPrefixMessage extends NetlinkMessage {
+    public static final short PREFIX_ADDRESS       = 1;
+    public static final short PREFIX_CACHEINFO     = 2;
+
+    @NonNull
+    private StructPrefixMsg mPrefixmsg;
+    @NonNull
+    private IpPrefix mPrefix;
+    private long mPreferredLifetime;
+    private long mValidLifetime;
+
+    @VisibleForTesting
+    public RtNetlinkPrefixMessage(@NonNull final StructNlMsgHdr header,
+            @NonNull final StructPrefixMsg prefixmsg,
+            @NonNull final IpPrefix prefix,
+            long preferred, long valid) {
+        super(header);
+        mPrefixmsg = prefixmsg;
+        mPrefix = prefix;
+        mPreferredLifetime = preferred;
+        mValidLifetime = valid;
+    }
+
+    private RtNetlinkPrefixMessage(@NonNull StructNlMsgHdr header) {
+        this(header, null, null, 0 /* preferredLifetime */, 0 /* validLifetime */);
+    }
+
+    @NonNull
+    public StructPrefixMsg getPrefixMsg() {
+        return mPrefixmsg;
+    }
+
+    @NonNull
+    public IpPrefix getPrefix() {
+        return mPrefix;
+    }
+
+    public long getPreferredLifetime() {
+        return mPreferredLifetime;
+    }
+
+    public long getValidLifetime() {
+        return mValidLifetime;
+    }
+
+    /**
+     * Parse rtnetlink prefix message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * RTM_NEWPREFIX Message Format:
+     *  +----------+- - -+-------------+- - -+---------------------+-----------------------+
+     *  | nlmsghdr | Pad |  prefixmsg  | Pad | PREFIX_ADDRESS attr | PREFIX_CACHEINFO attr |
+     *  +----------+- - -+-------------+- - -+---------------------+-----------------------+
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkPrefixMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        try {
+            final RtNetlinkPrefixMessage msg = new RtNetlinkPrefixMessage(header);
+            msg.mPrefixmsg = StructPrefixMsg.parse(byteBuffer);
+
+            // PREFIX_ADDRESS
+            final int baseOffset = byteBuffer.position();
+            StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_ADDRESS, byteBuffer);
+            if (nlAttr == null) return null;
+            final Inet6Address addr = (Inet6Address) nlAttr.getValueAsInetAddress();
+            if (addr == null) return null;
+            msg.mPrefix = new IpPrefix(addr, msg.mPrefixmsg.prefix_len);
+
+            // PREFIX_CACHEINFO
+            byteBuffer.position(baseOffset);
+            nlAttr = StructNlAttr.findNextAttrOfType(PREFIX_CACHEINFO, byteBuffer);
+            if (nlAttr == null) return null;
+            final ByteBuffer buffer = nlAttr.getValueAsByteBuffer();
+            if (buffer == null) return null;
+            final StructPrefixCacheInfo cacheinfo = StructPrefixCacheInfo.parse(buffer);
+            msg.mPreferredLifetime = cacheinfo.preferred_time;
+            msg.mValidLifetime = cacheinfo.valid_time;
+
+            return msg;
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Write a rtnetlink prefix message to {@link ByteBuffer}.
+     */
+    @VisibleForTesting
+    protected void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mPrefixmsg.pack(byteBuffer);
+
+        // PREFIX_ADDRESS attribute
+        final StructNlAttr prefixAddress =
+                new StructNlAttr(PREFIX_ADDRESS, mPrefix.getRawAddress());
+        prefixAddress.pack(byteBuffer);
+
+        // PREFIX_CACHEINFO attribute
+        final StructPrefixCacheInfo cacheinfo =
+                new StructPrefixCacheInfo(mPreferredLifetime, mValidLifetime);
+        final StructNlAttr prefixCacheinfo =
+                new StructNlAttr(PREFIX_CACHEINFO, cacheinfo.writeToBytes());
+        prefixCacheinfo.pack(byteBuffer);
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkPrefixMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "prefixmsg{" + mPrefixmsg.toString() + "}, "
+                + "IP Prefix{" + mPrefix + "}, "
+                + "preferred lifetime{" + mPreferredLifetime + "}, "
+                + "valid lifetime{" + mValidLifetime + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
index 851d09a..bde55c3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
@@ -17,14 +17,19 @@
 package com.android.net.module.util
 
 import android.util.Log
+import com.android.testutils.TryTestConfig
 import com.android.testutils.tryTest
+import java.util.function.Consumer
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
-import kotlin.test.fail
 
 private val TAG = CleanupTest::class.simpleName
 
@@ -34,6 +39,18 @@
     class TestException2 : Exception()
     class TestException3 : Exception()
 
+    private var originalDiagnosticsCollector: Consumer<Throwable>? = null
+
+    @Before
+    fun setUp() {
+        originalDiagnosticsCollector = TryTestConfig.swapDiagnosticsCollector(null)
+    }
+
+    @After
+    fun tearDown() {
+        TryTestConfig.swapDiagnosticsCollector(originalDiagnosticsCollector)
+    }
+
     @Test
     fun testNotThrow() {
         var x = 1
@@ -220,4 +237,74 @@
         assertTrue(thrown.suppressedExceptions[1] is TestException3)
         assert(x == 7)
     }
+
+    @Test
+    fun testNoErrorReportingWhenCaught() {
+        var error: Throwable? = null
+        TryTestConfig.swapDiagnosticsCollector {
+            error = it
+        }
+        var x = 1
+        tryTest {
+            x = 2
+            throw TestException1()
+            x = 3
+        }.catch<TestException1> {
+            x = 4
+        } cleanup {
+            x = 5
+        }
+
+        assertEquals(5, x)
+        assertNull(error)
+    }
+
+    @Test
+    fun testErrorReportingInTry() {
+        var error: Throwable? = null
+        TryTestConfig.swapDiagnosticsCollector {
+            assertNull(error)
+            error = it
+        }
+        var x = 1
+        assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+                x = 3
+            } cleanupStep {
+                throw TestException2()
+                x = 4
+            } cleanup {
+                x = 5
+            }
+        }
+
+        assertEquals(5, x)
+        assertTrue(error is TestException1)
+    }
+
+    @Test
+    fun testErrorReportingInCatch() {
+        var error: Throwable? = null
+        TryTestConfig.swapDiagnosticsCollector {
+            assertNull(error)
+            error = it
+        }
+        var x = 1
+        assertFailsWith<TestException2> {
+            tryTest {
+                throw TestException1()
+                x = 2
+            }.catch<TestException1> {
+                throw TestException2()
+                x = 3
+            } cleanup {
+                x = 4
+            }
+        }
+
+        assertEquals(4, x)
+        assertTrue(error is TestException2)
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
index 4ed3afd..7244803 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CollectionUtilsTest.kt
@@ -183,17 +183,17 @@
 
     @Test
     fun testGetIndexForValue() {
-        val sparseArray = SparseArray<String>();
-        sparseArray.put(5, "hello");
-        sparseArray.put(10, "abcd");
-        sparseArray.put(20, null);
+        val sparseArray = SparseArray<String>()
+        sparseArray.put(5, "hello")
+        sparseArray.put(10, "abcd")
+        sparseArray.put(20, null)
 
-        val value1 = "abcd";
+        val value1 = "abcd"
         val value1Copy = String(value1.toCharArray())
-        val value2 = null;
+        val value2 = null
 
-        assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1));
-        assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy));
-        assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2));
+        assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1))
+        assertEquals(1, CollectionUtils.getIndexForValue(sparseArray, value1Copy))
+        assertEquals(2, CollectionUtils.getIndexForValue(sparseArray, value2))
     }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
new file mode 100644
index 0000000..b1779cb
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkPrefixMessageTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 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.net.module.util.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkPrefixMessageTest {
+    private static final IpPrefix TEST_PREFIX = new IpPrefix("2001:db8:1:1::/64");
+
+    // An example of the full RTM_NEWPREFIX message.
+    private static final String RTM_NEWPREFIX_HEX =
+            "3C000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "1400010020010DB8000100010000000000000000"  // PREFIX_ADDRESS
+            + "0C000200803A0900008D2700";                 // PREFIX_CACHEINFO
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    @Test
+    public void testParseRtmNewPrefix() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkPrefixMessage);
+        final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+        final StructNlMsgHdr hdr = prefixmsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(60, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWPREFIX, hdr.nlmsg_type);
+        assertEquals(0, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructPrefixMsg prefixmsgHdr = prefixmsg.getPrefixMsg();
+        assertNotNull(prefixmsgHdr);
+        assertEquals((byte) OsConstants.AF_INET6, prefixmsgHdr.prefix_family);
+        assertEquals(3, prefixmsgHdr.prefix_type);
+        assertEquals(64, prefixmsgHdr.prefix_len);
+        assertEquals(0x03, prefixmsgHdr.prefix_flags);
+        assertEquals(0x2F, prefixmsgHdr.prefix_ifindex);
+
+        assertEquals(prefixmsg.getPrefix(), TEST_PREFIX);
+        assertEquals(604800L, prefixmsg.getPreferredLifetime());
+        assertEquals(2592000L, prefixmsg.getValidLifetime());
+    }
+
+    @Test
+    public void testPackRtmNewPrefix() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkPrefixMessage);
+        final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(60);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        prefixmsg.pack(packBuffer);
+        assertEquals(RTM_NEWPREFIX_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX =
+            "24000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "0C000200803A0900008D2700";                 // PREFIX_CACHEINFO
+
+    @Test
+    public void testParseRtmNewPrefix_withoutPrefixAddressAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_ADDRESS_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX =
+            "30000000340000000000000000000000"             // struct nlmsghr
+            + "0A0000002F00000003400300"                   // struct prefixmsg
+            + "140001002A0079E10ABCF6050000000000000000";  // PREFIX_ADDRESS
+
+    @Test
+    public void testParseRtmNewPrefix_withoutPrefixCacheinfoAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_WITHOUT_PREFIX_CACHEINFO_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX =
+            "3C000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "140001002A0079E10ABCF605000000000000"      // PREFIX_ADDRESS (truncated)
+            + "0C000200803A0900008D2700";                 // PREFIX_CACHEINFO
+
+    @Test
+    public void testParseRtmNewPrefix_truncatedPrefixAddressAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_ADDRESS_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX =
+            "3C000000340000000000000000000000"            // struct nlmsghr
+            + "0A0000002F00000003400300"                  // struct prefixmsg
+            + "140001002A0079E10ABCF6050000000000000000"  // PREFIX_ADDRESS
+            + "0C000200803A0900008D";                     // PREFIX_CACHEINFO (truncated)
+
+    @Test
+    public void testParseRtmNewPrefix_truncatedPrefixCacheinfoAttribute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_TRUNCATED_PREFIX_CACHEINFO_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNull(msg);
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWPREFIX_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkPrefixMessage);
+        final RtNetlinkPrefixMessage prefixmsg = (RtNetlinkPrefixMessage) msg;
+        final String expected = "RtNetlinkPrefixMessage{ "
+                + "nlmsghdr{StructNlMsgHdr{ nlmsg_len{60}, nlmsg_type{52(RTM_NEWPREFIX)}, "
+                + "nlmsg_flags{0()}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "prefixmsg{prefix_family: 10, prefix_ifindex: 47, prefix_type: 3, "
+                + "prefix_len: 64, prefix_flags: 3}, "
+                + "IP Prefix{2001:db8:1:1::/64}, "
+                + "preferred lifetime{604800}, valid lifetime{2592000} }";
+        assertEquals(expected, prefixmsg.toString());
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 2a26ef8..86aa8f1 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -98,6 +98,7 @@
         "cts",
         "mts-networking",
         "mts-tethering",
+        "mcts-tethering",
     ],
     device_common_data: [":ConnectivityTestPreparer"],
 }
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index e634f0e..8e27c62 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -16,27 +16,167 @@
 
 package com.android.testutils.connectivitypreparer
 
+import android.Manifest.permission.NETWORK_SETTINGS
 import android.content.pm.PackageManager.FEATURE_TELEPHONY
 import android.content.pm.PackageManager.FEATURE_WIFI
+import android.net.LinkAddress
+import android.net.Network
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
 import android.telephony.TelephonyManager
+import android.util.Log
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.net.module.util.HexDump
+import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY
+import com.android.testutils.AutoReleaseNetworkCallbackRule
 import com.android.testutils.ConnectUtil
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import java.io.IOException
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.util.Random
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private const val QUIC_SOCKET_TIMEOUT_MS = 5_000
+private const val QUIC_RETRY_COUNT = 5
+
 @RunWith(AndroidJUnit4::class)
 class ConnectivityCheckTest {
+    @get:Rule
+    val networkCallbackRule = AutoReleaseNetworkCallbackRule()
+
+    private val logTag = ConnectivityCheckTest::class.simpleName
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val pm by lazy { context.packageManager }
     private val connectUtil by lazy { ConnectUtil(context) }
 
+    // Skip IPv6 checks on virtual devices which do not support it. Tests that require IPv6 will
+    // still fail even if the preparer does not.
+    private fun ipv6Unsupported(wifiSsid: String?) = ConnectUtil.VIRTUAL_SSIDS.contains(
+        WifiInfo.sanitizeSsid(wifiSsid))
+
     @Test
     fun testCheckWifiSetup() {
         if (!pm.hasSystemFeature(FEATURE_WIFI)) return
         connectUtil.ensureWifiValidated()
+
+        val (wifiNetwork, wifiSsid) = runAsShell(NETWORK_SETTINGS) {
+            val cb = networkCallbackRule.requestNetwork(
+                NetworkRequest.Builder()
+                    .addTransportType(TRANSPORT_WIFI)
+                    .addCapability(NET_CAPABILITY_INTERNET)
+                    .build()
+            )
+            val capChanged = cb.eventuallyExpect<CapabilitiesChanged>(from = 0)
+            val network = capChanged.network
+            val ssid = capChanged.caps.ssid
+            assertFalse(ssid.isNullOrEmpty(), "No SSID for wifi network $network")
+            // Expect a global IPv6 address, and native or stacked IPv4
+            val lpChange = cb.history.poll(
+                pos = 0,
+                timeoutMs = 30_000L
+            ) {
+                it is LinkPropertiesChanged &&
+                it.network == network &&
+                it.lp.allLinkAddresses.any(LinkAddress::isIpv4) &&
+                        (ipv6Unsupported(ssid) || it.lp.hasGlobalIpv6Address())
+            }
+            assertNotNull(lpChange, "Wifi network $network needs an IPv4 address" +
+                    if (ipv6Unsupported(ssid)) "" else " and a global IPv6 address")
+
+            Pair(network, ssid)
+        }
+
+        // Checking QUIC is more important on Wi-Fi than cellular, as it finds firewall
+        // configuration problems on Wi-Fi, but cellular is not actionable by the test lab.
+        checkQuic(wifiNetwork, wifiSsid, ipv6 = false)
+        if (!ipv6Unsupported(wifiSsid)) {
+            checkQuic(wifiNetwork, wifiSsid, ipv6 = true)
+        }
+    }
+
+    /**
+     * Check that QUIC is working on the specified network.
+     *
+     * Some tests require QUIC (UDP), and some lab networks have been observed to not let it
+     * through due to firewalling. Ensure that devices are setup on a network that has the proper
+     * allowlists before trying to run the tests.
+     */
+    private fun checkQuic(network: Network, ssid: String, ipv6: Boolean) {
+        // Same endpoint as used in MultinetworkApiTest in CTS
+        val hostname = "connectivitycheck.android.com"
+        val targetAddrs = network.getAllByName(hostname)
+        val bindAddr = if (ipv6) IPV6_ADDR_ANY else IPV4_ADDR_ANY
+        if (targetAddrs.isEmpty()) {
+            Log.d(logTag, "No addresses found for $hostname")
+            return
+        }
+
+        val socket = DatagramSocket(0, bindAddr)
+        tryTest {
+            socket.soTimeout = QUIC_SOCKET_TIMEOUT_MS
+            network.bindSocket(socket)
+
+            // For reference see Version-Independent Properties of QUIC:
+            // https://datatracker.ietf.org/doc/html/rfc8999
+            // This packet just contains a long header with an unsupported version number, to force
+            // a version-negotiation packet in response.
+            val connectionId = ByteArray(8).apply { Random().nextBytes(this) }
+            val quicData = byteArrayOf(
+                // long header
+                0xc0.toByte(),
+                // version number (should be an unknown version for the server)
+                0xaa.toByte(), 0xda.toByte(), 0xca.toByte(), 0xca.toByte(),
+                // destination connection ID length
+                0x08,
+            ) + connectionId + byteArrayOf(
+                // source connection ID length
+                0x00,
+            ) + ByteArray(1185) // Ensure the packet is 1200 bytes long
+            val targetAddr = targetAddrs.firstOrNull { it.javaClass == bindAddr.javaClass }
+                ?: fail("No ${bindAddr.javaClass} found for $hostname " +
+                        "(got ${targetAddrs.joinToString()})")
+            repeat(QUIC_RETRY_COUNT) { i ->
+                socket.send(DatagramPacket(quicData, quicData.size, targetAddr, 443))
+
+                val receivedPacket = DatagramPacket(ByteArray(1500), 1500)
+                try {
+                    socket.receive(receivedPacket)
+                } catch (e: IOException) {
+                    Log.d(logTag, "No response from $hostname ($targetAddr) on QUIC try $i", e)
+                    return@repeat
+                }
+
+                val receivedConnectionId = receivedPacket.data.copyOfRange(7, 7 + 8)
+                if (connectionId.contentEquals(receivedConnectionId)) {
+                    return@tryTest
+                } else {
+                    val headerBytes = receivedPacket.data.copyOfRange(
+                        0, receivedPacket.length.coerceAtMost(15))
+                    Log.d(logTag, "Received invalid connection ID on QUIC try $i: " +
+                            HexDump.toHexString(headerBytes))
+                }
+            }
+            fail("QUIC is not working on SSID $ssid connecting to $targetAddr " +
+                    "with local source port ${socket.localPort}: check the firewall for UDP port " +
+                    "443 access."
+            )
+        } cleanup {
+            socket.close()
+        }
     }
 
     @Test
@@ -53,12 +193,16 @@
         if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
             fail("The device has no SIM card inserted. $commonError")
         } else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
-            fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
-                    commonError)
+            fail(
+                "The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+                    commonError
+            )
         }
-        assertTrue(tm.isDataConnectivityPossible,
+        assertTrue(
+            tm.isDataConnectivityPossible,
             "The device has a SIM card, but it does not supports data connectivity. " +
-            "Check the data plan, and verify that mobile data is working. " + commonError)
+            "Check the data plan, and verify that mobile data is working. " + commonError
+        )
         connectUtil.ensureCellularValidated()
     }
 }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 3857810..d60ab59 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -53,6 +53,10 @@
 private const val WIFI_ERROR_BUSY = 2
 
 class ConnectUtil(private val context: Context) {
+    companion object {
+        @JvmStatic
+        val VIRTUAL_SSIDS = listOf("VirtWifi", "AndroidWifi")
+    }
     private val TAG = ConnectUtil::class.java.simpleName
 
     private val cm = context.getSystemService(ConnectivityManager::class.java)
@@ -207,9 +211,8 @@
      */
     private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
         // Virtual wifi networks used on the emulator and cloud testing infrastructure
-        val virtualSsids = listOf("VirtWifi", "AndroidWifi")
         Log.d(TAG, "Wifi scan results: $scanResults")
-        val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+        val virtualScanResult = scanResults.firstOrNull { VIRTUAL_SSIDS.contains(it.SSID) }
                 ?: return null
 
         // Only add the virtual configuration if the virtual AP is detected in scans
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index 9e63910..e5b8471 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -114,7 +114,7 @@
     override fun onSetUp() {
         assertNull(instance, "ConnectivityDiagnosticsCollectors were set up multiple times")
         instance = this
-        TryTestConfig.setDiagnosticsCollector { throwable ->
+        TryTestConfig.swapDiagnosticsCollector { throwable ->
             if (runOnFailure(throwable)) {
                 collectTestFailureDiagnostics(throwable)
             }
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
index ce55fdc..31879af 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DeviceInfoUtils.java
@@ -16,6 +16,10 @@
 
 package com.android.testutils;
 
+import static com.android.modules.utils.build.SdkLevel.isAtLeastS;
+
+import android.os.Build;
+import android.os.SystemProperties;
 import android.os.VintfRuntimeInfo;
 import android.text.TextUtils;
 import android.util.Pair;
@@ -173,4 +177,14 @@
         final KVersion from = DeviceInfoUtils.getMajorMinorSubminorVersion(version);
         return current.isAtLeast(from);
     }
+
+    /**
+     * Check if the current build is a debuggable build.
+     */
+    public static boolean isDebuggable() {
+        if (isAtLeastS()) {
+            return Build.isDebuggable();
+        }
+        return SystemProperties.getInt("ro.debuggable", 0) == 1;
+    }
 }
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
index dcd422c..45c69c9 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -75,13 +75,21 @@
  */
 
 object TryTestConfig {
-    internal var diagnosticsCollector: Consumer<Throwable>? = null
+    private var diagnosticsCollector: Consumer<Throwable>? = null
 
     /**
      * Set the diagnostics collector to be used in case of failure in [tryTest].
+     *
+     * @return The previous collector.
      */
-    fun setDiagnosticsCollector(collector: Consumer<Throwable>) {
+    fun swapDiagnosticsCollector(collector: Consumer<Throwable>?): Consumer<Throwable>? {
+        val oldCollector = diagnosticsCollector
         diagnosticsCollector = collector
+        return oldCollector
+    }
+
+    fun reportError(e: Throwable) {
+        diagnosticsCollector?.accept(e)
     }
 }
 
@@ -90,14 +98,10 @@
         try {
             Result.success(block())
         } catch (e: Throwable) {
-            TryTestConfig.diagnosticsCollector?.accept(e)
             Result.failure(e)
-        })
+        }, skipErrorReporting = false)
 
-// Some downstream branches have an older kotlin that doesn't know about value classes.
-// TODO : Change this to "value class" when aosp no longer merges into such branches.
-@Suppress("INLINE_CLASS_DEPRECATED")
-inline class TryExpr<T>(val result: Result<T>) {
+class TryExpr<T>(val result: Result<T>, val skipErrorReporting: Boolean) {
     inline infix fun <reified E : Throwable> catch(block: (E) -> T): TryExpr<T> {
         val originalException = result.exceptionOrNull()
         if (originalException !is E) return this
@@ -105,23 +109,32 @@
             Result.success(block(originalException))
         } catch (e: Throwable) {
             Result.failure(e)
-        })
+        }, this.skipErrorReporting)
     }
 
     @CheckReturnValue
     inline infix fun cleanupStep(block: () -> Unit): TryExpr<T> {
+        // Report errors before the cleanup step, but after catch blocks that may suppress it
+        val originalException = result.exceptionOrNull()
+        var nextSkipErrorReporting = skipErrorReporting
+        if (!skipErrorReporting && originalException != null) {
+            TryTestConfig.reportError(originalException)
+            nextSkipErrorReporting = true
+        }
         try {
             block()
         } catch (e: Throwable) {
-            val originalException = result.exceptionOrNull()
-            return TryExpr(if (null == originalException) {
-                Result.failure(e)
+            return if (null == originalException) {
+                if (!skipErrorReporting) {
+                    TryTestConfig.reportError(e)
+                }
+                TryExpr(Result.failure(e), skipErrorReporting = true)
             } else {
                 originalException.addSuppressed(e)
-                Result.failure(originalException)
-            })
+                TryExpr(Result.failure(originalException), true)
+            }
         }
-        return this
+        return TryExpr(result, nextSkipErrorReporting)
     }
 
     inline infix fun cleanup(block: () -> Unit): T = cleanupStep(block).result.getOrThrow()
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
index d1d5649..176546a 100644
--- a/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/MiscAsserts.kt
@@ -20,6 +20,7 @@
 
 import com.android.testutils.FunctionalUtils.ThrowingRunnable
 import java.lang.reflect.Modifier
+import java.util.concurrent.TimeUnit
 import java.util.function.BooleanSupplier
 import kotlin.system.measureTimeMillis
 import kotlin.test.assertEquals
@@ -134,7 +135,7 @@
     // on host). When waiting for a condition during tests the device would generally not go into
     // deep sleep, and the polling sleep would go over the timeout anyway in that case, so this is
     // fine.
-    val limit = System.nanoTime() + timeoutMs * 1000
+    val limit = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs)
     while (!fn.asBoolean) {
         if (System.nanoTime() > limit) {
             fail(descr)
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 8e77b5d..320622b 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -21,6 +21,8 @@
 
 import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
 import android.content.pm.PackageManager.FEATURE_WIFI
 import android.net.ConnectivityManager
 import android.net.Network
@@ -50,6 +52,7 @@
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.PowerManager
+import android.os.UserManager
 import android.platform.test.annotations.AppModeFull
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
@@ -141,15 +144,34 @@
         fun turnScreenOff() {
             if (!wakeLock.isHeld()) wakeLock.acquire()
             runShellCommandOrThrow("input keyevent KEYCODE_SLEEP")
-            val result = pollingCheck({ !powerManager.isInteractive() }, timeout_ms = 2000)
-            assertThat(result).isTrue()
+            waitForInteractiveState(false)
         }
 
         fun turnScreenOn() {
             if (wakeLock.isHeld()) wakeLock.release()
             runShellCommandOrThrow("input keyevent KEYCODE_WAKEUP")
-            val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
-            assertThat(result).isTrue()
+            waitForInteractiveState(true)
+        }
+
+        private fun waitForInteractiveState(interactive: Boolean) {
+            // TODO(b/366037029): This test condition should be removed once
+            // PowerManager#isInteractive is fully implemented on automotive
+            // form factor with visible background user.
+            if (isAutomotiveWithVisibleBackgroundUser()) {
+                // Wait for 2 seconds to ensure the interactive state is updated.
+                // This is a workaround for b/366037029.
+                Thread.sleep(2000L)
+            } else {
+                val result = pollingCheck({ powerManager.isInteractive() }, timeout_ms = 2000)
+                assertThat(result).isEqualTo(interactive)
+            }
+        }
+
+        private fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
+            val packageManager = context.getPackageManager()
+            val userManager = context.getSystemService(UserManager::class.java)!!
+            return (packageManager.hasSystemFeature(FEATURE_AUTOMOTIVE)
+                    && userManager.isVisibleBackgroundUsersSupported)
         }
 
         @BeforeClass
@@ -157,9 +179,11 @@
         @Suppress("ktlint:standard:no-multi-spaces")
         fun setupOnce() {
             // TODO: assertions thrown in @BeforeClass / @AfterClass are not well supported in the
-            // test infrastructure. Consider saving excepion and throwing it in setUp().
+            // test infrastructure. Consider saving exception and throwing it in setUp().
+
             // APF must run when the screen is off and the device is not interactive.
             turnScreenOff()
+
             // Wait for APF to become active.
             Thread.sleep(1000)
             // TODO: check that there is no active wifi network. Otherwise, ApfFilter has already been
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 88309ed..feb4621 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -952,9 +952,8 @@
         final List<InetAddress> cellNetworkAddresses = cellLinkProperties.getAddresses();
         // In userdebug build, on cellular network, if the onNetwork check failed, we also try to
         // re-verify it by obtaining the IP address through DNS query.
-        boolean isUserDebug = Build.isDebuggable();
         if (cellAddress instanceof Inet6Address) {
-            if (isUserDebug && !cellNetworkAddresses.contains(cellAddress)) {
+            if (DeviceInfoUtils.isDebuggable() && !cellNetworkAddresses.contains(cellAddress)) {
                 final InetAddress ipv6AddressThroughDns = InetAddresses.parseNumericAddress(
                         getDeviceIpv6AddressThroughDnsQuery(cellNetwork));
                 assertContains(cellNetworkAddresses, ipv6AddressThroughDns);
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
index 10adee0..65daf57 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsBinderTest.java
@@ -114,7 +114,7 @@
             } catch (ClassNotFoundException e) {
                 /* not vulnerable if hidden API no longer available */
                 return;
-            } catch (NoSuchMethodException e) {
+            } catch (NoSuchMethodException | NoSuchMethodError e) {
                 /* not vulnerable if hidden API no longer available */
                 return;
             } catch (RemoteException e) {