Merge "Migrate reigster service callback backend"
diff --git a/Cronet/tests/common/Android.bp b/Cronet/tests/common/Android.bp
index f8bdb08..939a81c 100644
--- a/Cronet/tests/common/Android.bp
+++ b/Cronet/tests/common/Android.bp
@@ -27,7 +27,9 @@
 android_test {
     name: "NetHttpCoverageTests",
     defaults: ["CronetTestJavaDefaults"],
+    enforce_default_target_sdk_version: true,
     sdk_version: "test_current",
+    min_sdk_version: "30",
     test_suites: ["general-tests", "mts-tethering"],
     static_libs: [
         "modules-utils-native-coverage-listener",
diff --git a/Cronet/tests/common/AndroidManifest.xml b/Cronet/tests/common/AndroidManifest.xml
index efe880c..b00fc90 100644
--- a/Cronet/tests/common/AndroidManifest.xml
+++ b/Cronet/tests/common/AndroidManifest.xml
@@ -18,6 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           xmlns:tools="http://schemas.android.com/tools"
           package="com.android.net.http.tests.coverage">
+
     <!-- NetHttpCoverageTests combines CtsNetHttpTestCases and NetHttpTests targets,
      so permissions and others are declared in their respective manifests -->
     <application tools:replace="android:label"
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index ca298dd..2ac418f 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -20,8 +20,11 @@
     </target_preparer>
     <option name="test-tag" value="NetHttpCoverageTests" />
     <!-- Tethering/Connectivity is a SDK 30+ module -->
+    <!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
     <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+            class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
+    <option name="config-descriptor:metadata" key="mainline-param"
+            value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.net.http.tests.coverage" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 945b220..d969b54 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -29,6 +29,10 @@
 java_defaults {
     name: "CronetTestJavaDefaultsEnabled",
     enabled: true,
+    // TODO(danstahr): move to unconditional static_libs once the T branch is abandoned
+    static_libs: [
+        "truth",
+    ],
 }
 
 java_defaults {
@@ -43,7 +47,12 @@
 
 android_library {
     name: "CtsNetHttpTestsLib",
+    defaults: [
+        "cts_defaults",
+        "CronetTestJavaDefaults",
+    ],
     sdk_version: "test_current",
+    min_sdk_version: "30",
     srcs: [
         "src/**/*.java",
         "src/**/*.kt",
@@ -62,6 +71,7 @@
         "framework-tethering",
         "org.apache.http.legacy",
     ],
+    lint: { test: true }
 }
 
 android_test {
diff --git a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt b/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
index 13c220d..bead1f8 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt
@@ -63,7 +63,7 @@
     }
 
     private fun createBidirectionalStreamBuilder(url: String): BidirectionalStream.Builder {
-        return httpEngine.newBidirectionalStreamBuilder(url, callback, callback.executor)
+        return httpEngine.newBidirectionalStreamBuilder(url, callback.executor, callback)
     }
 
     @Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
index 45d27bf..34baedf 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -23,6 +23,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
@@ -49,12 +50,13 @@
     private TestUrlRequestCallback mCallback;
     private UrlRequest mRequest;
     private HttpEngine mEngine;
+    private Context mContext;
 
     @Before
     public void setUp() throws Exception {
-        Context context = ApplicationProvider.getApplicationContext();
-        skipIfNoInternetConnection(context);
-        mEngineBuilder = new HttpEngine.Builder(context);
+        mContext = ApplicationProvider.getApplicationContext();
+        skipIfNoInternetConnection(mContext);
+        mEngineBuilder = new HttpEngine.Builder(mContext);
         mCallback = new TestUrlRequestCallback();
     }
 
@@ -77,7 +79,7 @@
     public void testHttpEngine_Default() throws Exception {
         mEngine = mEngineBuilder.build();
         UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+                mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
         mRequest = builder.build();
         mRequest.start();
 
@@ -90,10 +92,42 @@
     }
 
     @Test
+    public void testHttpEngine_EnableHttpCache() {
+        // We need a server which sets cache-control != no-cache.
+        String url = "https://www.example.com";
+        mEngine =
+                mEngineBuilder
+                        .setStoragePath(mContext.getApplicationInfo().dataDir)
+                        .setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK,
+                                            /* maxSize */ 100 * 1024)
+                        .build();
+
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(url, mCallback, mCallback.getExecutor());
+        mRequest = builder.build();
+        mRequest.start();
+        // This tests uses a non-hermetic server. Instead of asserting, assume the next callback.
+        // This way, if the request were to fail, the test would just be skipped instead of failing.
+        mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED);
+        UrlResponseInfo info = mCallback.mResponseInfo;
+        assumeOKStatusCode(info);
+        assertFalse(info.wasCached());
+
+        mCallback = new TestUrlRequestCallback();
+        builder = mEngine.newUrlRequestBuilder(url, mCallback, mCallback.getExecutor());
+        mRequest = builder.build();
+        mRequest.start();
+        mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED);
+        info = mCallback.mResponseInfo;
+        assertOKStatusCode(info);
+        assertTrue(info.wasCached());
+    }
+
+    @Test
     public void testHttpEngine_DisableHttp2() throws Exception {
         mEngine = mEngineBuilder.setEnableHttp2(false).build();
         UrlRequest.Builder builder =
-                mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+                mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
         mRequest = builder.build();
         mRequest.start();
 
@@ -106,6 +140,36 @@
     }
 
     @Test
+    public void testHttpEngine_EnablePublicKeyPinningBypassForLocalTrustAnchors() {
+        // For known hosts, requests should succeed whether we're bypassing the local trust anchor
+        // or not.
+        mEngine = mEngineBuilder.setEnablePublicKeyPinningBypassForLocalTrustAnchors(false).build();
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+        mRequest = builder.build();
+        mRequest.start();
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+
+        mEngine.shutdown();
+        mEngine = mEngineBuilder.setEnablePublicKeyPinningBypassForLocalTrustAnchors(true).build();
+        mCallback = new TestUrlRequestCallback();
+        builder = mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+        mRequest = builder.build();
+        mRequest.start();
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+
+        // TODO(b/270918920): We should also test with a certificate not present in the device's
+        // trusted store.
+        // This requires either:
+        // * Mocking the underlying CertificateVerifier.
+        // * Or, having the server return a root certificate not present in the device's trusted
+        //   store.
+        // The former doesn't make sense for a CTS test as it would depend on the underlying
+        // implementation. The latter is something we should support once we write a proper test
+        // server.
+    }
+
+    @Test
     public void testHttpEngine_EnableQuic() throws Exception {
         mEngine = mEngineBuilder.setEnableQuic(true).addQuicHint(HOST, 443, 443).build();
         // The hint doesn't guarantee that QUIC will win the race, just that it will race TCP.
@@ -114,7 +178,7 @@
         for (int i = 0; i < 5; i++) {
             mCallback = new TestUrlRequestCallback();
             UrlRequest.Builder builder =
-                    mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+                    mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback);
             mRequest = builder.build();
             mRequest.start();
 
diff --git a/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
new file mode 100644
index 0000000..1888962
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.http.cts
+
+import android.net.http.QuicOptions
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class QuicOptionsTest {
+    @Test
+    fun testQuicOptions_defaultValues() {
+        val quicOptions = QuicOptions.Builder().build()
+        assertThat(quicOptions.quicHostAllowlist).isEmpty()
+        assertThat(quicOptions.handshakeUserAgent).isNull()
+        // TODO(danstahr): idleConnectionTimeout getter should be public
+        // assertThat(quicOptions.idleConnectionTimeout).isNull()
+        assertThat(quicOptions.inMemoryServerConfigsCacheSize).isNull()
+    }
+
+    @Test
+    fun testQuicOptions_quicHostAllowlist_returnsAddedValues() {
+        val quicOptions = QuicOptions.Builder()
+                .addAllowedQuicHost("foo")
+                .addAllowedQuicHost("bar")
+                .addAllowedQuicHost("foo")
+                .addAllowedQuicHost("baz")
+                .build()
+        assertThat(quicOptions.quicHostAllowlist)
+                .containsExactly("foo", "bar", "baz")
+                .inOrder()
+    }
+
+    // TODO(danstahr): idleConnectionTimeout getter should be public
+    /*
+    @Test
+    fun testQuicOptions_idleConnectionTimeout_returnsSetValue() {
+        val timeout = Duration.ofMinutes(10)
+        val quicOptions = QuicOptions.Builder()
+                .setIdleConnectionTimeout(timeout)
+                .build()
+        assertThat(quicOptions.idleConnectionTimeout)
+                .isEqualTo(timeout)
+    }
+    */
+
+    @Test
+    fun testQuicOptions_inMemoryServerConfigsCacheSize_returnsSetValue() {
+        val quicOptions = QuicOptions.Builder()
+                .setInMemoryServerConfigsCacheSize(42)
+                .build()
+        assertThat(quicOptions.inMemoryServerConfigsCacheSize)
+                .isEqualTo(42)
+    }
+}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
index 2bec9e6..735bdc6 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
@@ -70,7 +70,7 @@
     }
 
     private UrlRequest.Builder createUrlRequestBuilder(String url) {
-        return mHttpEngine.newUrlRequestBuilder(url, mCallback, mCallback.getExecutor());
+        return mHttpEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback);
     }
 
     @Test
@@ -113,8 +113,9 @@
         String testData = "test";
         UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getEchoBodyUrl());
 
-        TestUploadDataProvider dataProvider = new TestUploadDataProvider(
-                TestUploadDataProvider.SuccessCallbackMode.SYNC, mCallback.getExecutor());
+        TestUploadDataProvider dataProvider =
+                new TestUploadDataProvider(
+                        TestUploadDataProvider.SuccessCallbackMode.SYNC, mCallback.getExecutor());
         dataProvider.addRead(testData.getBytes());
         builder.setUploadDataProvider(dataProvider, mCallback.getExecutor());
         builder.addHeader("Content-Type", "text/html");
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt b/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
index e526c7d..3a4486f 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
@@ -24,7 +24,7 @@
 private const val TIMEOUT_MS = 12000L
 
 /** Test status listener for requests */
-class TestStatusListener : StatusListener() {
+class TestStatusListener : StatusListener {
     private val statusFuture = CompletableFuture<Int>()
 
     override fun onStatus(status: Int) {
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 1cabd63..03d163c 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -20,6 +20,8 @@
 android_library {
     name: "NetHttpTestsLibPreJarJar",
     srcs: [":cronet_aml_javatests_sources"],
+    sdk_version: "test_current",
+    min_sdk_version: "30",
     static_libs: [
         "androidx.test.ext.junit",
         "androidx.test.rules",
@@ -28,7 +30,8 @@
     libs: [
         "android.test.base",
         "framework-tethering-pre-jarjar",
-    ]
+    ],
+    lint: { test: true }
 }
 
 android_test {
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 8cb549e..0d780a1 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -16,8 +16,9 @@
   -->
 <configuration description="Runs NetHttp Mainline Tests.">
     <!-- Only run tests if the device under test is SDK version 30 or above. -->
+    <!-- TODO Switch back to Sdk30 when b/270049141 is fixed -->
     <object type="module_controller"
-            class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+            class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="NetHttpTests.apk" />
diff --git a/Cronet/tools/import/import_cronet.sh b/Cronet/tools/import/import_cronet.sh
index 7642914..eb82551 100755
--- a/Cronet/tools/import/import_cronet.sh
+++ b/Cronet/tools/import/import_cronet.sh
@@ -19,40 +19,70 @@
 #  Environment:
 #   ANDROID_BUILD_TOP: path the root of the current Android directory.
 #  Arguments:
-#   -l: The last revision that was imported.
-#   -n: The new revision to import.
+#   -l rev: The last revision that was imported.
+#  Optional Arguments:
+#   -n rev: The new revision to import.
+#   -f: Force copybara to ignore a failure to find the last imported revision.
 
-OPTSTRING=l:n:
+OPTSTRING=fl:n:
 
 usage() {
     cat <<EOF
-Usage: import_cronet.sh -l last-rev -n new-rev
+Usage: import_cronet.sh -n new-rev [-l last-rev] [-f]
 EOF
     exit 1
 }
 
 #######################################
+# Create upstream-import branch in external/cronet.
+# Globals:
+#   ANDROID_BUILD_TOP
+# Arguments:
+#   none
+#######################################
+setup_upstream_import_branch() {
+    local git_dir="${ANDROID_BUILD_TOP}/external/cronet"
+    local initial_empty_repo_sha="d1add53d6e90815f363c91d433735556ce79b0d2"
+
+    # Suppress error message if branch already exists.
+    (cd "${git_dir}" && git branch upstream-import "${initial_empty_repo_sha}") 2>/dev/null
+}
+
+#######################################
 # Runs the copybara import of Chromium
 # Globals:
 #   ANDROID_BUILD_TOP
 # Arguments:
-#   last_rev, string
 #   new_rev, string
+#   last_rev, string or empty
+#   force, string or empty
 #######################################
 do_run_copybara() {
-    local _last_rev=$1
-    local _new_rev=$2
+    local _new_rev=$1
+    local _last_rev=$2
+    local _force=$3
+
+    local -a flags
+    flags+=(--git-destination-url="file://${ANDROID_BUILD_TOP}/external/cronet")
+    flags+=(--repo-timeout 3h)
+
+    if [ ! -z "${_force}" ]; then
+        flags+=(--force)
+    fi
+
+    if [ ! -z "${_last_rev}" ]; then
+        flags+=(--last-rev "${_last_rev}")
+    fi
 
     /google/bin/releases/copybara/public/copybara/copybara \
-        --git-destination-url="file://${ANDROID_BUILD_TOP}/external/cronet" \
-        --last-rev "${_last_rev}" \
-        --repo-timeout 3h \
+        "${flags[@]}" \
         "${ANDROID_BUILD_TOP}/packages/modules/Connectivity/Cronet/tools/import/copy.bara.sky" \
         import_cronet "${_new_rev}"
 }
 
 while getopts $OPTSTRING opt; do
     case "${opt}" in
+        f) force=true ;;
         l) last_rev="${OPTARG}" ;;
         n) new_rev="${OPTARG}" ;;
         ?) usage ;;
@@ -60,17 +90,11 @@
     esac
 done
 
-# TODO: Get last-rev from METADATA file.
-# Setting last-rev may only be required for the first commit.
-if [ -z "${last_rev}" ]; then
-    echo "-l argument required"
-    usage
-fi
-
 if [ -z "${new_rev}" ]; then
     echo "-n argument required"
     usage
 fi
 
-do_run_copybara "${last_rev}" "${new_rev}"
+setup_upstream_import_branch
+do_run_copybara "${new_rev}" "${last_rev}" "${force}"
 
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4d3ecdf..70c5f85 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -213,6 +213,9 @@
     },
     {
       "name": "libnetworkstats_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
+    },
+    {
+      "name": "NetHttpCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
     }
   ],
   "mainline-postsubmit": [
diff --git a/Tethering/common/TetheringLib/cronet_enabled/api/current.txt b/Tethering/common/TetheringLib/cronet_enabled/api/current.txt
index 777138d..e954074 100644
--- a/Tethering/common/TetheringLib/cronet_enabled/api/current.txt
+++ b/Tethering/common/TetheringLib/cronet_enabled/api/current.txt
@@ -49,7 +49,7 @@
     method @Nullable public Boolean getEnablePathDegradationMigration();
   }
 
-  public static class ConnectionMigrationOptions.Builder {
+  public static final class ConnectionMigrationOptions.Builder {
     ctor public ConnectionMigrationOptions.Builder();
     method public android.net.http.ConnectionMigrationOptions build();
     method public android.net.http.ConnectionMigrationOptions.Builder setAllowNonDefaultNetworkUsage(boolean);
@@ -95,10 +95,11 @@
 
   public abstract class HttpEngine {
     method public void bindToNetwork(@Nullable android.net.Network);
-    method public abstract java.net.URLStreamHandlerFactory createURLStreamHandlerFactory();
+    method public abstract java.net.URLStreamHandlerFactory createUrlStreamHandlerFactory();
     method public static String getVersionString();
-    method public abstract android.net.http.BidirectionalStream.Builder newBidirectionalStreamBuilder(String, android.net.http.BidirectionalStream.Callback, java.util.concurrent.Executor);
-    method public abstract android.net.http.UrlRequest.Builder newUrlRequestBuilder(String, android.net.http.UrlRequest.Callback, java.util.concurrent.Executor);
+    method public abstract android.net.http.BidirectionalStream.Builder newBidirectionalStreamBuilder(String, java.util.concurrent.Executor, android.net.http.BidirectionalStream.Callback);
+    method public abstract android.net.http.UrlRequest.Builder newUrlRequestBuilder(String, java.util.concurrent.Executor, android.net.http.UrlRequest.Callback);
+    method public android.net.http.UrlRequest.Builder newUrlRequestBuilder(String, android.net.http.UrlRequest.Callback, java.util.concurrent.Executor);
     method public abstract java.net.URLConnection openConnection(java.net.URL) throws java.io.IOException;
     method public abstract void shutdown();
   }
@@ -126,7 +127,7 @@
   }
 
   public class HttpException extends java.io.IOException {
-    ctor public HttpException(String, Throwable);
+    ctor public HttpException(@Nullable String, @Nullable Throwable);
   }
 
   public final class InlineExecutionProhibitedException extends java.util.concurrent.RejectedExecutionException {
@@ -134,7 +135,7 @@
   }
 
   public abstract class NetworkException extends android.net.http.HttpException {
-    ctor public NetworkException(String, Throwable);
+    ctor public NetworkException(@Nullable String, @Nullable Throwable);
     method public abstract int getErrorCode();
     method public abstract boolean isImmediatelyRetryable();
     field public static final int ERROR_ADDRESS_UNREACHABLE = 9; // 0x9
@@ -151,60 +152,60 @@
   }
 
   public abstract class QuicException extends android.net.http.NetworkException {
-    ctor protected QuicException(String, Throwable);
+    ctor protected QuicException(@Nullable String, @Nullable Throwable);
   }
 
   public class QuicOptions {
     method @Nullable public String getHandshakeUserAgent();
     method @Nullable public Integer getInMemoryServerConfigsCacheSize();
-    method public java.util.Set<java.lang.String> getQuicHostAllowlist();
+    method @NonNull public java.util.Set<java.lang.String> getQuicHostAllowlist();
   }
 
-  public static class QuicOptions.Builder {
+  public static final class QuicOptions.Builder {
     ctor public QuicOptions.Builder();
-    method public android.net.http.QuicOptions.Builder addAllowedQuicHost(String);
-    method public android.net.http.QuicOptions build();
-    method public android.net.http.QuicOptions.Builder setHandshakeUserAgent(String);
+    method @NonNull public android.net.http.QuicOptions.Builder addAllowedQuicHost(@NonNull String);
+    method @NonNull public android.net.http.QuicOptions build();
+    method @NonNull public android.net.http.QuicOptions.Builder setHandshakeUserAgent(@NonNull String);
     method public android.net.http.QuicOptions.Builder setIdleConnectionTimeout(java.time.Duration);
-    method public android.net.http.QuicOptions.Builder setInMemoryServerConfigsCacheSize(int);
+    method @NonNull public android.net.http.QuicOptions.Builder setInMemoryServerConfigsCacheSize(int);
   }
 
   public abstract class UploadDataProvider implements java.io.Closeable {
     ctor public UploadDataProvider();
     method public void close() throws java.io.IOException;
     method public abstract long getLength() throws java.io.IOException;
-    method public abstract void read(android.net.http.UploadDataSink, java.nio.ByteBuffer) throws java.io.IOException;
-    method public abstract void rewind(android.net.http.UploadDataSink) throws java.io.IOException;
+    method public abstract void read(@NonNull android.net.http.UploadDataSink, @NonNull java.nio.ByteBuffer) throws java.io.IOException;
+    method public abstract void rewind(@NonNull android.net.http.UploadDataSink) throws java.io.IOException;
   }
 
   public abstract class UploadDataSink {
     ctor public UploadDataSink();
-    method public abstract void onReadError(Exception);
+    method public abstract void onReadError(@NonNull Exception);
     method public abstract void onReadSucceeded(boolean);
-    method public abstract void onRewindError(Exception);
+    method public abstract void onRewindError(@NonNull Exception);
     method public abstract void onRewindSucceeded();
   }
 
   public abstract class UrlRequest {
     method public abstract void cancel();
     method public abstract void followRedirect();
-    method public abstract void getStatus(android.net.http.UrlRequest.StatusListener);
+    method public abstract void getStatus(@NonNull android.net.http.UrlRequest.StatusListener);
     method public abstract boolean isDone();
-    method public abstract void read(java.nio.ByteBuffer);
+    method public abstract void read(@NonNull java.nio.ByteBuffer);
     method public abstract void start();
   }
 
   public abstract static class UrlRequest.Builder {
-    method public abstract android.net.http.UrlRequest.Builder addHeader(String, String);
-    method public abstract android.net.http.UrlRequest.Builder allowDirectExecutor();
-    method public abstract android.net.http.UrlRequest.Builder bindToNetwork(@Nullable android.net.Network);
-    method public abstract android.net.http.UrlRequest build();
-    method public abstract android.net.http.UrlRequest.Builder disableCache();
-    method public abstract android.net.http.UrlRequest.Builder setHttpMethod(String);
-    method public abstract android.net.http.UrlRequest.Builder setPriority(int);
+    method @NonNull public abstract android.net.http.UrlRequest.Builder addHeader(@NonNull String, @NonNull String);
+    method @NonNull public abstract android.net.http.UrlRequest.Builder bindToNetwork(@Nullable android.net.Network);
+    method @NonNull public abstract android.net.http.UrlRequest build();
+    method @NonNull public abstract android.net.http.UrlRequest.Builder setAllowDirectExecutor(boolean);
+    method @NonNull public abstract android.net.http.UrlRequest.Builder setDisableCache(boolean);
+    method @NonNull public abstract android.net.http.UrlRequest.Builder setHttpMethod(@NonNull String);
+    method @NonNull public abstract android.net.http.UrlRequest.Builder setPriority(int);
     method public abstract android.net.http.UrlRequest.Builder setTrafficStatsTag(int);
     method public abstract android.net.http.UrlRequest.Builder setTrafficStatsUid(int);
-    method public abstract android.net.http.UrlRequest.Builder setUploadDataProvider(android.net.http.UploadDataProvider, java.util.concurrent.Executor);
+    method @NonNull public abstract android.net.http.UrlRequest.Builder setUploadDataProvider(@NonNull android.net.http.UploadDataProvider, @NonNull java.util.concurrent.Executor);
     field public static final int REQUEST_PRIORITY_HIGHEST = 4; // 0x4
     field public static final int REQUEST_PRIORITY_IDLE = 0; // 0x0
     field public static final int REQUEST_PRIORITY_LOW = 2; // 0x2
@@ -214,12 +215,12 @@
 
   public abstract static class UrlRequest.Callback {
     ctor public UrlRequest.Callback();
-    method public void onCanceled(android.net.http.UrlRequest, android.net.http.UrlResponseInfo);
-    method public abstract void onFailed(android.net.http.UrlRequest, android.net.http.UrlResponseInfo, android.net.http.HttpException);
-    method public abstract void onReadCompleted(android.net.http.UrlRequest, android.net.http.UrlResponseInfo, java.nio.ByteBuffer) throws java.lang.Exception;
-    method public abstract void onRedirectReceived(android.net.http.UrlRequest, android.net.http.UrlResponseInfo, String) throws java.lang.Exception;
-    method public abstract void onResponseStarted(android.net.http.UrlRequest, android.net.http.UrlResponseInfo) throws java.lang.Exception;
-    method public abstract void onSucceeded(android.net.http.UrlRequest, android.net.http.UrlResponseInfo);
+    method public void onCanceled(@NonNull android.net.http.UrlRequest, @Nullable android.net.http.UrlResponseInfo);
+    method public abstract void onFailed(@NonNull android.net.http.UrlRequest, @Nullable android.net.http.UrlResponseInfo, @NonNull android.net.http.HttpException);
+    method public abstract void onReadCompleted(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo, @NonNull java.nio.ByteBuffer) throws java.lang.Exception;
+    method public abstract void onRedirectReceived(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo, @NonNull String) throws java.lang.Exception;
+    method public abstract void onResponseStarted(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo) throws java.lang.Exception;
+    method public abstract void onSucceeded(@NonNull android.net.http.UrlRequest, @NonNull android.net.http.UrlResponseInfo);
   }
 
   public static class UrlRequest.Status {
@@ -241,21 +242,19 @@
     field public static final int WAITING_FOR_STALLED_SOCKET_POOL = 1; // 0x1
   }
 
-  public abstract static class UrlRequest.StatusListener {
-    ctor public UrlRequest.StatusListener();
-    method public abstract void onStatus(int);
+  public static interface UrlRequest.StatusListener {
+    method public void onStatus(int);
   }
 
   public abstract class UrlResponseInfo {
     ctor public UrlResponseInfo();
-    method public abstract android.net.http.UrlResponseInfo.HeaderBlock getHeaders();
+    method @NonNull public abstract android.net.http.UrlResponseInfo.HeaderBlock getHeaders();
     method public abstract int getHttpStatusCode();
-    method public abstract String getHttpStatusText();
-    method public abstract String getNegotiatedProtocol();
-    method public abstract String getProxyServer();
+    method @NonNull public abstract String getHttpStatusText();
+    method @NonNull public abstract String getNegotiatedProtocol();
     method public abstract long getReceivedByteCount();
-    method public abstract String getUrl();
-    method public abstract java.util.List<java.lang.String> getUrlChain();
+    method @NonNull public abstract String getUrl();
+    method @NonNull public abstract java.util.List<java.lang.String> getUrlChain();
     method public abstract boolean wasCached();
   }
 
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index 7ef20c5..1a8d46b 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -156,7 +156,7 @@
         "//frameworks/opt/telephony/tests/telephonytests",
         "//packages/modules/CaptivePortalLogin/tests",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
-        "//packages/modules/Connectivity/nearby/tests:__subpackages__",
+        "//packages/modules/Connectivity/nearby:__subpackages__",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/IPsec/tests/iketests",
         "//packages/modules/NetworkStack/tests:__subpackages__",
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 7cef58b..381a18a 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -2521,7 +2521,7 @@
     @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
     public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network,
             @NonNull Socket socket,
-            @NonNull Executor executor,
+            @NonNull @CallbackExecutor Executor executor,
             @NonNull Callback callback) {
         ParcelFileDescriptor dup;
         try {
@@ -5494,9 +5494,9 @@
      * @return {@code uid} if the connection is found and the app has permission to observe it
      *     (e.g., if it is associated with the calling VPN app's VpnService tunnel) or {@link
      *     android.os.Process#INVALID_UID} if the connection is not found.
-     * @throws {@link SecurityException} if the caller is not the active VpnService for the current
+     * @throws SecurityException if the caller is not the active VpnService for the current
      *     user.
-     * @throws {@link IllegalArgumentException} if an unsupported protocol is requested.
+     * @throws IllegalArgumentException if an unsupported protocol is requested.
      */
     public int getConnectionOwnerUid(
             int protocol, @NonNull InetSocketAddress local, @NonNull InetSocketAddress remote) {
diff --git a/nearby/halfsheet/Android.bp b/nearby/halfsheet/Android.bp
index 2d0d327..8011dc6 100644
--- a/nearby/halfsheet/Android.bp
+++ b/nearby/halfsheet/Android.bp
@@ -27,7 +27,7 @@
     certificate: ":com.android.nearby.halfsheetcertificate",
     libs: [
         "framework-bluetooth",
-        "framework-connectivity-t",
+        "framework-connectivity-t.impl",
         "nearby-service-string",
     ],
     static_libs: [
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
index aeadb4a..be4ffe3 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -55,13 +55,12 @@
   NetworkTraceHandler::RegisterDataSource();
 }
 
-NetworkTraceHandler::NetworkTraceHandler()
-    : NetworkTraceHandler([this](const PacketTrace& pkt) {
-        NetworkTraceHandler::Trace(
-            [this, pkt](NetworkTraceHandler::TraceContext ctx) {
-              Fill(pkt, *ctx.NewTracePacket());
-            });
-      }) {}
+// static
+NetworkTracePoller NetworkTraceHandler::sPoller([](const PacketTrace& pkt) {
+  NetworkTraceHandler::Trace([pkt](NetworkTraceHandler::TraceContext ctx) {
+    NetworkTraceHandler::Fill(pkt, *ctx.NewTracePacket());
+  });
+});
 
 void NetworkTraceHandler::OnSetup(const SetupArgs& args) {
   const std::string& raw = args.config->network_packet_trace_config_raw();
@@ -75,21 +74,27 @@
 }
 
 void NetworkTraceHandler::OnStart(const StartArgs&) {
-  if (!Start()) return;
-  mTaskRunner = perfetto::Platform::GetDefaultPlatform()->CreateTaskRunner({});
-  Loop();
+  mStarted = sPoller.Start(mPollMs);
 }
 
 void NetworkTraceHandler::OnStop(const StopArgs&) {
-  Stop();
-  mTaskRunner.reset();
+  if (mStarted) sPoller.Stop();
+  mStarted = false;
 }
 
-void NetworkTraceHandler::Loop() {
-  mTaskRunner->PostDelayedTask([this]() { Loop(); }, mPollMs);
-  ConsumeAll();
+void NetworkTracePoller::SchedulePolling() {
+  // Schedules another run of ourselves to recursively poll periodically.
+  mTaskRunner->PostDelayedTask(
+      [this]() {
+        mMutex.lock();
+        SchedulePolling();
+        ConsumeAllLocked();
+        mMutex.unlock();
+      },
+      mPollMs);
 }
 
+// static class method
 void NetworkTraceHandler::Fill(const PacketTrace& src, TracePacket& dst) {
   dst.set_timestamp(src.timestampNs);
   auto* event = dst.set_network_packet();
@@ -113,9 +118,23 @@
   }
 }
 
-bool NetworkTraceHandler::Start() {
+bool NetworkTracePoller::Start(uint32_t pollMs) {
   ALOGD("Starting datasource");
 
+  std::scoped_lock<std::mutex> lock(mMutex);
+  if (mSessionCount > 0) {
+    if (mPollMs != pollMs) {
+      // Nothing technical prevents mPollMs from changing, it's just unclear
+      // what the right behavior is. Taking the min of active values could poll
+      // too frequently giving some sessions too much data. Taking the max could
+      // be too infrequent. For now, do nothing.
+      ALOGI("poll_ms can't be changed while running, ignoring poll_ms=%d",
+            pollMs);
+    }
+    mSessionCount++;
+    return true;
+  }
+
   auto status = mConfigurationMap.init(PACKET_TRACE_ENABLED_MAP_PATH);
   if (!status.ok()) {
     ALOGW("Failed to bind config map: %s", status.error().message().c_str());
@@ -136,24 +155,41 @@
     return false;
   }
 
+  // Start a task runner to run ConsumeAll every mPollMs milliseconds.
+  mTaskRunner = perfetto::Platform::GetDefaultPlatform()->CreateTaskRunner({});
+  mPollMs = pollMs;
+  SchedulePolling();
+
+  mSessionCount++;
   return true;
 }
 
-bool NetworkTraceHandler::Stop() {
+bool NetworkTracePoller::Stop() {
   ALOGD("Stopping datasource");
 
+  std::scoped_lock<std::mutex> lock(mMutex);
+  if (mSessionCount == 0) return false;  // This should never happen
+
+  // If this isn't the last session, don't clean up yet.
+  if (--mSessionCount > 0) return true;
+
   auto res = mConfigurationMap.writeValue(0, false, BPF_ANY);
   if (!res.ok()) {
     ALOGW("Failed to disable tracing: %s", res.error().message().c_str());
-    return false;
   }
 
+  mTaskRunner.reset();
   mRingBuffer.reset();
 
-  return true;
+  return res.ok();
 }
 
-bool NetworkTraceHandler::ConsumeAll() {
+bool NetworkTracePoller::ConsumeAll() {
+  std::scoped_lock<std::mutex> lock(mMutex);
+  return ConsumeAllLocked();
+}
+
+bool NetworkTracePoller::ConsumeAllLocked() {
   if (mRingBuffer == nullptr) {
     ALOGW("Tracing is not active");
     return false;
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
index 560194f..543be21 100644
--- a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
@@ -39,6 +39,9 @@
 
 namespace android {
 namespace bpf {
+// Use uint32 max to cause the handler to never Loop. Instead, the tests will
+// manually drive things by calling ConsumeAll explicitly.
+constexpr uint32_t kNeverPoll = std::numeric_limits<uint32_t>::max();
 
 __be16 bindAndListen(int s) {
   sockaddr_in sin = {.sin_family = AF_INET};
@@ -83,7 +86,7 @@
   }
 };
 
-class NetworkTraceHandlerTest : public testing::Test {
+class NetworkTracePollerTest : public testing::Test {
  protected:
   void SetUp() {
     if (access(PACKET_TRACE_RINGBUF_PATH, R_OK)) {
@@ -95,31 +98,49 @@
   }
 };
 
-TEST_F(NetworkTraceHandlerTest, PollWhileInactive) {
-  NetworkTraceHandler handler([&](const PacketTrace& pkt) {});
+TEST_F(NetworkTracePollerTest, PollWhileInactive) {
+  NetworkTracePoller handler([&](const PacketTrace& pkt) {});
 
   // One succeed after start and before stop.
   EXPECT_FALSE(handler.ConsumeAll());
-  ASSERT_TRUE(handler.Start());
+  ASSERT_TRUE(handler.Start(kNeverPoll));
   EXPECT_TRUE(handler.ConsumeAll());
   ASSERT_TRUE(handler.Stop());
   EXPECT_FALSE(handler.ConsumeAll());
 }
 
-TEST_F(NetworkTraceHandlerTest, TraceTcpSession) {
+TEST_F(NetworkTracePollerTest, ConcurrentSessions) {
+  // Simulate two concurrent sessions (two starts followed by two stops). Check
+  // that tracing is stopped only after both sessions finish.
+  NetworkTracePoller handler([&](const PacketTrace& pkt) {});
+
+  ASSERT_TRUE(handler.Start(kNeverPoll));
+  EXPECT_TRUE(handler.ConsumeAll());
+
+  ASSERT_TRUE(handler.Start(kNeverPoll));
+  EXPECT_TRUE(handler.ConsumeAll());
+
+  ASSERT_TRUE(handler.Stop());
+  EXPECT_TRUE(handler.ConsumeAll());
+
+  ASSERT_TRUE(handler.Stop());
+  EXPECT_FALSE(handler.ConsumeAll());
+}
+
+TEST_F(NetworkTracePollerTest, TraceTcpSession) {
   __be16 server_port = 0;
   std::vector<PacketTrace> packets;
 
   // Record all packets with the bound address and current uid. This callback is
   // involked only within ConsumeAll, at which point the port should have
   // already been filled in and all packets have been processed.
-  NetworkTraceHandler handler([&](const PacketTrace& pkt) {
+  NetworkTracePoller handler([&](const PacketTrace& pkt) {
     if (pkt.sport != server_port && pkt.dport != server_port) return;
     if (pkt.uid != getuid()) return;
     packets.push_back(pkt);
   });
 
-  ASSERT_TRUE(handler.Start());
+  ASSERT_TRUE(handler.Start(kNeverPoll));
   const uint32_t kClientTag = 2468;
   const uint32_t kServerTag = 1357;
 
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
index c257aa0..3f244b3 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -22,6 +22,7 @@
 #include <string>
 #include <unordered_map>
 
+#include "android-base/thread_annotations.h"
 #include "bpf/BpfMap.h"
 #include "bpf/BpfRingbuf.h"
 
@@ -31,6 +32,56 @@
 namespace android {
 namespace bpf {
 
+// NetworkTracePoller is responsible for interactions with the BPF ring buffer
+// including polling. This class is an internal helper for NetworkTraceHandler,
+// it is not meant to be used elsewhere.
+class NetworkTracePoller {
+ public:
+  // Testonly: initialize with a callback capable of intercepting data.
+  NetworkTracePoller(std::function<void(const PacketTrace&)> callback)
+      : mCallback(std::move(callback)) {}
+
+  // Starts tracing with the given poll interval.
+  bool Start(uint32_t pollMs) EXCLUDES(mMutex);
+
+  // Stops tracing and release any held state.
+  bool Stop() EXCLUDES(mMutex);
+
+  // Consumes all available events from the ringbuffer.
+  bool ConsumeAll() EXCLUDES(mMutex);
+
+ private:
+  void SchedulePolling() REQUIRES(mMutex);
+  bool ConsumeAllLocked() REQUIRES(mMutex);
+
+  std::mutex mMutex;
+
+  // Records the number of successfully started active sessions so that only the
+  // first active session attempts setup and only the last cleans up. Note that
+  // the session count will remain zero if Start fails. It is expected that Stop
+  // will not be called for any trace session where Start fails.
+  int mSessionCount GUARDED_BY(mMutex);
+
+  // How often to poll the ring buffer, defined by the trace config.
+  uint32_t mPollMs GUARDED_BY(mMutex);
+
+  // The function to process PacketTrace, typically a Perfetto sink.
+  std::function<void(const PacketTrace&)> mCallback GUARDED_BY(mMutex);
+
+  // The BPF ring buffer handle.
+  std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer GUARDED_BY(mMutex);
+
+  // The packet tracing config map (really a 1-element array).
+  BpfMap<uint32_t, bool> mConfigurationMap GUARDED_BY(mMutex);
+
+  // This must be the last member, causing it to be the first deleted. If it is
+  // not, members required for callbacks can be deleted before it's stopped.
+  std::unique_ptr<perfetto::base::TaskRunner> mTaskRunner GUARDED_BY(mMutex);
+};
+
+// NetworkTraceHandler implements the android.network_packets data source. This
+// class is registered with Perfetto and is instantiated when tracing starts and
+// destroyed when tracing ends. There is one instance per trace session.
 class NetworkTraceHandler : public perfetto::DataSource<NetworkTraceHandler> {
  public:
   // Registers this DataSource.
@@ -39,45 +90,19 @@
   // Connects to the system Perfetto daemon and registers the trace handler.
   static void InitPerfettoTracing();
 
-  // Initialize with the default Perfetto callback.
-  NetworkTraceHandler();
-
-  // Testonly: initialize with a callback capable of intercepting data.
-  NetworkTraceHandler(std::function<void(const PacketTrace&)> callback)
-      : mCallback(std::move(callback)) {}
-
-  // Testonly: standalone functions without perfetto dependency.
-  bool Start();
-  bool Stop();
-  bool ConsumeAll();
-
   // perfetto::DataSource overrides:
-  void OnSetup(const SetupArgs&) override;
+  void OnSetup(const SetupArgs& args) override;
   void OnStart(const StartArgs&) override;
   void OnStop(const StopArgs&) override;
 
-  // Convert a PacketTrace into a Perfetto trace packet.
-  void Fill(const PacketTrace& src,
-            ::perfetto::protos::pbzero::TracePacket& dst);
-
  private:
-  void Loop();
+  // Convert a PacketTrace into a Perfetto trace packet.
+  static void Fill(const PacketTrace& src,
+                   ::perfetto::protos::pbzero::TracePacket& dst);
 
-  // How often to poll the ring buffer, defined by the trace config.
+  static NetworkTracePoller sPoller;
   uint32_t mPollMs;
-
-  // The function to process PacketTrace, typically a Perfetto sink.
-  std::function<void(const PacketTrace&)> mCallback;
-
-  // The BPF ring buffer handle.
-  std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer;
-
-  // The packet tracing config map (really a 1-element array).
-  BpfMap<uint32_t, bool> mConfigurationMap;
-
-  // This must be the last member, causing it to be the first deleted. If it is
-  // not, members required for callbacks can be deleted before it's stopped.
-  std::unique_ptr<perfetto::base::TaskRunner> mTaskRunner;
+  bool mStarted;
 };
 
 }  // namespace bpf
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index f426062..8b70a94 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -1152,17 +1152,27 @@
                                 Log.e(TAG, "Invalid attribute", e);
                             }
                         }
-                        try {
-                            if (serviceInfo.getIpv4Address() != null) {
-                                info.setHost(InetAddresses.parseNumericAddress(
-                                        serviceInfo.getIpv4Address()));
-                            } else {
-                                info.setHost(InetAddresses.parseNumericAddress(
-                                        serviceInfo.getIpv6Address()));
+                        final List<InetAddress> addresses = new ArrayList<>();
+                        for (String ipv4Address : serviceInfo.getIpv4Addresses()) {
+                            try {
+                                addresses.add(InetAddresses.parseNumericAddress(ipv4Address));
+                            } catch (IllegalArgumentException e) {
+                                Log.wtf(TAG, "Invalid ipv4 address", e);
                             }
+                        }
+                        for (String ipv6Address : serviceInfo.getIpv6Addresses()) {
+                            try {
+                                addresses.add(InetAddresses.parseNumericAddress(ipv6Address));
+                            } catch (IllegalArgumentException e) {
+                                Log.wtf(TAG, "Invalid ipv6 address", e);
+                            }
+                        }
+
+                        if (addresses.size() != 0) {
+                            info.setHostAddresses(addresses);
                             clientInfo.onResolveServiceSucceeded(clientId, info);
-                        } catch (IllegalArgumentException e) {
-                            Log.wtf(TAG, "Invalid address in RESOLVE_SERVICE_SUCCEEDED", e);
+                        } else {
+                            // No address. Notify resolution failure.
                             clientInfo.onResolveServiceFailed(
                                     clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 606d3f8..78df6df 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -31,10 +31,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.TreeMap;
 
 /**
  * A class representing a discovered mDNS service instance.
@@ -205,17 +205,14 @@
         // compatibility. We should prefer only {@code textEntries} if it's not null.
         List<TextEntry> entries =
                 (this.textEntries != null) ? this.textEntries : parseTextStrings(this.textStrings);
-        Map<String, byte[]> attributes = new HashMap<>(entries.size());
+        // The map of attributes is case-insensitive.
+        final Map<String, byte[]> attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
         for (TextEntry entry : entries) {
-            String key = entry.getKey().toLowerCase(Locale.ENGLISH);
-
             // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4, only the first entry
             // of the same key should be accepted:
             // If a client receives a TXT record containing the same key more than once, then the
             // client MUST silently ignore all but the first occurrence of that attribute.
-            if (!attributes.containsKey(key)) {
-                attributes.put(key, entry.getValue());
-            }
+            attributes.putIfAbsent(entry.getKey(), entry.getValue());
         }
         this.attributes = Collections.unmodifiableMap(attributes);
         this.interfaceIndex = interfaceIndex;
@@ -336,12 +333,12 @@
      */
     @Nullable
     public byte[] getAttributeAsBytes(@NonNull String key) {
-        return attributes.get(key.toLowerCase(Locale.ENGLISH));
+        return attributes.get(key);
     }
 
     /** Returns an immutable map of all attributes. */
     public Map<String, String> getAttributes() {
-        Map<String, String> map = new HashMap<>(attributes.size());
+        Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
         for (Map.Entry<String, byte[]> kv : attributes.entrySet()) {
             final byte[] value = kv.getValue();
             map.put(kv.getKey(), value == null ? null : new String(value, UTF_8));
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6ba0fda..805dd65 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -60,7 +60,11 @@
 
 import com.android.internal.util.HexDump;
 import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl;
+import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl;
 import com.android.networkstack.apishim.VpnManagerShimImpl;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
+import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
 import com.android.networkstack.apishim.common.VpnManagerShim;
 import com.android.networkstack.apishim.common.VpnProfileStateShim;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -223,17 +227,28 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileCommon(
-            @NonNull Ikev2VpnProfile.Builder builder, boolean isRestrictedToTestNetworks,
-            boolean requiresValidation) throws Exception {
+            @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks,
+            boolean requiresValidation, boolean automaticIpVersionSelectionEnabled,
+            boolean automaticNattKeepaliveTimerEnabled) throws Exception {
 
-        builder.setBypassable(true)
+        builderShim.setBypassable(true)
                 .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS)
                 .setProxy(TEST_PROXY_INFO)
                 .setMaxMtu(TEST_MTU)
                 .setMetered(false);
         if (TestUtils.shouldTestTApis()) {
-            builder.setRequiresInternetValidation(requiresValidation);
+            builderShim.setRequiresInternetValidation(requiresValidation);
         }
+
+        if (TestUtils.shouldTestUApis()) {
+            builderShim.setAutomaticIpVersionSelectionEnabled(automaticIpVersionSelectionEnabled);
+            builderShim.setAutomaticNattKeepaliveTimerEnabled(automaticNattKeepaliveTimerEnabled);
+        }
+
+        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
+        // method and is not defined in shims.
+        // TODO: replace it in alternative way to remove the hidden method usage
+        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -249,13 +264,16 @@
                         ? IkeSessionTestUtils.IKE_PARAMS_V6 : IkeSessionTestUtils.IKE_PARAMS_V4,
                         IkeSessionTestUtils.CHILD_PARAMS);
 
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(params)
+        final Ikev2VpnProfileBuilderShim builderShim =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(params)
                         .setRequiresInternetValidation(requiresValidation)
                         .setProxy(TEST_PROXY_INFO)
                         .setMaxMtu(TEST_MTU)
                         .setMetered(false);
-
+        // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden
+        // method and is not defined in shims.
+        // TODO: replace it in alternative way to remove the hidden method usage
+        final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder();
         if (isRestrictedToTestNetworks) {
             builder.restrictToTestNetworks();
         }
@@ -263,31 +281,35 @@
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote,
-            boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK);
+            boolean isRestrictedToTestNetworks, boolean requiresValidation)
+            throws Exception {
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY)
+                        .setAuthPsk(TEST_PSK);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
-                requiresValidation);
+                requiresValidation, false /* automaticIpVersionSelectionEnabled */,
+                false /* automaticNattKeepaliveTimerEnabled */);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileUsernamePassword(boolean isRestrictedToTestNetworks)
             throws Exception {
-
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
-                false /* requiresValidation */);
+                false /* requiresValidation */, false /* automaticIpVersionSelectionEnabled */,
+                false /* automaticNattKeepaliveTimerEnabled */);
     }
 
     private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks)
             throws Exception {
-        final Ikev2VpnProfile.Builder builder =
-                new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
                         .setAuthDigitalSignature(
                                 mUserCertKey.cert, mUserCertKey.key, mServerRootCa);
         return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks,
-                false /* requiresValidation */);
+                false /* requiresValidation */, false /* automaticIpVersionSelectionEnabled */,
+                false /* automaticNattKeepaliveTimerEnabled */);
     }
 
     private void checkBasicIkev2VpnProfile(@NonNull Ikev2VpnProfile profile) throws Exception {
@@ -687,6 +709,56 @@
                 true /* testSessionKey */, false /* testIkeTunConnParams */);
     }
 
+    @Test
+    public void testBuildIkev2VpnProfileWithAutomaticNattKeepaliveTimerEnabled() throws Exception {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
+        // 34 shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestUApis());
+
+        final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
+                Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
+        assertFalse(shimWithDefaultValue.isAutomaticNattKeepaliveTimerEnabled());
+
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+                        .setAuthPsk(TEST_PSK);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
+                false /* isRestrictedToTestNetworks */,
+                false /* requiresValidation */,
+                false /* automaticIpVersionSelectionEnabled */,
+                true /* automaticNattKeepaliveTimerEnabled */);
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
+                Ikev2VpnProfileShimImpl.newInstance(profile);
+        assertTrue(shim.isAutomaticNattKeepaliveTimerEnabled());
+    }
+
+    @Test
+    public void testBuildIkev2VpnProfileWithAutomaticIpVersionSelectionEnabled() throws Exception {
+        // Cannot use @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) because this test also requires API
+        // 34 shims, and @IgnoreUpTo does not check that.
+        assumeTrue(TestUtils.shouldTestUApis());
+
+        final Ikev2VpnProfile profileWithDefaultValue = buildIkev2VpnProfilePsk(TEST_SERVER_ADDR_V6,
+                false /* isRestrictedToTestNetworks */, false /* requiresValidation */);
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shimWithDefaultValue =
+                Ikev2VpnProfileShimImpl.newInstance(profileWithDefaultValue);
+        assertFalse(shimWithDefaultValue.isAutomaticIpVersionSelectionEnabled());
+
+        final Ikev2VpnProfileBuilderShim builder =
+                Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY)
+                        .setAuthPsk(TEST_PSK);
+        final Ikev2VpnProfile profile = buildIkev2VpnProfileCommon(builder,
+                false /* isRestrictedToTestNetworks */,
+                false /* requiresValidation */,
+                true /* automaticIpVersionSelectionEnabled */,
+                false /* automaticNattKeepaliveTimerEnabled */);
+        final Ikev2VpnProfileShim<Ikev2VpnProfile> shim =
+                Ikev2VpnProfileShimImpl.newInstance(profile);
+        assertTrue(shim.isAutomaticIpVersionSelectionEnabled());
+    }
+
     private static class CertificateAndKey {
         public final X509Certificate cert;
         public final PrivateKey key;
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 10331b3..0b48e08 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -978,7 +978,7 @@
                 new String[]{"android", "local"}, /* hostName */
                 PORT,
                 List.of(IPV4_ADDRESS),
-                List.of(IPV6_ADDRESS),
+                List.of("2001:db8::1", "2001:db8::2"),
                 List.of() /* textStrings */,
                 List.of(MdnsServiceInfo.TextEntry.fromBytes(new byte[]{
                         'k', 'e', 'y', '=', (byte) 0xFF, (byte) 0xFE})) /* textEntries */,
@@ -998,6 +998,11 @@
         assertEquals(1, info.getAttributes().size());
         assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFE}, info.getAttributes().get("key"));
         assertEquals(parseNumericAddress(IPV4_ADDRESS), info.getHost());
+        assertEquals(3, info.getHostAddresses().size());
+        assertTrue(info.getHostAddresses().stream().anyMatch(
+                address -> address.equals(parseNumericAddress("2001:db8::1"))));
+        assertTrue(info.getHostAddresses().stream().anyMatch(
+                address -> address.equals(parseNumericAddress("2001:db8::2"))));
         assertEquals(network, info.getNetwork());
 
         // Verify the listener has been unregistered.
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
index a4e03a8..e7d7a98 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -119,6 +119,26 @@
     }
 
     @Test
+    public void constructor_createWithUppercaseKeys_correctAttributes() {
+        MdnsServiceInfo info =
+                new MdnsServiceInfo(
+                        "my-mdns-service",
+                        new String[] {"_testtype", "_tcp"},
+                        List.of(),
+                        new String[] {"my-host", "local"},
+                        12345,
+                        "192.168.1.1",
+                        "2001::1",
+                        List.of("KEY=Value"),
+                        /* textEntries= */ null);
+
+        assertEquals("Value", info.getAttributeByKey("key"));
+        assertEquals("Value", info.getAttributeByKey("KEY"));
+        assertEquals(1, info.getAttributes().size());
+        assertEquals("KEY", info.getAttributes().keySet().iterator().next());
+    }
+
+    @Test
     public void getInterfaceIndex_constructorWithDefaultValues_returnsMinusOne() {
         MdnsServiceInfo info =
                 new MdnsServiceInfo(