Merge "cronet: delete cronet prebuilts"
diff --git a/Cronet/TEST_MAPPING b/Cronet/TEST_MAPPING
deleted file mode 100644
index b1f3088..0000000
--- a/Cronet/TEST_MAPPING
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "CtsCronetTestCases"
-    }
-  ]
-}
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 95fc9ab..2c28b8d 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -42,7 +42,7 @@
 }
 
 android_test {
-    name: "CtsCronetTestCases",
+    name: "CtsNetHttpTestCases",
     compile_multilib: "both", // Include both the 32 and 64 bit versions
     defaults: [
         "CronetTestJavaDefaults",
@@ -59,6 +59,7 @@
         "ctstestrunner-axt",
         "ctstestserver",
         "junit",
+        "hamcrest-library",
     ],
     libs: [
         "android.test.runner",
diff --git a/Cronet/tests/cts/AndroidTest.xml b/Cronet/tests/cts/AndroidTest.xml
index d2422f1..e0421fd 100644
--- a/Cronet/tests/cts/AndroidTest.xml
+++ b/Cronet/tests/cts/AndroidTest.xml
@@ -23,14 +23,14 @@
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="CtsCronetTestCases.apk" />
+        <option name="test-file-name" value="CtsNetHttpTestCases.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.net.http.cts" />
         <option name="runtime-hint" value="10s" />
     </test>
 
-    <!-- Only run CtsCronetTestcasess in MTS if the Tethering Mainline module is installed. -->
+    <!-- Only run CtsNetHttpTestCases in MTS if the Tethering Mainline module is installed. -->
     <object type="module_controller"
             class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
         <option name="mainline-module-package-name" value="com.google.android.tethering" />
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
new file mode 100644
index 0000000..b07367a
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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 static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode;
+import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.net.http.HttpEngine;
+import android.net.http.UrlRequest;
+import android.net.http.UrlResponseInfo;
+import android.net.http.cts.util.TestUrlRequestCallback;
+import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class HttpEngineTest {
+    private static final String HOST = "source.android.com";
+    private static final String URL = "https://" + HOST;
+
+    private HttpEngine.Builder mEngineBuilder;
+    private TestUrlRequestCallback mCallback;
+    private HttpEngine mEngine;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        skipIfNoInternetConnection(context);
+        mEngineBuilder = new HttpEngine.Builder(context);
+        mCallback = new TestUrlRequestCallback();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mEngine != null) {
+            mEngine.shutdown();
+        }
+    }
+
+    @Test
+    public void testHttpEngine_Default() throws Exception {
+        mEngine = mEngineBuilder.build();
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+        builder.build().start();
+
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+        UrlResponseInfo info = mCallback.mResponseInfo;
+        assertOKStatusCode(info);
+        assertEquals("h2", info.getNegotiatedProtocol());
+    }
+
+    @Test
+    public void testHttpEngine_DisableHttp2() throws Exception {
+        mEngine = mEngineBuilder.setEnableHttp2(false).build();
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+        builder.build().start();
+
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+        UrlResponseInfo info = mCallback.mResponseInfo;
+        assertOKStatusCode(info);
+        assertEquals("http/1.1", info.getNegotiatedProtocol());
+    }
+
+    @Test
+    public void testHttpEngine_EnableQuic() throws Exception {
+        // The hint doesn't guarantee that QUIC will win the race, just that it will race TCP.
+        // If this ends up being flaky, consider sending multiple requests.
+        mEngine = mEngineBuilder.setEnableQuic(true).addQuicHint(HOST, 443, 443).build();
+        UrlRequest.Builder builder =
+                mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+        builder.build().start();
+
+        mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+        UrlResponseInfo info = mCallback.mResponseInfo;
+        assertOKStatusCode(info);
+        assertEquals("h3", info.getNegotiatedProtocol());
+    }
+
+    @Test
+    public void testHttpEngine_GetDefaultUserAgent() throws Exception {
+        assertThat(mEngineBuilder.getDefaultUserAgent(), containsString("AndroidHttpClient"));
+    }
+}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
similarity index 61%
rename from Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
rename to Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
index 598be0e..d7d3679 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
@@ -16,22 +16,22 @@
 
 package android.net.http.cts;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode;
+import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
 
 import android.content.Context;
-import android.net.ConnectivityManager;
 import android.net.http.HttpEngine;
 import android.net.http.UrlRequest;
 import android.net.http.UrlRequest.Status;
 import android.net.http.UrlResponseInfo;
-import android.net.http.cts.util.CronetCtsTestServer;
+import android.net.http.cts.util.HttpCtsTestServer;
 import android.net.http.cts.util.TestStatusListener;
 import android.net.http.cts.util.TestUrlRequestCallback;
 import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
 
-import androidx.annotation.NonNull;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -41,40 +41,29 @@
 import org.junit.runner.RunWith;
 
 @RunWith(AndroidJUnit4.class)
-public class CronetUrlRequestTest {
-    private static final String TAG = CronetUrlRequestTest.class.getSimpleName();
-
-    @NonNull private HttpEngine mHttpEngine;
-    @NonNull private TestUrlRequestCallback mCallback;
-    @NonNull private ConnectivityManager mCm;
-    @NonNull private CronetCtsTestServer mTestServer;
+public class UrlRequestTest {
+    private TestUrlRequestCallback mCallback;
+    private HttpCtsTestServer mTestServer;
+    private HttpEngine mHttpEngine;
 
     @Before
     public void setUp() throws Exception {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        skipIfNoInternetConnection(context);
         HttpEngine.Builder builder = new HttpEngine.Builder(context);
-        builder.setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_IN_MEMORY, 100 * 1024)
-                .setEnableHttp2(true)
-                // .setEnableBrotli(true)
-                .setEnableQuic(true);
         mHttpEngine = builder.build();
         mCallback = new TestUrlRequestCallback();
-        mTestServer = new CronetCtsTestServer(context);
+        mTestServer = new HttpCtsTestServer(context);
     }
 
     @After
     public void tearDown() throws Exception {
-        mHttpEngine.shutdown();
-        mTestServer.shutdown();
-    }
-
-    private static void assertGreaterThan(String msg, int first, int second) {
-        assertTrue(msg + " Excepted " + first + " to be greater than " + second, first > second);
-    }
-
-    private void assertHasTestableNetworks() {
-        assertNotNull("This test requires a working Internet connection", mCm.getActiveNetwork());
+        if (mHttpEngine != null) {
+            mHttpEngine.shutdown();
+        }
+        if (mTestServer != null) {
+            mTestServer.shutdown();
+        }
     }
 
     private UrlRequest buildUrlRequest(String url) {
@@ -83,18 +72,14 @@
 
     @Test
     public void testUrlRequestGet_CompletesSuccessfully() throws Exception {
-        assertHasTestableNetworks();
         String url = mTestServer.getSuccessUrl();
         UrlRequest request = buildUrlRequest(url);
         request.start();
 
         mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
         UrlResponseInfo info = mCallback.mResponseInfo;
-        assertEquals(
-                "Unexpected http status code from " + url + ".", 200, info.getHttpStatusCode());
-        assertGreaterThan(
-                "Received byte from " + url + " is 0.", (int) info.getReceivedByteCount(), 0);
+        assertOKStatusCode(info);
+        assertThat("Received byte count must be > 0", info.getReceivedByteCount(), greaterThan(0L));
     }
 
     @Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt b/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
similarity index 82%
rename from Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
rename to Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
index 3ccb571..87d5108 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
@@ -19,8 +19,8 @@
 import android.content.Context
 import android.webkit.cts.CtsTestServer
 
-/** Extends CtsTestServer to handle POST requests and other cronet specific test requests */
-class CronetCtsTestServer(context: Context) : CtsTestServer(context) {
+/** Extends CtsTestServer to handle POST requests and other test specific requests */
+class HttpCtsTestServer(context: Context) : CtsTestServer(context) {
 
     val successUrl: String = getAssetUrl("html/hello_world.html")
 }
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUploadDataProvider.java b/Cronet/tests/cts/src/android/net/http/cts/util/TestUploadDataProvider.java
new file mode 100644
index 0000000..d047828
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestUploadDataProvider.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http.cts.util;
+
+import android.net.http.UploadDataProvider;
+import android.net.http.UploadDataSink;
+import android.os.ConditionVariable;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** An UploadDataProvider implementation used in tests. */
+public class TestUploadDataProvider extends UploadDataProvider {
+    // Indicates whether all success callbacks are synchronous or asynchronous.
+    // Doesn't apply to errors.
+    public enum SuccessCallbackMode {
+        SYNC,
+        ASYNC
+    }
+
+    // Indicates whether failures should throw exceptions, invoke callbacks
+    // synchronously, or invoke callback asynchronously.
+    public enum FailMode {
+        NONE,
+        THROWN,
+        CALLBACK_SYNC,
+        CALLBACK_ASYNC
+    }
+
+    private final ArrayList<byte[]> mReads = new ArrayList<byte[]>();
+    private final SuccessCallbackMode mSuccessCallbackMode;
+    private final Executor mExecutor;
+
+    private boolean mChunked;
+
+    // Index of read to fail on.
+    private int mReadFailIndex = -1;
+    // Indicates how to fail on a read.
+    private FailMode mReadFailMode = FailMode.NONE;
+
+    private FailMode mRewindFailMode = FailMode.NONE;
+
+    private FailMode mLengthFailMode = FailMode.NONE;
+
+    private int mNumReadCalls;
+    private int mNumRewindCalls;
+
+    private int mNextRead;
+    private boolean mStarted;
+    private boolean mReadPending;
+    private boolean mRewindPending;
+    // Used to ensure there are no read/rewind requests after a failure.
+    private boolean mFailed;
+
+    private final AtomicBoolean mClosed = new AtomicBoolean(false);
+    private final ConditionVariable mAwaitingClose = new ConditionVariable(false);
+
+    public TestUploadDataProvider(
+            SuccessCallbackMode successCallbackMode, final Executor executor) {
+        mSuccessCallbackMode = successCallbackMode;
+        mExecutor = executor;
+    }
+
+    // Adds the result to be returned by a successful read request.  The
+    // returned bytes must all fit within the read buffer provided by Cronet.
+    // After a rewind, if there is one, all reads will be repeated.
+    public void addRead(byte[] read) {
+        if (mStarted) {
+            throw new IllegalStateException("Adding bytes after read");
+        }
+        mReads.add(read);
+    }
+
+    public void setReadFailure(int readFailIndex, FailMode readFailMode) {
+        mReadFailIndex = readFailIndex;
+        mReadFailMode = readFailMode;
+    }
+
+    public void setLengthFailure() {
+        mLengthFailMode = FailMode.THROWN;
+    }
+
+    public void setRewindFailure(FailMode rewindFailMode) {
+        mRewindFailMode = rewindFailMode;
+    }
+
+    public void setChunked(boolean chunked) {
+        mChunked = chunked;
+    }
+
+    public int getNumReadCalls() {
+        return mNumReadCalls;
+    }
+
+    public int getNumRewindCalls() {
+        return mNumRewindCalls;
+    }
+
+    /** Returns the cumulative length of all data added by calls to addRead. */
+    @Override
+    public long getLength() throws IOException {
+        if (mClosed.get()) {
+            throw new ClosedChannelException();
+        }
+        if (mLengthFailMode == FailMode.THROWN) {
+            throw new IllegalStateException("Sync length failure");
+        }
+        return getUploadedLength();
+    }
+
+    public long getUploadedLength() {
+        if (mChunked) {
+            return -1;
+        }
+        long length = 0;
+        for (byte[] read : mReads) {
+            length += read.length;
+        }
+        return length;
+    }
+
+    @Override
+    public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer)
+            throws IOException {
+        int currentReadCall = mNumReadCalls;
+        ++mNumReadCalls;
+        if (mClosed.get()) {
+            throw new ClosedChannelException();
+        }
+        assertIdle();
+
+        if (maybeFailRead(currentReadCall, uploadDataSink)) {
+            mFailed = true;
+            return;
+        }
+
+        mReadPending = true;
+        mStarted = true;
+
+        final boolean finalChunk = (mChunked && mNextRead == mReads.size() - 1);
+        if (mNextRead < mReads.size()) {
+            if ((byteBuffer.limit() - byteBuffer.position()) < mReads.get(mNextRead).length) {
+                throw new IllegalStateException("Read buffer smaller than expected.");
+            }
+            byteBuffer.put(mReads.get(mNextRead));
+            ++mNextRead;
+        } else {
+            throw new IllegalStateException("Too many reads: " + mNextRead);
+        }
+
+        Runnable completeRunnable =
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mReadPending = false;
+                        uploadDataSink.onReadSucceeded(finalChunk);
+                    }
+                };
+        if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) {
+            completeRunnable.run();
+        } else {
+            mExecutor.execute(completeRunnable);
+        }
+    }
+
+    @Override
+    public void rewind(final UploadDataSink uploadDataSink) throws IOException {
+        ++mNumRewindCalls;
+        if (mClosed.get()) {
+            throw new ClosedChannelException();
+        }
+        assertIdle();
+
+        if (maybeFailRewind(uploadDataSink)) {
+            mFailed = true;
+            return;
+        }
+
+        if (mNextRead == 0) {
+            // Should never try and rewind when rewinding does nothing.
+            throw new IllegalStateException("Unexpected rewind when already at beginning");
+        }
+
+        mRewindPending = true;
+        mNextRead = 0;
+
+        Runnable completeRunnable =
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mRewindPending = false;
+                        uploadDataSink.onRewindSucceeded();
+                    }
+                };
+        if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) {
+            completeRunnable.run();
+        } else {
+            mExecutor.execute(completeRunnable);
+        }
+    }
+
+    private void assertIdle() {
+        if (mReadPending) {
+            throw new IllegalStateException("Unexpected operation during read");
+        }
+        if (mRewindPending) {
+            throw new IllegalStateException("Unexpected operation during rewind");
+        }
+        if (mFailed) {
+            throw new IllegalStateException("Unexpected operation after failure");
+        }
+    }
+
+    private boolean maybeFailRead(int readIndex, final UploadDataSink uploadDataSink) {
+        if (readIndex != mReadFailIndex) return false;
+
+        switch (mReadFailMode) {
+            case THROWN:
+                throw new IllegalStateException("Thrown read failure");
+            case CALLBACK_SYNC:
+                uploadDataSink.onReadError(new IllegalStateException("Sync read failure"));
+                return true;
+            case CALLBACK_ASYNC:
+                Runnable errorRunnable =
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                uploadDataSink.onReadError(
+                                        new IllegalStateException("Async read failure"));
+                            }
+                        };
+                mExecutor.execute(errorRunnable);
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private boolean maybeFailRewind(final UploadDataSink uploadDataSink) {
+        switch (mRewindFailMode) {
+            case THROWN:
+                throw new IllegalStateException("Thrown rewind failure");
+            case CALLBACK_SYNC:
+                uploadDataSink.onRewindError(new IllegalStateException("Sync rewind failure"));
+                return true;
+            case CALLBACK_ASYNC:
+                Runnable errorRunnable =
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                uploadDataSink.onRewindError(
+                                        new IllegalStateException("Async rewind failure"));
+                            }
+                        };
+                mExecutor.execute(errorRunnable);
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (!mClosed.compareAndSet(false, true)) {
+            throw new AssertionError("Closed twice");
+        }
+        mAwaitingClose.open();
+    }
+
+    public void assertClosed() {
+        mAwaitingClose.block(5000);
+        if (!mClosed.get()) {
+            throw new AssertionError("Was not closed");
+        }
+    }
+}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt b/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt
new file mode 100644
index 0000000..d30c059
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.util
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.http.UrlResponseInfo
+import org.junit.Assert.assertEquals
+import org.junit.Assume.assumeNotNull
+
+fun skipIfNoInternetConnection(context: Context) {
+    val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
+    assumeNotNull(
+        "This test requires a working Internet connection", connectivityManager.getActiveNetwork())
+}
+
+fun assertOKStatusCode(info: UrlResponseInfo) {
+    assertEquals("Status code must be 200 OK", 200, info.getHttpStatusCode())
+}
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 172670e..6d17476 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,4 +1,3 @@
-chenbruce@google.com
 chiachangwang@google.com
 cken@google.com
 huangaaron@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 700a085..a1e81c8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -64,6 +64,9 @@
       "name": "connectivity_native_test"
     },
     {
+      "name": "CtsNetHttpTestCases"
+    },
+    {
       "name": "libclat_test"
     },
     {
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index b832e16..23467e7 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -43,7 +43,9 @@
     <uses-permission android:name="android.permission.WRITE_SETTINGS" />
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 
+    <!-- Sending non-protected broadcast from system uid is not allowed. -->
     <protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
+    <protected-broadcast android:name="com.android.server.connectivity.KeepaliveTracker.TCP_POLLING_ALARM" />
 
     <application
         android:process="com.android.networkstack.process"
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 63702f2..f90b3a4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -1802,7 +1802,7 @@
             TestNetworkAgent mobile, TestNetworkAgent wifi, TestNetworkAgent dun) throws Exception {
         final NetworkCallback dunNetworkCallback = setupDunUpstreamTest(automatic, inOrder);
 
-        // Pretend cellular connected and expect the upstream to be set.
+        // Pretend cellular connected and expect the upstream to be not set.
         mobile.fakeConnect();
         mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST);
         mLooper.dispatchAll();
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index c39269e..b7ca3af 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -21,6 +21,18 @@
 #include <stdbool.h>
 #include <stdint.h>
 
+// bionic kernel uapi linux/udp.h header is munged...
+#define __kernel_udphdr udphdr
+#include <linux/udp.h>
+
+// Offsets from beginning of L4 (TCP/UDP) header
+#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
+#define UDP_OFFSET(field) offsetof(struct udphdr, field)
+
+// Offsets from beginning of L3 (IPv4/IPv6) header
+#define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+
 // this returns 0 iff skb->sk is NULL
 static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
 
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 43920d0..84da79d 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -46,8 +46,9 @@
 static const bool INGRESS = false;
 static const bool EGRESS = true;
 
-#define IP_PROTO_OFF offsetof(struct iphdr, protocol)
-#define IPV6_PROTO_OFF offsetof(struct ipv6hdr, nexthdr)
+// Used for 'bool enable_tracing'
+static const bool TRACE_ON = true;
+static const bool TRACE_OFF = false;
 
 // offsetof(struct iphdr, ihl) -- but that's a bitfield
 #define IPPROTO_IHL_OFF 0
@@ -60,14 +61,18 @@
 #define TCP_FLAG32_OFF 12
 
 // For maps netd does not need to access
-#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
-                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)      \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,              \
+                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false, \
+                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,       \
+                       /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
 
 // For maps netd only needs read only access to
-#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
-    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
-                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries)         \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries,                 \
+                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false, \
+                       BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,       \
+                       /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
 
 // For maps netd needs to be able to read and write
 #define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -95,6 +100,19 @@
 /* never actually used from ebpf */
 DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
 
+// A single-element configuration array, packet tracing is enabled when 'true'.
+DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
+                   AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+                   BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,
+                   /*ignore_on_user*/true, /*ignore_on_userdebug*/false)
+
+// A ring buffer on which packet information is pushed. This map will only be loaded
+// on eng and userdebug devices. User devices won't load this to save memory.
+DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
+                       AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+                       BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,
+                       /*ignore_on_user*/true, /*ignore_on_userdebug*/false);
+
 // iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
 // selinux contexts, because even non-xt_bpf iptables mutations are implemented as
 // a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -222,12 +240,72 @@
         : bpf_skb_load_bytes(skb, L3_off, to, len);
 }
 
+static __always_inline inline void do_packet_tracing(
+        const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
+        const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+    if (!enable_tracing) return;
+    if (kver < KVER(5, 8, 0)) return;
+
+    uint32_t mapKey = 0;
+    bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
+    if (traceConfig == NULL) return;
+    if (*traceConfig == false) return;
+
+    PacketTrace* pkt = bpf_packet_trace_ringbuf_reserve();
+    if (pkt == NULL) return;
+
+    // Errors from bpf_skb_load_bytes_net are ignored to favor returning something
+    // over returning nothing. In the event of an error, the kernel will fill in
+    // zero for the destination memory. Do not change the default '= 0' below.
+
+    uint8_t proto = 0;
+    uint8_t L4_off = 0;
+    uint8_t ipVersion = 0;
+    if (skb->protocol == htons(ETH_P_IP)) {
+        (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &proto, sizeof(proto), kver);
+        (void)bpf_skb_load_bytes_net(skb, IPPROTO_IHL_OFF, &L4_off, sizeof(L4_off), kver);
+        L4_off = (L4_off & 0x0F) * 4;  // IHL calculation.
+        ipVersion = 4;
+    } else if (skb->protocol == htons(ETH_P_IPV6)) {
+        (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &proto, sizeof(proto), kver);
+        L4_off = sizeof(struct ipv6hdr);
+        ipVersion = 6;
+    }
+
+    uint8_t flags = 0;
+    __be16 sport = 0, dport = 0;
+    if (proto == IPPROTO_TCP && L4_off >= 20) {
+        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_FLAG32_OFF + 1, &flags, sizeof(flags), kver);
+        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(source), &sport, sizeof(sport), kver);
+        (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(dest), &dport, sizeof(dport), kver);
+    } else if (proto == IPPROTO_UDP && L4_off >= 20) {
+        (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(source), &sport, sizeof(sport), kver);
+        (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(dest), &dport, sizeof(dport), kver);
+    }
+
+    pkt->timestampNs = bpf_ktime_get_boot_ns();
+    pkt->ifindex = skb->ifindex;
+    pkt->length = skb->len;
+
+    pkt->uid = uid;
+    pkt->tag = tag;
+    pkt->sport = sport;
+    pkt->dport = dport;
+
+    pkt->egress = egress;
+    pkt->ipProto = proto;
+    pkt->tcpFlags = flags;
+    pkt->ipVersion = ipVersion;
+
+    bpf_packet_trace_ringbuf_submit(pkt);
+}
+
 static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, const unsigned kver) {
     uint32_t flag = 0;
     if (skb->protocol == htons(ETH_P_IP)) {
         uint8_t proto;
         // no need to check for success, proto will be zeroed if bpf_skb_load_bytes_net() fails
-        (void)bpf_skb_load_bytes_net(skb, IP_PROTO_OFF, &proto, sizeof(proto), kver);
+        (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &proto, sizeof(proto), kver);
         if (proto == IPPROTO_ESP) return true;
         if (proto != IPPROTO_TCP) return false;  // handles read failure above
         uint8_t ihl;
@@ -243,7 +321,7 @@
     } else if (skb->protocol == htons(ETH_P_IPV6)) {
         uint8_t proto;
         // no need to check for success, proto will be zeroed if bpf_skb_load_bytes_net() fails
-        (void)bpf_skb_load_bytes_net(skb, IPV6_PROTO_OFF, &proto, sizeof(proto), kver);
+        (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &proto, sizeof(proto), kver);
         if (proto == IPPROTO_ESP) return true;
         if (proto != IPPROTO_TCP) return false;  // handles read failure above
         // if the read below fails, we'll just assume no TCP flags are set, which is fine.
@@ -315,6 +393,7 @@
 }
 
 static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+                                                      const bool enable_tracing,
                                                       const unsigned kver) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     uint64_t cookie = bpf_get_socket_cookie(skb);
@@ -374,34 +453,51 @@
         key.tag = 0;
     }
 
+    do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
     update_stats_with_config(skb, egress, &key, *selectedMap);
     update_app_uid_stats_map(skb, egress, &uid);
     asm("%0 &= 1" : "+r"(match));
     return match;
 }
 
+DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
+                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
+                    "fs_bpf_netd_readonly", "", false, true, false)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+}
+
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
                                 bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, KVER(4, 19, 0));
+    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
                                 bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, INGRESS, KVER_NONE);
+    return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
+}
+
+DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
+                    bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
+                    BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
+                    "fs_bpf_netd_readonly", "", false, true, false)
+(struct __sk_buff* skb) {
+    return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
                                 bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, KVER(4, 19, 0));
+    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
 }
 
 DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
                                 bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
 (struct __sk_buff* skb) {
-    return bpf_traffic_account(skb, EGRESS, KVER_NONE);
+    return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
 }
 
 // WARNING: Android T's non-updatable netd depends on the name of this program.
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index cc88680..be604f9 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -69,6 +69,24 @@
     uint64_t tcpTxPackets;
 } Stats;
 
+typedef struct {
+  uint64_t timestampNs;
+  uint32_t ifindex;
+  uint32_t length;
+
+  uint32_t uid;
+  uint32_t tag;
+
+  __be16 sport;
+  __be16 dport;
+
+  bool egress;
+  uint8_t ipProto;
+  uint8_t tcpFlags;
+  uint8_t ipVersion; // 4=IPv4, 6=IPv6, 0=unknown
+} PacketTrace;
+STRUCT_SIZE(PacketTrace, 8+4+4 + 4+4 + 2+2 + 1+1+1+1);
+
 // Since we cannot garbage collect the stats map since device boot, we need to make these maps as
 // large as possible. The maximum size of number of map entries we can have is depend on the rlimit
 // of MEM_LOCK granted to netd. The memory space needed by each map can be calculated by the
@@ -87,7 +105,8 @@
 // dozable_uid_map:     key:  4 bytes, value:  1 bytes, cost:  145216 bytes    =   145Kbytes
 // standby_uid_map:     key:  4 bytes, value:  1 bytes, cost:  145216 bytes    =   145Kbytes
 // powersave_uid_map:   key:  4 bytes, value:  1 bytes, cost:  145216 bytes    =   145Kbytes
-// total:                                                                         4930Kbytes
+// packet_trace_ringbuf:key:  0 bytes, value: 24 bytes, cost:   32768 bytes    =    32Kbytes
+// total:                                                                         4962Kbytes
 // It takes maximum 4.9MB kernel memory space if all maps are full, which requires any devices
 // running this module to have a memlock rlimit to be larger then 5MB. In the old qtaguid module,
 // we don't have a total limit for data entries but only have limitation of tags each uid can have.
@@ -102,6 +121,7 @@
 static const int IFACE_STATS_MAP_SIZE = 1000;
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 4000;
+static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
 
 #ifdef __cplusplus
 
@@ -145,6 +165,8 @@
 #define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
 #define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
 #define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
+#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
+#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
 
 #endif // __cplusplus
 
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index eb77288..5532853 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -192,15 +192,20 @@
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
     method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
     method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
-    method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
-    method public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
+    method public void registerServiceInfoCallback(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
+    method @Deprecated public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
+    method @Deprecated public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
     method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener);
+    method public void stopServiceResolution(@NonNull android.net.nsd.NsdManager.ResolveListener);
     method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener);
+    method public void unregisterServiceInfoCallback(@NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
     field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
     field public static final String EXTRA_NSD_STATE = "nsd_state";
     field public static final int FAILURE_ALREADY_ACTIVE = 3; // 0x3
+    field public static final int FAILURE_BAD_PARAMETERS = 6; // 0x6
     field public static final int FAILURE_INTERNAL_ERROR = 0; // 0x0
     field public static final int FAILURE_MAX_LIMIT = 4; // 0x4
+    field public static final int FAILURE_OPERATION_NOT_RUNNING = 5; // 0x5
     field public static final int NSD_STATE_DISABLED = 1; // 0x1
     field public static final int NSD_STATE_ENABLED = 2; // 0x2
     field public static final int PROTOCOL_DNS_SD = 1; // 0x1
@@ -224,21 +229,32 @@
 
   public static interface NsdManager.ResolveListener {
     method public void onResolveFailed(android.net.nsd.NsdServiceInfo, int);
+    method public default void onResolveStopped(@NonNull android.net.nsd.NsdServiceInfo);
     method public void onServiceResolved(android.net.nsd.NsdServiceInfo);
+    method public default void onStopResolutionFailed(@NonNull android.net.nsd.NsdServiceInfo, int);
+  }
+
+  public static interface NsdManager.ServiceInfoCallback {
+    method public void onServiceInfoCallbackRegistrationFailed(int);
+    method public void onServiceInfoCallbackUnregistered();
+    method public void onServiceLost();
+    method public void onServiceUpdated(@NonNull android.net.nsd.NsdServiceInfo);
   }
 
   public final class NsdServiceInfo implements android.os.Parcelable {
     ctor public NsdServiceInfo();
     method public int describeContents();
     method public java.util.Map<java.lang.String,byte[]> getAttributes();
-    method public java.net.InetAddress getHost();
+    method @Deprecated public java.net.InetAddress getHost();
+    method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
     method @Nullable public android.net.Network getNetwork();
     method public int getPort();
     method public String getServiceName();
     method public String getServiceType();
     method public void removeAttribute(String);
     method public void setAttribute(String, String);
-    method public void setHost(java.net.InetAddress);
+    method @Deprecated public void setHost(java.net.InetAddress);
+    method public void setHostAddresses(@NonNull java.util.List<java.net.InetAddress>);
     method public void setNetwork(@Nullable android.net.Network);
     method public void setPort(int);
     method public void setServiceName(String);
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
index 1a262ec..d89bfa9 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -36,4 +36,10 @@
     void onUnregisterServiceSucceeded(int listenerKey);
     void onResolveServiceFailed(int listenerKey, int error);
     void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info);
+    void onStopResolutionFailed(int listenerKey, int error);
+    void onStopResolutionSucceeded(int listenerKey);
+    void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error);
+    void onServiceUpdated(int listenerKey, in NsdServiceInfo info);
+    void onServiceUpdatedLost(int listenerKey);
+    void onServiceInfoCallbackUnregistered(int listenerKey);
 }
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index b06ae55..5533154 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -32,4 +32,7 @@
     void stopDiscovery(int listenerKey);
     void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
     void startDaemon();
+    void stopResolution(int listenerKey);
+    void registerServiceInfoCallback(int listenerKey, in NsdServiceInfo serviceInfo);
+    void unregisterServiceInfoCallback(int listenerKey);
 }
\ No newline at end of file
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 45def36..122e3a0 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -16,6 +16,7 @@
 
 package android.net.nsd;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -44,6 +45,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -230,7 +233,6 @@
 
     /** @hide */
     public static final int DAEMON_CLEANUP                          = 18;
-
     /** @hide */
     public static final int DAEMON_STARTUP                          = 19;
 
@@ -245,6 +247,27 @@
     /** @hide */
     public static final int MDNS_DISCOVERY_MANAGER_EVENT            = 23;
 
+    /** @hide */
+    public static final int STOP_RESOLUTION                         = 24;
+    /** @hide */
+    public static final int STOP_RESOLUTION_FAILED                  = 25;
+    /** @hide */
+    public static final int STOP_RESOLUTION_SUCCEEDED               = 26;
+
+    /** @hide */
+    public static final int REGISTER_SERVICE_CALLBACK               = 27;
+    /** @hide */
+    public static final int REGISTER_SERVICE_CALLBACK_FAILED        = 28;
+    /** @hide */
+    public static final int SERVICE_UPDATED                         = 29;
+    /** @hide */
+    public static final int SERVICE_UPDATED_LOST                    = 30;
+
+    /** @hide */
+    public static final int UNREGISTER_SERVICE_CALLBACK             = 31;
+    /** @hide */
+    public static final int UNREGISTER_SERVICE_CALLBACK_SUCCEEDED   = 32;
+
     /** Dns based service discovery protocol */
     public static final int PROTOCOL_DNS_SD = 0x0001;
 
@@ -270,6 +293,15 @@
         EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
         EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
         EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT");
+        EVENT_NAMES.put(STOP_RESOLUTION, "STOP_RESOLUTION");
+        EVENT_NAMES.put(STOP_RESOLUTION_FAILED, "STOP_RESOLUTION_FAILED");
+        EVENT_NAMES.put(STOP_RESOLUTION_SUCCEEDED, "STOP_RESOLUTION_SUCCEEDED");
+        EVENT_NAMES.put(REGISTER_SERVICE_CALLBACK, "REGISTER_SERVICE_CALLBACK");
+        EVENT_NAMES.put(REGISTER_SERVICE_CALLBACK_FAILED, "REGISTER_SERVICE_CALLBACK_FAILED");
+        EVENT_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED");
+        EVENT_NAMES.put(UNREGISTER_SERVICE_CALLBACK, "UNREGISTER_SERVICE_CALLBACK");
+        EVENT_NAMES.put(UNREGISTER_SERVICE_CALLBACK_SUCCEEDED,
+                "UNREGISTER_SERVICE_CALLBACK_SUCCEEDED");
     }
 
     /** @hide */
@@ -595,6 +627,36 @@
         public void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
             sendInfo(RESOLVE_SERVICE_SUCCEEDED, listenerKey, info);
         }
+
+        @Override
+        public void onStopResolutionFailed(int listenerKey, int error) {
+            sendError(STOP_RESOLUTION_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onStopResolutionSucceeded(int listenerKey) {
+            sendNoArg(STOP_RESOLUTION_SUCCEEDED, listenerKey);
+        }
+
+        @Override
+        public void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+            sendError(REGISTER_SERVICE_CALLBACK_FAILED, listenerKey, error);
+        }
+
+        @Override
+        public void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+            sendInfo(SERVICE_UPDATED, listenerKey, info);
+        }
+
+        @Override
+        public void onServiceUpdatedLost(int listenerKey) {
+            sendNoArg(SERVICE_UPDATED_LOST, listenerKey);
+        }
+
+        @Override
+        public void onServiceInfoCallbackUnregistered(int listenerKey) {
+            sendNoArg(UNREGISTER_SERVICE_CALLBACK_SUCCEEDED, listenerKey);
+        }
     }
 
     /**
@@ -618,6 +680,37 @@
      */
     public static final int FAILURE_MAX_LIMIT                   = 4;
 
+    /**
+     * Indicates that the stop operation failed because it is not running.
+     * This failure is passed with {@link ResolveListener#onStopResolutionFailed}.
+     */
+    public static final int FAILURE_OPERATION_NOT_RUNNING       = 5;
+
+    /**
+     * Indicates that the service has failed to resolve because of bad parameters.
+     *
+     * This failure is passed with
+     * {@link ServiceInfoCallback#onServiceInfoCallbackRegistrationFailed}.
+     */
+    public static final int FAILURE_BAD_PARAMETERS              = 6;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            FAILURE_OPERATION_NOT_RUNNING,
+    })
+    public @interface StopOperationFailureCode {
+    }
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            FAILURE_ALREADY_ACTIVE,
+            FAILURE_BAD_PARAMETERS,
+    })
+    public @interface ResolutionFailureCode {
+    }
+
     /** Interface for callback invocation for service discovery */
     public interface DiscoveryListener {
 
@@ -646,12 +739,97 @@
         public void onServiceUnregistered(NsdServiceInfo serviceInfo);
     }
 
-    /** Interface for callback invocation for service resolution */
+    /**
+     * Callback for use with {@link NsdManager#resolveService} to resolve the service info and use
+     * with {@link NsdManager#stopServiceResolution} to stop resolution.
+     */
     public interface ResolveListener {
 
-        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
+        /**
+         * Called on the internal thread or with an executor passed to
+         * {@link NsdManager#resolveService} to report the resolution was failed with an error.
+         *
+         * A resolution operation would call either onServiceResolved or onResolveFailed once based
+         * on the result.
+         */
+        void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
 
-        public void onServiceResolved(NsdServiceInfo serviceInfo);
+        /**
+         * Called on the internal thread or with an executor passed to
+         * {@link NsdManager#resolveService} to report the resolved service info.
+         *
+         * A resolution operation would call either onServiceResolved or onResolveFailed once based
+         * on the result.
+         */
+        void onServiceResolved(NsdServiceInfo serviceInfo);
+
+        /**
+         * Called on the internal thread or with an executor passed to
+         * {@link NsdManager#resolveService} to report the resolution was stopped.
+         *
+         * A stop resolution operation would call either onResolveStopped or onStopResolutionFailed
+         * once based on the result.
+         */
+        default void onResolveStopped(@NonNull NsdServiceInfo serviceInfo) { }
+
+        /**
+         * Called once on the internal thread or with an executor passed to
+         * {@link NsdManager#resolveService} to report that stopping resolution failed with an
+         * error.
+         *
+         * A stop resolution operation would call either onResolveStopped or onStopResolutionFailed
+         * once based on the result.
+         */
+        default void onStopResolutionFailed(@NonNull NsdServiceInfo serviceInfo,
+                @StopOperationFailureCode int errorCode) { }
+    }
+
+    /**
+     * Callback to listen to service info updates.
+     *
+     * For use with {@link NsdManager#registerServiceInfoCallback} to register, and with
+     * {@link NsdManager#unregisterServiceInfoCallback} to stop listening.
+     */
+    public interface ServiceInfoCallback {
+
+        /**
+         * Reports that registering the callback failed with an error.
+         *
+         * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}.
+         *
+         * onServiceInfoCallbackRegistrationFailed will be called exactly once when the callback
+         * could not be registered. No other callback will be sent in that case.
+         */
+        void onServiceInfoCallbackRegistrationFailed(@ResolutionFailureCode int errorCode);
+
+        /**
+         * Reports updated service info.
+         *
+         * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}. Any
+         * service updates will be notified via this callback until
+         * {@link NsdManager#unregisterServiceInfoCallback} is called. This will only be called once
+         * the service is found, so may never be called if the service is never present.
+         */
+        void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo);
+
+        /**
+         * Reports when the service that this callback listens to becomes unavailable.
+         *
+         * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}. The
+         * service may become available again, in which case {@link #onServiceUpdated} will be
+         * called.
+         */
+        void onServiceLost();
+
+        /**
+         * Reports that service info updates have stopped.
+         *
+         * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}.
+         *
+         * A callback unregistration operation will call onServiceInfoCallbackUnregistered
+         * once. After this, the callback may be reused.
+         */
+        void onServiceInfoCallbackUnregistered();
     }
 
     @VisibleForTesting
@@ -744,6 +922,33 @@
                     executor.execute(() -> ((ResolveListener) listener).onServiceResolved(
                             (NsdServiceInfo) obj));
                     break;
+                case STOP_RESOLUTION_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((ResolveListener) listener).onStopResolutionFailed(
+                            ns, errorCode));
+                    break;
+                case STOP_RESOLUTION_SUCCEEDED:
+                    removeListener(key);
+                    executor.execute(() -> ((ResolveListener) listener).onResolveStopped(
+                            ns));
+                    break;
+                case REGISTER_SERVICE_CALLBACK_FAILED:
+                    removeListener(key);
+                    executor.execute(() -> ((ServiceInfoCallback) listener)
+                            .onServiceInfoCallbackRegistrationFailed(errorCode));
+                    break;
+                case SERVICE_UPDATED:
+                    executor.execute(() -> ((ServiceInfoCallback) listener)
+                            .onServiceUpdated((NsdServiceInfo) obj));
+                    break;
+                case SERVICE_UPDATED_LOST:
+                    executor.execute(() -> ((ServiceInfoCallback) listener).onServiceLost());
+                    break;
+                case UNREGISTER_SERVICE_CALLBACK_SUCCEEDED:
+                    removeListener(key);
+                    executor.execute(() -> ((ServiceInfoCallback) listener)
+                            .onServiceInfoCallbackUnregistered());
+                    break;
                 default:
                     Log.d(TAG, "Ignored " + message);
                     break;
@@ -1055,7 +1260,14 @@
      * @param serviceInfo service to be resolved
      * @param listener to receive callback upon success or failure. Cannot be null.
      * Cannot be in use for an active service resolution.
+     *
+     * @deprecated the returned ServiceInfo may get stale at any time after resolution, including
+     * immediately after the callback is called, and may not contain some service information that
+     * could be delivered later, like additional host addresses. Prefer using
+     * {@link #registerServiceInfoCallback}, which will keep the application up-to-date with the
+     * state of the service.
      */
+    @Deprecated
     public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
         resolveService(serviceInfo, Runnable::run, listener);
     }
@@ -1067,7 +1279,14 @@
      * @param serviceInfo service to be resolved
      * @param executor Executor to run listener callbacks with
      * @param listener to receive callback upon success or failure.
+     *
+     * @deprecated the returned ServiceInfo may get stale at any time after resolution, including
+     * immediately after the callback is called, and may not contain some service information that
+     * could be delivered later, like additional host addresses. Prefer using
+     * {@link #registerServiceInfoCallback}, which will keep the application up-to-date with the
+     * state of the service.
      */
+    @Deprecated
     public void resolveService(@NonNull NsdServiceInfo serviceInfo,
             @NonNull Executor executor, @NonNull ResolveListener listener) {
         checkServiceInfo(serviceInfo);
@@ -1079,6 +1298,85 @@
         }
     }
 
+    /**
+     * Stop service resolution initiated with {@link #resolveService}.
+     *
+     * A successful stop is notified with a call to {@link ResolveListener#onResolveStopped}.
+     *
+     * <p> Upon failure to stop service resolution for example if resolution is done or the
+     * requester stops resolution repeatedly, the application is notified
+     * {@link ResolveListener#onStopResolutionFailed} with {@link #FAILURE_OPERATION_NOT_RUNNING}
+     *
+     * @param listener This should be a listener object that was passed to {@link #resolveService}.
+     *                 It identifies the resolution that should be stopped and notifies of a
+     *                 successful or unsuccessful stop. Throws {@code IllegalArgumentException} if
+     *                 the listener was not passed to resolveService before.
+     */
+    public void stopServiceResolution(@NonNull ResolveListener listener) {
+        int id = getListenerKey(listener);
+        try {
+            mService.stopResolution(id);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Register a callback to listen for updates to a service.
+     *
+     * An application can listen to a service to continuously monitor availability of given service.
+     * The callback methods will be called on the passed executor. And service updates are sent with
+     * continuous calls to {@link ServiceInfoCallback#onServiceUpdated}.
+     *
+     * This is different from {@link #resolveService} which provides one shot service information.
+     *
+     * <p> An application can listen to a service once a time. It needs to cancel the registration
+     * before registering other callbacks. Upon failure to register a callback for example if
+     * it's a duplicated registration, the application is notified through
+     * {@link ServiceInfoCallback#onServiceInfoCallbackRegistrationFailed} with
+     * {@link #FAILURE_BAD_PARAMETERS} or {@link #FAILURE_ALREADY_ACTIVE}.
+     *
+     * @param serviceInfo the service to receive updates for
+     * @param executor Executor to run callbacks with
+     * @param listener to receive callback upon service update
+     */
+    public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
+            @NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
+        checkServiceInfo(serviceInfo);
+        int key = putListener(listener, executor, serviceInfo);
+        try {
+            mService.registerServiceInfoCallback(key, serviceInfo);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregister a callback registered with {@link #registerServiceInfoCallback}.
+     *
+     * A successful unregistration is notified with a call to
+     * {@link ServiceInfoCallback#onServiceInfoCallbackUnregistered}. The same callback can only be
+     * reused after this is called.
+     *
+     * <p>If the callback is not already registered, this will throw with
+     * {@link IllegalArgumentException}.
+     *
+     * @param listener This should be a listener object that was passed to
+     *                 {@link #registerServiceInfoCallback}. It identifies the registration that
+     *                 should be unregistered and notifies of a successful or unsuccessful stop.
+     *                 Throws {@code IllegalArgumentException} if the listener was not passed to
+     *                 {@link #registerServiceInfoCallback} before.
+     */
+    public void unregisterServiceInfoCallback(@NonNull ServiceInfoCallback listener) {
+        // Will throw IllegalArgumentException if the listener is not known
+        int id = getListenerKey(listener);
+        try {
+            mService.unregisterServiceInfoCallback(id);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     private static void checkListener(Object listener) {
         Objects.requireNonNull(listener, "listener cannot be null");
     }
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 6438a60..caeecdd 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -26,10 +26,14 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import com.android.net.module.util.InetAddressUtils;
+
 import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -46,7 +50,7 @@
 
     private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
 
-    private InetAddress mHost;
+    private final List<InetAddress> mHostAddresses = new ArrayList<>();
 
     private int mPort;
 
@@ -84,17 +88,32 @@
         mServiceType = s;
     }
 
-    /** Get the host address. The host address is valid for a resolved service. */
+    /**
+     * Get the host address. The host address is valid for a resolved service.
+     *
+     * @deprecated Use {@link #getHostAddresses()} to get the entire list of addresses for the host.
+     */
+    @Deprecated
     public InetAddress getHost() {
-        return mHost;
+        return mHostAddresses.size() == 0 ? null : mHostAddresses.get(0);
     }
 
-    /** Set the host address */
+    /**
+     * Set the host address
+     *
+     * @deprecated Use {@link #setHostAddresses(List)} to set multiple addresses for the host.
+     */
+    @Deprecated
     public void setHost(InetAddress s) {
-        mHost = s;
+        setHostAddresses(Collections.singletonList(s));
     }
 
-    /** Get port number. The port number is valid for a resolved service. */
+    /**
+     * Get port number. The port number is valid for a resolved service.
+     *
+     * The port is valid for all addresses.
+     * @see #getHostAddresses()
+     */
     public int getPort() {
         return mPort;
     }
@@ -105,6 +124,24 @@
     }
 
     /**
+     * Get the host addresses.
+     *
+     * All host addresses are valid for the resolved service.
+     * All addresses share the same port
+     * @see #getPort()
+     */
+    @NonNull
+    public List<InetAddress> getHostAddresses() {
+        return new ArrayList<>(mHostAddresses);
+    }
+
+    /** Set the host addresses */
+    public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+        mHostAddresses.clear();
+        mHostAddresses.addAll(addresses);
+    }
+
+    /**
      * Unpack txt information from a base-64 encoded byte array.
      *
      * @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -359,7 +396,7 @@
         StringBuilder sb = new StringBuilder();
         sb.append("name: ").append(mServiceName)
                 .append(", type: ").append(mServiceType)
-                .append(", host: ").append(mHost)
+                .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
                 .append(", port: ").append(mPort)
                 .append(", network: ").append(mNetwork);
 
@@ -377,12 +414,6 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeString(mServiceName);
         dest.writeString(mServiceType);
-        if (mHost != null) {
-            dest.writeInt(1);
-            dest.writeByteArray(mHost.getAddress());
-        } else {
-            dest.writeInt(0);
-        }
         dest.writeInt(mPort);
 
         // TXT record key/value pairs.
@@ -401,6 +432,10 @@
 
         dest.writeParcelable(mNetwork, 0);
         dest.writeInt(mInterfaceIndex);
+        dest.writeInt(mHostAddresses.size());
+        for (InetAddress address : mHostAddresses) {
+            InetAddressUtils.parcelInetAddress(dest, address, flags);
+        }
     }
 
     /** Implement the Parcelable interface */
@@ -410,13 +445,6 @@
                 NsdServiceInfo info = new NsdServiceInfo();
                 info.mServiceName = in.readString();
                 info.mServiceType = in.readString();
-
-                if (in.readInt() == 1) {
-                    try {
-                        info.mHost = InetAddress.getByAddress(in.createByteArray());
-                    } catch (java.net.UnknownHostException e) {}
-                }
-
                 info.mPort = in.readInt();
 
                 // TXT record key/value pairs.
@@ -432,6 +460,10 @@
                 }
                 info.mNetwork = in.readParcelable(null, Network.class);
                 info.mInterfaceIndex = in.readInt();
+                int size = in.readInt();
+                for (int i = 0; i < size; i++) {
+                    info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
+                }
                 return info;
             }
 
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index dd3404c..0b03983 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -470,7 +470,9 @@
   }
 
   public abstract class SocketKeepalive implements java.lang.AutoCloseable {
+    method public final void start(@IntRange(from=0xa, to=0xe10) int, int);
     field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
+    field public static final int FLAG_AUTOMATIC_ON_OFF = 1; // 0x1
     field public static final int SUCCESS = 0; // 0x0
   }
 
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 7b6e769..7db231e 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -188,7 +188,7 @@
 
     void startNattKeepaliveWithFd(in Network network, in ParcelFileDescriptor pfd, int resourceId,
             int intervalSeconds, in ISocketKeepaliveCallback cb, String srcAddr,
-            String dstAddr);
+            String dstAddr, boolean automaticOnOffKeepalives);
 
     void startTcpKeepalive(in Network network, in ParcelFileDescriptor pfd, int intervalSeconds,
             in ISocketKeepaliveCallback cb);
diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java
index 56cc923..4d45e70 100644
--- a/framework/src/android/net/NattSocketKeepalive.java
+++ b/framework/src/android/net/NattSocketKeepalive.java
@@ -47,13 +47,39 @@
         mResourceId = resourceId;
     }
 
+    /**
+     * Request that keepalive be started with the given {@code intervalSec}.
+     *
+     * When a VPN is running with the network for this keepalive as its underlying network, the
+     * system can monitor the TCP connections on that VPN to determine whether this keepalive is
+     * necessary. To enable this behavior, pass {@link SocketKeepalive#FLAG_AUTOMATIC_ON_OFF} into
+     * the flags. When this is enabled, the system will disable sending keepalive packets when
+     * there are no TCP connections over the VPN(s) running over this network to save battery, and
+     * restart sending them as soon as any TCP connection is opened over one of the VPN networks.
+     * When no VPN is running on top of this network, this flag has no effect, i.e. the keepalives
+     * are always sent with the specified interval.
+     *
+     * Also {@see SocketKeepalive}.
+     *
+     * @param intervalSec The target interval in seconds between keepalive packet transmissions.
+     *                    The interval should be between 10 seconds and 3600 seconds. Otherwise,
+     *                    the supplied {@link Callback} will see a call to
+     *                    {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}.
+     * @param flags Flags to enable/disable available options on this keepalive.
+     * @hide
+     */
     @Override
-    protected void startImpl(int intervalSec) {
+    protected void startImpl(int intervalSec, int flags) {
+        if (0 != (flags & ~FLAG_AUTOMATIC_ON_OFF)) {
+            throw new IllegalArgumentException("Illegal flag value for "
+                    + this.getClass().getSimpleName() + " : " + flags);
+        }
+        final boolean automaticOnOffKeepalives = 0 != (flags & FLAG_AUTOMATIC_ON_OFF);
         mExecutor.execute(() -> {
             try {
                 mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId,
-                        intervalSec, mCallback,
-                        mSource.getHostAddress(), mDestination.getHostAddress());
+                        intervalSec, mCallback, mSource.getHostAddress(),
+                        mDestination.getHostAddress(), automaticOnOffKeepalives);
             } catch (RemoteException e) {
                 Log.e(TAG, "Error starting socket keepalive: ", e);
                 throw e.rethrowFromSystemServer();
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 1486619..732bd87 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -483,6 +483,20 @@
      */
     public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
 
+    /**
+     * Sent by AutomaticOnOffKeepaliveTracker periodically (when relevant) to trigger monitor
+     * automatic keepalive request.
+     *
+     * NATT keepalives have an automatic mode where the system only sends keepalive packets when
+     * TCP sockets are open over a VPN. The system will check periodically for presence of
+     * such open sockets, and this message is what triggers the re-evaluation.
+     *
+     * arg1 = hardware slot number of the keepalive
+     * obj = {@link Network} that the keepalive is started on.
+     * @hide
+     */
+    public static final int CMD_MONITOR_AUTOMATIC_KEEPALIVE = BASE + 30;
+
     private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
         final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
                 config.legacyTypeName, config.legacySubTypeName);
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index 57cf5e3..90e5e9b 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -16,6 +16,8 @@
 
 package android.net;
 
+import static android.annotation.SystemApi.Client.PRIVILEGED_APPS;
+
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -174,6 +176,27 @@
     public @interface KeepaliveEvent {}
 
     /**
+     * Whether the system automatically toggles keepalive when no TCP connection is open on the VPN.
+     *
+     * If this flag is present, the system will monitor the VPN(s) running on top of the specified
+     * network for open TCP connections. When no such connections are open, it will turn off the
+     * keepalives to conserve battery power. When there is at least one such connection it will
+     * turn on the keepalives to make sure functionality is preserved.
+     *
+     * This only works with {@link NattSocketKeepalive}.
+     * @hide
+     */
+    @SystemApi
+    public static final int FLAG_AUTOMATIC_ON_OFF = 1 << 0;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "FLAG_"}, flag = true, value = {
+            FLAG_AUTOMATIC_ON_OFF
+    })
+    public @interface StartFlags {}
+
+    /**
      * The minimum interval in seconds between keepalive packet transmissions.
      *
      * @hide
@@ -294,13 +317,15 @@
     }
 
     /**
-     * Request that keepalive be started with the given {@code intervalSec}. See
-     * {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an exception
-     * when invoking start or stop of the {@link SocketKeepalive}, a {@link RemoteException} will be
-     * thrown into the {@code executor}. This is typically not important to catch because the remote
-     * party is the system, so if it is not in shape to communicate through binder the system is
-     * probably going down anyway. If the caller cares regardless, it can use a custom
-     * {@link Executor} to catch the {@link RemoteException}.
+     * Request that keepalive be started with the given {@code intervalSec}.
+     *
+     * See {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an
+     * exception when invoking start or stop of the {@link SocketKeepalive}, a
+     * {@link RuntimeException} caused by a {@link RemoteException} will be thrown into the
+     * {@link Executor}. This is typically not important to catch because the remote party is
+     * the system, so if it is not in shape to communicate through binder the system is going
+     * down anyway. If the caller still cares, it can use a custom {@link Executor} to catch the
+     * {@link RuntimeException}.
      *
      * @param intervalSec The target interval in seconds between keepalive packet transmissions.
      *                    The interval should be between 10 seconds and 3600 seconds, otherwise
@@ -308,11 +333,35 @@
      */
     public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
             int intervalSec) {
-        startImpl(intervalSec);
+        startImpl(intervalSec, 0 /* flags */);
+    }
+
+    /**
+     * Request that keepalive be started with the given {@code intervalSec}.
+     *
+     * See {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an
+     * exception when invoking start or stop of the {@link SocketKeepalive}, a
+     * {@link RuntimeException} caused by a {@link RemoteException} will be thrown into the
+     * {@link Executor}. This is typically not important to catch because the remote party is
+     * the system, so if it is not in shape to communicate through binder the system is going
+     * down anyway. If the caller still cares, it can use a custom {@link Executor} to catch the
+     * {@link RuntimeException}.
+     *
+     * @param intervalSec The target interval in seconds between keepalive packet transmissions.
+     *                    The interval should be between 10 seconds and 3600 seconds. Otherwise,
+     *                    the supplied {@link Callback} will see a call to
+     *                    {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}.
+     * @param flags Flags to enable/disable available options on this keepalive.
+     * @hide
+     */
+    @SystemApi(client = PRIVILEGED_APPS)
+    public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
+            int intervalSec, @StartFlags int flags) {
+        startImpl(intervalSec, flags);
     }
 
     /** @hide */
-    protected abstract void startImpl(int intervalSec);
+    protected abstract void startImpl(int intervalSec, @StartFlags int flags);
 
     /**
      * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java
index 7131784..51d805e 100644
--- a/framework/src/android/net/TcpSocketKeepalive.java
+++ b/framework/src/android/net/TcpSocketKeepalive.java
@@ -50,7 +50,11 @@
      *   acknowledgement.
      */
     @Override
-    protected void startImpl(int intervalSec) {
+    protected void startImpl(int intervalSec, int flags) {
+        if (0 != flags) {
+            throw new IllegalArgumentException("Illegal flag value for "
+                    + this.getClass().getSimpleName() + " : " + flags);
+        }
         mExecutor.execute(() -> {
             try {
                 mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback);
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index 39cbaf7..af0b8d8 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -30,9 +30,11 @@
 
 #include "bpf/BpfUtils.h"
 #include "netdbpf/BpfNetworkStats.h"
+#include "netdbpf/NetworkTraceHandler.h"
 
 using android::bpf::bpfGetUidStats;
 using android::bpf::bpfGetIfaceStats;
+using android::bpf::NetworkTraceHandler;
 
 namespace android {
 
@@ -67,7 +69,7 @@
     }
 }
 
-static jlong getTotalStat(JNIEnv* env, jclass clazz, jint type) {
+static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
     Stats stats = {};
 
     if (bpfGetIfaceStats(NULL, &stats) == 0) {
@@ -77,7 +79,7 @@
     }
 }
 
-static jlong getIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
     ScopedUtfChars iface8(env, iface);
     if (iface8.c_str() == NULL) {
         return UNKNOWN;
@@ -92,7 +94,7 @@
     }
 }
 
-static jlong getUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
     Stats stats = {};
 
     if (bpfGetUidStats(uid, &stats) == 0) {
@@ -102,10 +104,15 @@
     }
 }
 
+static void nativeInitNetworkTracing(JNIEnv* env, jclass clazz) {
+    NetworkTraceHandler::InitPerfettoTracing();
+}
+
 static const JNINativeMethod gMethods[] = {
-        {"nativeGetTotalStat", "(I)J", (void*)getTotalStat},
-        {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)getIfaceStat},
-        {"nativeGetUidStat", "(II)J", (void*)getUidStat},
+        {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
+        {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
+        {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat},
+        {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing},
 };
 
 int register_android_server_net_NetworkStatsService(JNIEnv* env) {
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 5b3d314..aa1ee41 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -24,12 +24,19 @@
     host_supported: false,
     header_libs: ["bpf_connectivity_headers"],
     srcs: [
-        "BpfNetworkStats.cpp"
+        "BpfNetworkStats.cpp",
+        "NetworkTraceHandler.cpp",
     ],
     shared_libs: [
         "libbase",
         "liblog",
     ],
+    static_libs: [
+        "libperfetto_client_experimental",
+    ],
+    export_static_lib_headers: [
+        "libperfetto_client_experimental",
+    ],
     export_include_dirs: ["include"],
     cflags: [
         "-Wall",
@@ -54,6 +61,7 @@
     header_libs: ["bpf_connectivity_headers"],
     srcs: [
         "BpfNetworkStatsTest.cpp",
+        "NetworkTraceHandlerTest.cpp",
     ],
     cflags: [
         "-Wall",
@@ -64,10 +72,12 @@
     static_libs: [
         "libgmock",
         "libnetworkstats",
+        "libperfetto_client_experimental",
     ],
     shared_libs: [
         "libbase",
         "liblog",
+        "libandroid_net",
     ],
     compile_multilib: "both",
     multilib: {
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
new file mode 100644
index 0000000..4c37b8d
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "NetworkTrace"
+
+#include "netdbpf/NetworkTraceHandler.h"
+
+#include <arpa/inet.h>
+#include <bpf/BpfUtils.h>
+#include <log/log.h>
+#include <perfetto/config/android/network_trace_config.pbzero.h>
+#include <perfetto/trace/android/network_trace.pbzero.h>
+#include <perfetto/trace/profiling/profile_packet.pbzero.h>
+#include <perfetto/tracing/platform.h>
+#include <perfetto/tracing/tracing.h>
+
+// Note: this is initializing state for a templated Perfetto type that resides
+// in the `perfetto` namespace. This must be defined in the global scope.
+PERFETTO_DEFINE_DATA_SOURCE_STATIC_MEMBERS(android::bpf::NetworkTraceHandler);
+
+namespace android {
+namespace bpf {
+using ::perfetto::protos::pbzero::NetworkPacketEvent;
+using ::perfetto::protos::pbzero::NetworkPacketTraceConfig;
+using ::perfetto::protos::pbzero::TracePacket;
+using ::perfetto::protos::pbzero::TrafficDirection;
+
+// static
+void NetworkTraceHandler::RegisterDataSource() {
+  ALOGD("Registering Perfetto data source");
+  perfetto::DataSourceDescriptor dsd;
+  dsd.set_name("android.network_packets");
+  NetworkTraceHandler::Register(dsd);
+}
+
+// static
+void NetworkTraceHandler::InitPerfettoTracing() {
+  perfetto::TracingInitArgs args = {};
+  args.backends |= perfetto::kSystemBackend;
+  perfetto::Tracing::Initialize(args);
+  NetworkTraceHandler::RegisterDataSource();
+}
+
+NetworkTraceHandler::NetworkTraceHandler()
+    : NetworkTraceHandler([this](const PacketTrace& pkt) {
+        NetworkTraceHandler::Trace(
+            [this, pkt](NetworkTraceHandler::TraceContext ctx) {
+              Fill(pkt, *ctx.NewTracePacket());
+            });
+      }) {}
+
+void NetworkTraceHandler::OnSetup(const SetupArgs& args) {
+  const std::string& raw = args.config->network_packet_trace_config_raw();
+  NetworkPacketTraceConfig::Decoder config(raw);
+
+  mPollMs = config.poll_ms();
+  if (mPollMs < 100) {
+    ALOGI("poll_ms is missing or below the 100ms minimum. Increasing to 100ms");
+    mPollMs = 100;
+  }
+}
+
+void NetworkTraceHandler::OnStart(const StartArgs&) {
+  if (!Start()) return;
+  mTaskRunner = perfetto::Platform::GetDefaultPlatform()->CreateTaskRunner({});
+  Loop();
+}
+
+void NetworkTraceHandler::OnStop(const StopArgs&) {
+  Stop();
+  mTaskRunner.reset();
+}
+
+void NetworkTraceHandler::Loop() {
+  mTaskRunner->PostDelayedTask([this]() { Loop(); }, mPollMs);
+  ConsumeAll();
+}
+
+void NetworkTraceHandler::Fill(const PacketTrace& src, TracePacket& dst) {
+  dst.set_timestamp(src.timestampNs);
+  auto* event = dst.set_network_packet();
+  event->set_direction(src.egress ? TrafficDirection::DIR_EGRESS
+                                  : TrafficDirection::DIR_INGRESS);
+  event->set_length(src.length);
+  event->set_uid(src.uid);
+  event->set_tag(src.tag);
+
+  event->set_local_port(src.egress ? ntohs(src.sport) : ntohs(src.dport));
+  event->set_remote_port(src.egress ? ntohs(src.dport) : ntohs(src.sport));
+
+  event->set_ip_proto(src.ipProto);
+  event->set_tcp_flags(src.tcpFlags);
+
+  char ifname[IF_NAMESIZE] = {};
+  if (if_indextoname(src.ifindex, ifname) == ifname) {
+    event->set_interface(std::string(ifname));
+  } else {
+    event->set_interface("error");
+  }
+}
+
+bool NetworkTraceHandler::Start() {
+  ALOGD("Starting datasource");
+
+  auto status = mConfigurationMap.init(PACKET_TRACE_ENABLED_MAP_PATH);
+  if (!status.ok()) {
+    ALOGW("Failed to bind config map: %s", status.error().message().c_str());
+    return false;
+  }
+
+  auto rb = BpfRingbuf<PacketTrace>::Create(PACKET_TRACE_RINGBUF_PATH);
+  if (!rb.ok()) {
+    ALOGW("Failed to create ringbuf: %s", rb.error().message().c_str());
+    return false;
+  }
+
+  mRingBuffer = std::move(*rb);
+
+  auto res = mConfigurationMap.writeValue(0, true, BPF_ANY);
+  if (!res.ok()) {
+    ALOGW("Failed to enable tracing: %s", res.error().message().c_str());
+    return false;
+  }
+
+  return true;
+}
+
+bool NetworkTraceHandler::Stop() {
+  ALOGD("Stopping datasource");
+
+  auto res = mConfigurationMap.writeValue(0, false, BPF_ANY);
+  if (!res.ok()) {
+    ALOGW("Failed to disable tracing: %s", res.error().message().c_str());
+    return false;
+  }
+
+  mRingBuffer.reset();
+
+  return true;
+}
+
+bool NetworkTraceHandler::ConsumeAll() {
+  if (mRingBuffer == nullptr) {
+    ALOGW("Tracing is not active");
+    return false;
+  }
+
+  base::Result<int> ret = mRingBuffer->ConsumeAll(mCallback);
+  if (!ret.ok()) {
+    ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
new file mode 100644
index 0000000..760ae91
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+#include <android-base/unique_fd.h>
+#include <android/multinetwork.h>
+#include <arpa/inet.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <inttypes.h>
+#include <net/if.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include "netdbpf/NetworkTraceHandler.h"
+
+using ::testing::AllOf;
+using ::testing::AnyOf;
+using ::testing::Each;
+using ::testing::Eq;
+using ::testing::Field;
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+__be16 bindAndListen(int s) {
+  sockaddr_in sin = {.sin_family = AF_INET};
+  socklen_t len = sizeof(sin);
+  if (bind(s, (sockaddr*)&sin, sizeof(sin))) return 0;
+  if (listen(s, 1)) return 0;
+  if (getsockname(s, (sockaddr*)&sin, &len)) return 0;
+  return sin.sin_port;
+}
+
+// This takes tcp flag constants from the standard library and makes them usable
+// with the flags we get from BPF. The standard library flags are big endian
+// whereas the BPF flags are reported in host byte order. BPF also trims the
+// flags down to the 8 single-bit flag bits (fin, syn, rst, etc).
+constexpr inline uint8_t FlagToHost(__be32 be_unix_flags) {
+  return ntohl(be_unix_flags) >> 16;
+}
+
+// Pretty prints all fields for a list of packets (useful for debugging).
+struct PacketPrinter {
+  const std::vector<PacketTrace>& data;
+  static constexpr char kTcpFlagNames[] = "FSRPAUEC";
+
+  friend std::ostream& operator<<(std::ostream& os, const PacketPrinter& d) {
+    os << "Packet count: " << d.data.size();
+    for (const PacketTrace& info : d.data) {
+      os << "\nifidx=" << info.ifindex;
+      os << ", len=" << info.length;
+      os << ", uid=" << info.uid;
+      os << ", tag=" << info.tag;
+      os << ", sport=" << info.sport;
+      os << ", dport=" << info.dport;
+      os << ", direction=" << (info.egress ? "egress" : "ingress");
+      os << ", proto=" << static_cast<int>(info.ipProto);
+      os << ", ip=" << static_cast<int>(info.ipVersion);
+      os << ", flags=";
+      for (int i = 0; i < 8; i++) {
+        os << ((info.tcpFlags & (1 << i)) ? kTcpFlagNames[i] : '.');
+      }
+    }
+    return os;
+  }
+};
+
+class NetworkTraceHandlerTest : public testing::Test {
+ protected:
+  void SetUp() {
+    if (access(PACKET_TRACE_RINGBUF_PATH, R_OK)) {
+      GTEST_SKIP() << "Network tracing is not enabled/loaded on this build";
+    }
+  }
+};
+
+TEST_F(NetworkTraceHandlerTest, PollWhileInactive) {
+  NetworkTraceHandler handler([&](const PacketTrace& pkt) {});
+
+  // One succeed after start and before stop.
+  EXPECT_FALSE(handler.ConsumeAll());
+  ASSERT_TRUE(handler.Start());
+  EXPECT_TRUE(handler.ConsumeAll());
+  ASSERT_TRUE(handler.Stop());
+  EXPECT_FALSE(handler.ConsumeAll());
+}
+
+TEST_F(NetworkTraceHandlerTest, 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) {
+    if (pkt.sport != server_port && pkt.dport != server_port) return;
+    if (pkt.uid != getuid()) return;
+    packets.push_back(pkt);
+  });
+
+  ASSERT_TRUE(handler.Start());
+  const uint32_t kClientTag = 2468;
+  const uint32_t kServerTag = 1357;
+
+  // Go through a typical connection sequence between two v4 sockets using tcp.
+  // This covers connection handshake, shutdown, and one data packet.
+  {
+    android::base::unique_fd clientsocket(socket(AF_INET, SOCK_STREAM, 0));
+    ASSERT_NE(-1, clientsocket) << "Failed to open client socket";
+    ASSERT_EQ(android_tag_socket(clientsocket, kClientTag), 0);
+
+    android::base::unique_fd serversocket(socket(AF_INET, SOCK_STREAM, 0));
+    ASSERT_NE(-1, serversocket) << "Failed to open server socket";
+    ASSERT_EQ(android_tag_socket(serversocket, kServerTag), 0);
+
+    server_port = bindAndListen(serversocket);
+    ASSERT_NE(0, server_port) << "Can't bind to server port";
+
+    sockaddr_in addr = {.sin_family = AF_INET, .sin_port = server_port};
+    ASSERT_EQ(0, connect(clientsocket, (sockaddr*)&addr, sizeof(addr)))
+        << "connect to loopback failed: " << strerror(errno);
+
+    int accepted = accept(serversocket, nullptr, nullptr);
+    ASSERT_NE(-1, accepted) << "accept connection failed: " << strerror(errno);
+
+    const char data[] = "abcdefghijklmnopqrstuvwxyz";
+    EXPECT_EQ(send(clientsocket, data, sizeof(data), 0), sizeof(data))
+        << "failed to send message: " << strerror(errno);
+
+    char buff[100] = {};
+    EXPECT_EQ(recv(accepted, buff, sizeof(buff), 0), sizeof(data))
+        << "failed to receive message: " << strerror(errno);
+
+    EXPECT_EQ(std::string(data), std::string(buff));
+  }
+
+  ASSERT_TRUE(handler.ConsumeAll());
+  ASSERT_TRUE(handler.Stop());
+
+  // There are 12 packets in total (6 messages: each seen by client & server):
+  // 1. Client connects to server with syn
+  // 2. Server responds with syn ack
+  // 3. Client responds with ack
+  // 4. Client sends data with psh ack
+  // 5. Server acks the data packet
+  // 6. Client closes connection with fin ack
+  ASSERT_EQ(packets.size(), 12) << PacketPrinter{packets};
+
+  // All packets should be TCP packets.
+  EXPECT_THAT(packets, Each(Field(&PacketTrace::ipProto, Eq(IPPROTO_TCP))));
+
+  // Packet 1: client requests connection with server.
+  EXPECT_EQ(packets[0].egress, 1) << PacketPrinter{packets};
+  EXPECT_EQ(packets[0].dport, server_port) << PacketPrinter{packets};
+  EXPECT_EQ(packets[0].tag, kClientTag) << PacketPrinter{packets};
+  EXPECT_EQ(packets[0].tcpFlags, FlagToHost(TCP_FLAG_SYN))
+      << PacketPrinter{packets};
+
+  // Packet 2: server receives request from client.
+  EXPECT_EQ(packets[1].egress, 0) << PacketPrinter{packets};
+  EXPECT_EQ(packets[1].dport, server_port) << PacketPrinter{packets};
+  EXPECT_EQ(packets[1].tag, kServerTag) << PacketPrinter{packets};
+  EXPECT_EQ(packets[1].tcpFlags, FlagToHost(TCP_FLAG_SYN))
+      << PacketPrinter{packets};
+
+  // Packet 3: server replies back with syn ack.
+  EXPECT_EQ(packets[2].egress, 1) << PacketPrinter{packets};
+  EXPECT_EQ(packets[2].sport, server_port) << PacketPrinter{packets};
+  EXPECT_EQ(packets[2].tcpFlags, FlagToHost(TCP_FLAG_SYN | TCP_FLAG_ACK))
+      << PacketPrinter{packets};
+
+  // Packet 4: client receives the server's syn ack.
+  EXPECT_EQ(packets[3].egress, 0) << PacketPrinter{packets};
+  EXPECT_EQ(packets[3].sport, server_port) << PacketPrinter{packets};
+  EXPECT_EQ(packets[3].tcpFlags, FlagToHost(TCP_FLAG_SYN | TCP_FLAG_ACK))
+      << PacketPrinter{packets};
+}
+
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
new file mode 100644
index 0000000..c257aa0
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -0,0 +1,84 @@
+/**
+ * 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.
+ */
+
+#pragma once
+
+#include <perfetto/base/task_runner.h>
+#include <perfetto/tracing.h>
+
+#include <string>
+#include <unordered_map>
+
+#include "bpf/BpfMap.h"
+#include "bpf/BpfRingbuf.h"
+
+// For PacketTrace struct definition
+#include "netd.h"
+
+namespace android {
+namespace bpf {
+
+class NetworkTraceHandler : public perfetto::DataSource<NetworkTraceHandler> {
+ public:
+  // Registers this DataSource.
+  static void RegisterDataSource();
+
+  // 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 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();
+
+  // How often to poll the ring buffer, defined by the trace config.
+  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;
+};
+
+}  // namespace bpf
+}  // namespace android
diff --git a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
index 0ea126a..82a4fbd 100644
--- a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
+++ b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.net.TrafficStats;
+import android.os.Build;
 import android.util.Log;
 
 import com.android.modules.utils.build.SdkLevel;
@@ -46,6 +47,15 @@
                     /* allowIsolated= */ false);
             TrafficStats.init(getContext());
         }
+
+        // The following code registers the Perfetto Network Trace Handler on non-user builds.
+        // The enhanced tracing is intended to be used for debugging and diagnosing issues. This
+        // is conditional on the build type rather than `isDebuggable` to match the system_server
+        // selinux rules which only allow the Perfetto connection under the same circumstances.
+        if (SdkLevel.isAtLeastU() && !Build.TYPE.equals("user")) {
+            Log.i(TAG, "Initializing network tracing hooks");
+            NetworkStatsService.nativeInitNetworkTracing();
+        }
     }
 
     @Override
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 84b9f12..5dcf860 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -60,6 +60,7 @@
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.PermissionUtils;
 import com.android.server.connectivity.mdns.ExecutorProvider;
+import com.android.server.connectivity.mdns.MdnsAdvertiser;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
 import com.android.server.connectivity.mdns.MdnsMultinetworkSocketClient;
 import com.android.server.connectivity.mdns.MdnsSearchOptions;
@@ -74,9 +75,15 @@
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -89,10 +96,22 @@
 public class NsdService extends INsdManager.Stub {
     private static final String TAG = "NsdService";
     private static final String MDNS_TAG = "mDnsConnector";
+    /**
+     * Enable discovery using the Java DiscoveryManager, instead of the legacy mdnsresponder
+     * implementation.
+     */
     private static final String MDNS_DISCOVERY_MANAGER_VERSION = "mdns_discovery_manager_version";
     private static final String LOCAL_DOMAIN_NAME = "local";
+    // Max label length as per RFC 1034/1035
+    private static final int MAX_LABEL_LENGTH = 63;
 
-    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+    /**
+     * Enable advertising using the Java MdnsAdvertiser, instead of the legacy mdnsresponder
+     * implementation.
+     */
+    private static final String MDNS_ADVERTISER_VERSION = "mdns_advertiser_version";
+
+    public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
     private static final long CLEANUP_DELAY_MS = 10000;
     private static final int IFACE_IDX_ANY = 0;
 
@@ -106,6 +125,8 @@
     private final MdnsDiscoveryManager mMdnsDiscoveryManager;
     @Nullable
     private final MdnsSocketProvider mMdnsSocketProvider;
+    @Nullable
+    private final MdnsAdvertiser mAdvertiser;
     // WARNING : Accessing these values in any thread is not safe, it must only be changed in the
     // state machine thread. If change this outside state machine, it will need to introduce
     // synchronization.
@@ -345,7 +366,7 @@
                                 mLegacyClientCount -= 1;
                             }
                         }
-                        if (mMdnsDiscoveryManager != null) {
+                        if (mMdnsDiscoveryManager != null || mAdvertiser != null) {
                             maybeStopMonitoringSocketsIfNoActiveRequest();
                         }
                         maybeScheduleStop();
@@ -385,6 +406,20 @@
                                     clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         }
                         break;
+                    case NsdManager.STOP_RESOLUTION:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onStopResolutionFailed(
+                                    clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
+                        }
+                        break;
+                    case NsdManager.REGISTER_SERVICE_CALLBACK:
+                        cInfo = getClientInfoForReply(msg);
+                        if (cInfo != null) {
+                            cInfo.onServiceInfoCallbackRegistrationFailed(
+                                    clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+                        }
+                        break;
                     case NsdManager.DAEMON_CLEANUP:
                         maybeStopDaemon();
                         break;
@@ -446,6 +481,7 @@
                 clientInfo.mClientRequests.delete(clientId);
                 mIdToClientInfoMap.remove(globalId);
                 maybeScheduleStop();
+                maybeStopMonitoringSocketsIfNoActiveRequest();
             }
 
             private void storeListenerMap(int clientId, int transactionId, MdnsListener listener,
@@ -462,6 +498,11 @@
                 maybeStopMonitoringSocketsIfNoActiveRequest();
             }
 
+            private void clearRegisteredServiceInfo(ClientInfo clientInfo) {
+                clientInfo.mRegisteredService = null;
+                clientInfo.mClientIdForServiceUpdates = 0;
+            }
+
             /**
              * Check the given service type is valid and construct it to a service type
              * which can use for discovery / resolution service.
@@ -484,8 +525,31 @@
                 final Matcher matcher = serviceTypePattern.matcher(serviceType);
                 if (!matcher.matches()) return null;
                 return matcher.group(1) == null
-                        ? serviceType + ".local"
-                        : matcher.group(1) + "_sub." + matcher.group(2) + ".local";
+                        ? serviceType
+                        : matcher.group(1) + "_sub." + matcher.group(2);
+            }
+
+            /**
+             * Truncate a service name to up to 63 UTF-8 bytes.
+             *
+             * See RFC6763 4.1.1: service instance names are UTF-8 and up to 63 bytes. Truncating
+             * names used in registerService follows historical behavior (see mdnsresponder
+             * handle_regservice_request).
+             */
+            @NonNull
+            private String truncateServiceName(@NonNull String originalName) {
+                // UTF-8 is at most 4 bytes per character; return early in the common case where
+                // the name can't possibly be over the limit given its string length.
+                if (originalName.length() <= MAX_LABEL_LENGTH / 4) return originalName;
+
+                final Charset utf8 = StandardCharsets.UTF_8;
+                final CharsetEncoder encoder = utf8.newEncoder();
+                final ByteBuffer out = ByteBuffer.allocate(MAX_LABEL_LENGTH);
+                // encode will write as many characters as possible to the out buffer, and just
+                // return an overflow code if there were too many characters (no need to check the
+                // return code here, this method truncates the name on purpose).
+                encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */);
+                return new String(out.array(), 0, out.position(), utf8);
             }
 
             @Override
@@ -523,14 +587,16 @@
                                 break;
                             }
 
+                            final String listenServiceType = serviceType + ".local";
                             maybeStartMonitoringSockets();
                             final MdnsListener listener =
-                                    new DiscoveryListener(clientId, id, info, serviceType);
+                                    new DiscoveryListener(clientId, id, info, listenServiceType);
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                     .setNetwork(info.getNetwork())
                                     .setIsPassiveMode(true)
                                     .build();
-                            mMdnsDiscoveryManager.registerListener(serviceType, listener, options);
+                            mMdnsDiscoveryManager.registerListener(
+                                    listenServiceType, listener, options);
                             storeListenerMap(clientId, id, listener, clientInfo);
                             clientInfo.onDiscoverServicesStarted(clientId, info);
                         } else {
@@ -608,16 +674,36 @@
                             break;
                         }
 
-                        maybeStartDaemon();
                         id = getUniqueId();
-                        if (registerService(id, args.serviceInfo)) {
-                            if (DBG) Log.d(TAG, "Register " + clientId + " " + id);
+                        if (mAdvertiser != null) {
+                            final NsdServiceInfo serviceInfo = args.serviceInfo;
+                            final String serviceType = serviceInfo.getServiceType();
+                            final String registerServiceType = constructServiceType(serviceType);
+                            if (registerServiceType == null) {
+                                Log.e(TAG, "Invalid service type: " + serviceType);
+                                clientInfo.onRegisterServiceFailed(clientId,
+                                        NsdManager.FAILURE_INTERNAL_ERROR);
+                                break;
+                            }
+                            serviceInfo.setServiceType(registerServiceType);
+                            serviceInfo.setServiceName(truncateServiceName(
+                                    serviceInfo.getServiceName()));
+
+                            maybeStartMonitoringSockets();
+                            mAdvertiser.addService(id, serviceInfo);
                             storeRequestMap(clientId, id, clientInfo, msg.what);
-                            // Return success after mDns reports success
                         } else {
-                            unregisterService(id);
-                            clientInfo.onRegisterServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            maybeStartDaemon();
+                            if (registerService(id, args.serviceInfo)) {
+                                if (DBG) Log.d(TAG, "Register " + clientId + " " + id);
+                                storeRequestMap(clientId, id, clientInfo, msg.what);
+                                // Return success after mDns reports success
+                            } else {
+                                unregisterService(id);
+                                clientInfo.onRegisterServiceFailed(
+                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            }
+
                         }
                         break;
                     case NsdManager.UNREGISTER_SERVICE:
@@ -633,11 +719,17 @@
                         }
                         id = clientInfo.mClientIds.get(clientId);
                         removeRequestMap(clientId, id, clientInfo);
-                        if (unregisterService(id)) {
+
+                        if (mAdvertiser != null) {
+                            mAdvertiser.removeService(id);
                             clientInfo.onUnregisterServiceSucceeded(clientId);
                         } else {
-                            clientInfo.onUnregisterServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            if (unregisterService(id)) {
+                                clientInfo.onUnregisterServiceSucceeded(clientId);
+                            } else {
+                                clientInfo.onUnregisterServiceFailed(
+                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            }
                         }
                         break;
                     case NsdManager.RESOLVE_SERVICE: {
@@ -661,15 +753,17 @@
                                         NsdManager.FAILURE_INTERNAL_ERROR);
                                 break;
                             }
+                            final String resolveServiceType = serviceType + ".local";
 
                             maybeStartMonitoringSockets();
                             final MdnsListener listener =
-                                    new ResolutionListener(clientId, id, info, serviceType);
+                                    new ResolutionListener(clientId, id, info, resolveServiceType);
                             final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
                                     .setNetwork(info.getNetwork())
                                     .setIsPassiveMode(true)
                                     .build();
-                            mMdnsDiscoveryManager.registerListener(serviceType, listener, options);
+                            mMdnsDiscoveryManager.registerListener(
+                                    resolveServiceType, listener, options);
                             storeListenerMap(clientId, id, listener, clientInfo);
                         } else {
                             if (clientInfo.mResolvedService != null) {
@@ -689,6 +783,79 @@
                         }
                         break;
                     }
+                    case NsdManager.STOP_RESOLUTION:
+                        if (DBG) Log.d(TAG, "Stop service resolution");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in stop resolution");
+                            break;
+                        }
+
+                        id = clientInfo.mClientIds.get(clientId);
+                        removeRequestMap(clientId, id, clientInfo);
+                        if (stopResolveService(id)) {
+                            clientInfo.onStopResolutionSucceeded(clientId);
+                        } else {
+                            clientInfo.onStopResolutionFailed(
+                                    clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
+                        }
+                        clientInfo.mResolvedService = null;
+                        // TODO: Implement the stop resolution with MdnsDiscoveryManager.
+                        break;
+                    case NsdManager.REGISTER_SERVICE_CALLBACK:
+                        if (DBG) Log.d(TAG, "Register a service callback");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in callback registration");
+                            break;
+                        }
+
+                        if (clientInfo.mRegisteredService != null) {
+                            clientInfo.onServiceInfoCallbackRegistrationFailed(
+                                    clientId, NsdManager.FAILURE_ALREADY_ACTIVE);
+                            break;
+                        }
+
+                        maybeStartDaemon();
+                        id = getUniqueId();
+                        if (resolveService(id, args.serviceInfo)) {
+                            clientInfo.mRegisteredService = new NsdServiceInfo();
+                            clientInfo.mClientIdForServiceUpdates = clientId;
+                            storeRequestMap(clientId, id, clientInfo, msg.what);
+                        } else {
+                            clientInfo.onServiceInfoCallbackRegistrationFailed(
+                                    clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+                        }
+                        break;
+                    case NsdManager.UNREGISTER_SERVICE_CALLBACK:
+                        if (DBG) Log.d(TAG, "Unregister a service callback");
+                        args = (ListenerArgs) msg.obj;
+                        clientInfo = mClients.get(args.connector);
+                        // If the binder death notification for a INsdManagerCallback was received
+                        // before any calls are received by NsdService, the clientInfo would be
+                        // cleared and cause NPE. Add a null check here to prevent this corner case.
+                        if (clientInfo == null) {
+                            Log.e(TAG, "Unknown connector in callback unregistration");
+                            break;
+                        }
+
+                        id = clientInfo.mClientIds.get(clientId);
+                        removeRequestMap(clientId, id, clientInfo);
+                        if (stopResolveService(id)) {
+                            clientInfo.onServiceInfoCallbackUnregistered(clientId);
+                        } else {
+                            Log.e(TAG, "Failed to unregister service info callback");
+                        }
+                        clearRegisteredServiceInfo(clientInfo);
+                        break;
                     case MDNS_SERVICE_EVENT:
                         if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) {
                             return NOT_HANDLED;
@@ -705,6 +872,19 @@
                 return HANDLED;
             }
 
+            private void notifyResolveFailedResult(boolean isListenedToUpdates, int clientId,
+                    ClientInfo clientInfo, int error) {
+                if (isListenedToUpdates) {
+                    clientInfo.onServiceInfoCallbackRegistrationFailed(clientId, error);
+                    clearRegisteredServiceInfo(clientInfo);
+                } else {
+                    // The resolve API always returned FAILURE_INTERNAL_ERROR on error; keep it
+                    // for backwards compatibility.
+                    clientInfo.onResolveServiceFailed(clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                    clientInfo.mResolvedService = null;
+                }
+            }
+
             private boolean handleMDnsServiceEvent(int code, int id, Object obj) {
                 NsdServiceInfo servInfo;
                 ClientInfo clientInfo = mIdToClientInfoMap.get(id);
@@ -739,6 +919,12 @@
                             // interfaces that do not have an associated Network.
                             break;
                         }
+                        if (foundNetId == INetd.DUMMY_NET_ID) {
+                            // Ignore services on the dummy0 interface: they are only seen when
+                            // discovering locally advertised services, and are not reachable
+                            // through that interface.
+                            break;
+                        }
                         setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
                         clientInfo.onServiceFound(clientId, servInfo);
                         break;
@@ -755,6 +941,8 @@
                         // found services on the same interface index and their network at the time
                         setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
                         clientInfo.onServiceLost(clientId, servInfo);
+                        // TODO: also support registered service lost when not discovering
+                        clientInfo.maybeNotifyRegisteredServiceLost(servInfo);
                         break;
                     }
                     case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
@@ -791,10 +979,15 @@
                         String rest = fullName.substring(index);
                         String type = rest.replace(".local.", "");
 
-                        clientInfo.mResolvedService.setServiceName(name);
-                        clientInfo.mResolvedService.setServiceType(type);
-                        clientInfo.mResolvedService.setPort(info.port);
-                        clientInfo.mResolvedService.setTxtRecords(info.txtRecord);
+                        final boolean isListenedToUpdates =
+                                clientId == clientInfo.mClientIdForServiceUpdates;
+                        final NsdServiceInfo serviceInfo = isListenedToUpdates
+                                ? clientInfo.mRegisteredService : clientInfo.mResolvedService;
+
+                        serviceInfo.setServiceName(name);
+                        serviceInfo.setServiceType(type);
+                        serviceInfo.setPort(info.port);
+                        serviceInfo.setTxtRecords(info.txtRecord);
                         // Network will be added after SERVICE_GET_ADDR_SUCCESS
 
                         stopResolveService(id);
@@ -804,9 +997,8 @@
                         if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) {
                             storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
                         } else {
-                            clientInfo.onResolveServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
-                            clientInfo.mResolvedService = null;
+                            notifyResolveFailedResult(isListenedToUpdates, clientId, clientInfo,
+                                    NsdManager.FAILURE_BAD_PARAMETERS);
                         }
                         break;
                     }
@@ -814,17 +1006,17 @@
                         /* NNN resolveId errorCode */
                         stopResolveService(id);
                         removeRequestMap(clientId, id, clientInfo);
-                        clientInfo.mResolvedService = null;
-                        clientInfo.onResolveServiceFailed(
-                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        notifyResolveFailedResult(
+                                clientId == clientInfo.mClientIdForServiceUpdates,
+                                clientId, clientInfo, NsdManager.FAILURE_BAD_PARAMETERS);
                         break;
                     case IMDnsEventListener.SERVICE_GET_ADDR_FAILED:
                         /* NNN resolveId errorCode */
                         stopGetAddrInfo(id);
                         removeRequestMap(clientId, id, clientInfo);
-                        clientInfo.mResolvedService = null;
-                        clientInfo.onResolveServiceFailed(
-                                clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                        notifyResolveFailedResult(
+                                clientId == clientInfo.mClientIdForServiceUpdates,
+                                clientId, clientInfo, NsdManager.FAILURE_BAD_PARAMETERS);
                         break;
                     case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: {
                         /* NNN resolveId hostname ttl addr interfaceIdx netId */
@@ -841,19 +1033,38 @@
                         // If the resolved service is on an interface without a network, consider it
                         // as a failure: it would not be usable by apps as they would need
                         // privileged permissions.
-                        if (netId != NETID_UNSET && serviceHost != null) {
-                            clientInfo.mResolvedService.setHost(serviceHost);
-                            setServiceNetworkForCallback(clientInfo.mResolvedService,
-                                    netId, info.interfaceIdx);
-                            clientInfo.onResolveServiceSucceeded(
-                                    clientId, clientInfo.mResolvedService);
+                        if (clientId == clientInfo.mClientIdForServiceUpdates) {
+                            if (netId != NETID_UNSET && serviceHost != null) {
+                                setServiceNetworkForCallback(clientInfo.mRegisteredService,
+                                        netId, info.interfaceIdx);
+                                final List<InetAddress> addresses =
+                                        clientInfo.mRegisteredService.getHostAddresses();
+                                addresses.add(serviceHost);
+                                clientInfo.mRegisteredService.setHostAddresses(addresses);
+                                clientInfo.onServiceUpdated(
+                                        clientId, clientInfo.mRegisteredService);
+                            } else {
+                                stopGetAddrInfo(id);
+                                removeRequestMap(clientId, id, clientInfo);
+                                clearRegisteredServiceInfo(clientInfo);
+                                clientInfo.onServiceInfoCallbackRegistrationFailed(
+                                        clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+                            }
                         } else {
-                            clientInfo.onResolveServiceFailed(
-                                    clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            if (netId != NETID_UNSET && serviceHost != null) {
+                                clientInfo.mResolvedService.setHost(serviceHost);
+                                setServiceNetworkForCallback(clientInfo.mResolvedService,
+                                        netId, info.interfaceIdx);
+                                clientInfo.onResolveServiceSucceeded(
+                                        clientId, clientInfo.mResolvedService);
+                            } else {
+                                clientInfo.onResolveServiceFailed(
+                                        clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+                            }
+                            stopGetAddrInfo(id);
+                            removeRequestMap(clientId, id, clientInfo);
+                            clientInfo.mResolvedService = null;
                         }
-                        stopGetAddrInfo(id);
-                        removeRequestMap(clientId, id, clientInfo);
-                        clientInfo.mResolvedService = null;
                         break;
                     }
                     default:
@@ -1005,18 +1216,32 @@
         mNsdStateMachine.start();
         mMDnsManager = ctx.getSystemService(MDnsManager.class);
         mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
-        if (deps.isMdnsDiscoveryManagerEnabled(ctx)) {
+
+        final boolean discoveryManagerEnabled = deps.isMdnsDiscoveryManagerEnabled(ctx);
+        final boolean advertiserEnabled = deps.isMdnsAdvertiserEnabled(ctx);
+        if (discoveryManagerEnabled || advertiserEnabled) {
             mMdnsSocketProvider = deps.makeMdnsSocketProvider(ctx, handler.getLooper());
+        } else {
+            mMdnsSocketProvider = null;
+        }
+
+        if (discoveryManagerEnabled) {
             mMdnsSocketClient =
                     new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider);
             mMdnsDiscoveryManager =
                     deps.makeMdnsDiscoveryManager(new ExecutorProvider(), mMdnsSocketClient);
             handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
         } else {
-            mMdnsSocketProvider = null;
             mMdnsSocketClient = null;
             mMdnsDiscoveryManager = null;
         }
+
+        if (advertiserEnabled) {
+            mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
+                    new AdvertiserCallback());
+        } else {
+            mAdvertiser = null;
+        }
     }
 
     /**
@@ -1025,10 +1250,10 @@
     @VisibleForTesting
     public static class Dependencies {
         /**
-         * Check whether or not MdnsDiscoveryManager feature is enabled.
+         * Check whether the MdnsDiscoveryManager feature is enabled.
          *
          * @param context The global context information about an app environment.
-         * @return true if MdnsDiscoveryManager feature is enabled.
+         * @return true if the MdnsDiscoveryManager feature is enabled.
          */
         public boolean isMdnsDiscoveryManagerEnabled(Context context) {
             return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
@@ -1036,6 +1261,17 @@
         }
 
         /**
+         * Check whether the MdnsAdvertiser feature is enabled.
+         *
+         * @param context The global context information about an app environment.
+         * @return true if the MdnsAdvertiser feature is enabled.
+         */
+        public boolean isMdnsAdvertiserEnabled(Context context) {
+            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
+                    MDNS_ADVERTISER_VERSION, false /* defaultEnabled */);
+        }
+
+        /**
          * @see MdnsDiscoveryManager
          */
         public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@@ -1044,6 +1280,15 @@
         }
 
         /**
+         * @see MdnsAdvertiser
+         */
+        public MdnsAdvertiser makeMdnsAdvertiser(
+                @NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
+                @NonNull MdnsAdvertiser.AdvertiserCallback cb) {
+            return new MdnsAdvertiser(looper, socketProvider, cb);
+        }
+
+        /**
          * @see MdnsSocketProvider
          */
         public MdnsSocketProvider makeMdnsSocketProvider(Context context, Looper looper) {
@@ -1101,6 +1346,49 @@
         }
     }
 
+    private class AdvertiserCallback implements MdnsAdvertiser.AdvertiserCallback {
+        @Override
+        public void onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo) {
+            final ClientInfo clientInfo = getClientInfoOrLog(serviceId);
+            if (clientInfo == null) return;
+
+            final int clientId = getClientIdOrLog(clientInfo, serviceId);
+            if (clientId < 0) return;
+
+            // onRegisterServiceSucceeded only has the service name in its info. This aligns with
+            // historical behavior.
+            final NsdServiceInfo cbInfo = new NsdServiceInfo(registeredInfo.getServiceName(), null);
+            clientInfo.onRegisterServiceSucceeded(clientId, cbInfo);
+        }
+
+        @Override
+        public void onRegisterServiceFailed(int serviceId, int errorCode) {
+            final ClientInfo clientInfo = getClientInfoOrLog(serviceId);
+            if (clientInfo == null) return;
+
+            final int clientId = getClientIdOrLog(clientInfo, serviceId);
+            if (clientId < 0) return;
+
+            clientInfo.onRegisterServiceFailed(clientId, errorCode);
+        }
+
+        private ClientInfo getClientInfoOrLog(int serviceId) {
+            final ClientInfo clientInfo = mIdToClientInfoMap.get(serviceId);
+            if (clientInfo == null) {
+                Log.e(TAG, String.format("Callback for service %d has no client", serviceId));
+            }
+            return clientInfo;
+        }
+
+        private int getClientIdOrLog(@NonNull ClientInfo info, int serviceId) {
+            final int clientId = info.getClientId(serviceId);
+            if (clientId < 0) {
+                Log.e(TAG, String.format("Client ID not found for service %d", serviceId));
+            }
+            return clientId;
+        }
+    }
+
     @Override
     public INsdServiceConnector connect(INsdManagerCallback cb) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
@@ -1156,6 +1444,26 @@
         }
 
         @Override
+        public void stopResolution(int listenerKey) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+        }
+
+        @Override
+        public void registerServiceInfoCallback(int listenerKey, NsdServiceInfo serviceInfo) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.REGISTER_SERVICE_CALLBACK, 0, listenerKey,
+                    new ListenerArgs(this, serviceInfo)));
+        }
+
+        @Override
+        public void unregisterServiceInfoCallback(int listenerKey) {
+            mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+                    NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
+                    new ListenerArgs(this, null)));
+        }
+
+        @Override
         public void startDaemon() {
             mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
                     NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
@@ -1316,6 +1624,11 @@
         // The target SDK of this client < Build.VERSION_CODES.S
         private boolean mIsLegacy = false;
 
+        /*** The service that is registered to listen to its updates */
+        private NsdServiceInfo mRegisteredService;
+        /*** The client id that listen to updates */
+        private int mClientIdForServiceUpdates;
+
         private ClientInfo(INsdManagerCallback cb) {
             mCb = cb;
             if (DBG) Log.d(TAG, "New client");
@@ -1364,7 +1677,11 @@
                         stopResolveService(globalId);
                         break;
                     case NsdManager.REGISTER_SERVICE:
-                        unregisterService(globalId);
+                        if (mAdvertiser != null) {
+                            mAdvertiser.removeService(globalId);
+                        } else {
+                            unregisterService(globalId);
+                        }
                         break;
                     default:
                         break;
@@ -1393,6 +1710,18 @@
             return mClientIds.keyAt(idx);
         }
 
+        private void maybeNotifyRegisteredServiceLost(@NonNull NsdServiceInfo info) {
+            if (mRegisteredService == null) return;
+            if (!Objects.equals(mRegisteredService.getServiceName(), info.getServiceName())) return;
+            // Resolved services have a leading dot appended at the beginning of their type, but in
+            // discovered info it's at the end
+            if (!Objects.equals(
+                    mRegisteredService.getServiceType() + ".", "." + info.getServiceType())) {
+                return;
+            }
+            onServiceUpdatedLost(mClientIdForServiceUpdates);
+        }
+
         void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
             try {
                 mCb.onDiscoverServicesStarted(listenerKey, info);
@@ -1488,5 +1817,53 @@
                 Log.e(TAG, "Error calling onResolveServiceSucceeded", e);
             }
         }
+
+        void onStopResolutionFailed(int listenerKey, int error) {
+            try {
+                mCb.onStopResolutionFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onStopResolutionFailed", e);
+            }
+        }
+
+        void onStopResolutionSucceeded(int listenerKey) {
+            try {
+                mCb.onStopResolutionSucceeded(listenerKey);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onStopResolutionSucceeded", e);
+            }
+        }
+
+        void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+            try {
+                mCb.onServiceInfoCallbackRegistrationFailed(listenerKey, error);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onServiceInfoCallbackRegistrationFailed", e);
+            }
+        }
+
+        void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+            try {
+                mCb.onServiceUpdated(listenerKey, info);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onServiceUpdated", e);
+            }
+        }
+
+        void onServiceUpdatedLost(int listenerKey) {
+            try {
+                mCb.onServiceUpdatedLost(listenerKey);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onServiceUpdatedLost", e);
+            }
+        }
+
+        void onServiceInfoCallbackUnregistered(int listenerKey) {
+            try {
+                mCb.onServiceInfoCallbackUnregistered(listenerKey);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error calling onServiceInfoCallbackUnregistered", e);
+            }
+        }
     }
 }
diff --git a/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
index a14b5ad..c616e01 100644
--- a/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
@@ -278,14 +278,23 @@
      * Reset a service to the probing state due to a conflict found on the network.
      */
     public void restartProbingForConflict(int serviceId) {
-        // TODO: implement
+        final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
+        if (probingInfo == null) return;
+
+        mProber.restartForConflict(probingInfo);
     }
 
     /**
      * Rename a service following a conflict found on the network, and restart probing.
+     *
+     * If the service was not registered on this {@link MdnsInterfaceAdvertiser}, this is a no-op.
      */
     public void renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
-        // TODO: implement
+        final MdnsProber.ProbingInfo probingInfo = mRecordRepository.renameServiceForConflict(
+                serviceId, newInfo);
+        if (probingInfo == null) return;
+
+        mProber.restartForConflict(probingInfo);
     }
 
     /**
@@ -319,8 +328,15 @@
                             + packet.additionalRecords.size() + " additional from " + src);
         }
 
-        final MdnsRecordRepository.ReplyInfo answers =
-                mRecordRepository.getReply(packet, src);
+        for (int conflictServiceId : mRecordRepository.getConflictingServices(packet)) {
+            mCbHandler.post(() -> mCb.onServiceConflict(this, conflictServiceId));
+        }
+
+        // Even in case of conflict, add replies for other services. But in general conflicts would
+        // happen when the incoming packet has answer records (not a question), so there will be no
+        // answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
+        // conflicting service is still probing and won't reply either.
+        final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
 
         if (answers == null) return;
         mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/mdns/MdnsProber.java b/service-t/src/com/android/server/mdns/MdnsProber.java
index 2cd9148..669b323 100644
--- a/service-t/src/com/android/server/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/mdns/MdnsProber.java
@@ -33,13 +33,13 @@
  * TODO: implement receiving replies and handling conflicts.
  */
 public class MdnsProber extends MdnsPacketRepeater<MdnsProber.ProbingInfo> {
+    private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
     @NonNull
     private final String mLogTag;
 
     public MdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
             @NonNull MdnsReplySender replySender,
             @NonNull PacketRepeaterCallback<ProbingInfo> cb) {
-        // 3 packets as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
         super(looper, replySender, cb);
         mLogTag = MdnsProber.class.getSimpleName() + "/" + interfaceTag;
     }
@@ -140,4 +140,18 @@
     private void startProbing(@NonNull ProbingInfo info, long delay) {
         startSending(info.getServiceId(), info, delay);
     }
+
+    /**
+     * Restart probing with new service info as a conflict was found.
+     */
+    public void restartForConflict(@NonNull ProbingInfo newInfo) {
+        stop(newInfo.getServiceId());
+
+        /* RFC 6762 8.1: "If fifteen conflicts occur within any ten-second period, then the host
+        MUST wait at least five seconds before each successive additional probe attempt. [...]
+        For very simple devices, a valid way to comply with this requirement is to always wait
+        five seconds after any failed probe attempt before trying again. */
+        // TODO: count 15 conflicts in 10s instead of waiting for 5s every time
+        startProbing(newInfo, CONFLICT_RETRY_DELAY_MS);
+    }
 }
diff --git a/service-t/src/com/android/server/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
index 4b2f553..e975ab4 100644
--- a/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
@@ -43,6 +43,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.TreeMap;
@@ -721,6 +722,55 @@
     }
 
     /**
+     * Get the service IDs of services conflicting with a received packet.
+     */
+    public Set<Integer> getConflictingServices(MdnsPacket packet) {
+        // Avoid allocating a new set for each incoming packet: use an empty set by default.
+        Set<Integer> conflicting = Collections.emptySet();
+        for (MdnsRecord record : packet.answers) {
+            for (int i = 0; i < mServices.size(); i++) {
+                final ServiceRegistration registration = mServices.valueAt(i);
+                if (registration.exiting) continue;
+
+                // Only look for conflicts in service name, as a different service name can be used
+                // if there is a conflict, but there is nothing actionable if any other conflict
+                // happens. In fact probing is only done for the service name in the SRV record.
+                // This means only SRV and TXT records need to be checked.
+                final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
+                if (!Arrays.equals(record.getName(), srvRecord.record.getName())) continue;
+
+                // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
+                // data.
+                if (record instanceof MdnsServiceRecord) {
+                    final MdnsServiceRecord local = srvRecord.record;
+                    final MdnsServiceRecord other = (MdnsServiceRecord) record;
+                    // Note "equals" does not consider TTL or receipt time, as intended here
+                    if (Objects.equals(local, other)) {
+                        continue;
+                    }
+                }
+
+                if (record instanceof MdnsTextRecord) {
+                    final MdnsTextRecord local = registration.txtRecord.record;
+                    final MdnsTextRecord other = (MdnsTextRecord) record;
+                    if (Objects.equals(local, other)) {
+                        continue;
+                    }
+                }
+
+                if (conflicting.size() == 0) {
+                    // Conflict was found: use a mutable set
+                    conflicting = new ArraySet<>();
+                }
+                final int serviceId = mServices.keyAt(i);
+                conflicting.add(serviceId);
+            }
+        }
+
+        return conflicting;
+    }
+
+    /**
      * (Re)set a service to the probing state.
      * @return The {@link MdnsProber.ProbingInfo} to send for probing.
      */
@@ -754,6 +804,21 @@
     }
 
     /**
+     * Rename a service to the newly provided info, following a conflict.
+     *
+     * If the specified service does not exist, this returns null.
+     */
+    @Nullable
+    public MdnsProber.ProbingInfo renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
+        if (!mServices.contains(serviceId)) return null;
+
+        final ServiceRegistration newService = new ServiceRegistration(
+                mDeviceHostname, newInfo);
+        mServices.put(serviceId, newService);
+        return makeProbingInfo(serviceId, newService.srvRecord.record);
+    }
+
+    /**
      * Called when {@link MdnsAdvertiser} sent an advertisement for the given service.
      */
     public void onAdvertisementSent(int serviceId) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 5852a30..4eeaf6b 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -3269,4 +3269,7 @@
     private static native long nativeGetTotalStat(int type);
     private static native long nativeGetIfaceStat(String iface, int type);
     private static native long nativeGetUidStat(int uid, int type);
+
+    /** Initializes and registers the Perfetto Network Trace data source */
+    public static native void nativeInitNetworkTracing();
 }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a7e6a2e..f5c6fb7 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -269,6 +269,7 @@
 import com.android.networkstack.apishim.common.BroadcastOptionsShim;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 import com.android.server.connectivity.AutodestructReference;
+import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
 import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
@@ -843,7 +844,7 @@
 
     private final LocationPermissionChecker mLocationPermissionChecker;
 
-    private final KeepaliveTracker mKeepaliveTracker;
+    private final AutomaticOnOffKeepaliveTracker mKeepaliveTracker;
     private final QosCallbackTracker mQosCallbackTracker;
     private final NetworkNotificationManager mNotifier;
     private final LingerMonitor mLingerMonitor;
@@ -1565,7 +1566,7 @@
         mSettingsObserver = new SettingsObserver(mContext, mHandler);
         registerSettingsCallbacks();
 
-        mKeepaliveTracker = new KeepaliveTracker(mContext, mHandler);
+        mKeepaliveTracker = new AutomaticOnOffKeepaliveTracker(mContext, mHandler);
         mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager);
         mQosCallbackTracker = new QosCallbackTracker(mHandler, mNetworkRequestCounter);
 
@@ -5544,6 +5545,33 @@
                     mKeepaliveTracker.handleStartKeepalive(msg);
                     break;
                 }
+                case NetworkAgent.CMD_MONITOR_AUTOMATIC_KEEPALIVE: {
+                    final Network network = (Network) msg.obj;
+                    final int slot = msg.arg1;
+
+                    boolean networkFound = false;
+                    final ArrayList<NetworkAgentInfo> vpnsRunningOnThisNetwork = new ArrayList<>();
+                    for (NetworkAgentInfo n : mNetworkAgentInfos) {
+                        if (n.network.equals(network)) networkFound = true;
+                        if (n.isVPN() && n.everConnected() && hasUnderlyingNetwork(n, network)) {
+                            vpnsRunningOnThisNetwork.add(n);
+                        }
+                    }
+
+                    // If the network no longer exists, then the keepalive should have been
+                    // cleaned up already. There is no point trying to resume keepalives.
+                    if (!networkFound) return;
+
+                    if (!vpnsRunningOnThisNetwork.isEmpty()) {
+                        mKeepaliveTracker.handleMonitorAutomaticKeepalive(network, slot,
+                                // TODO: check all the VPNs running on top of this network
+                                vpnsRunningOnThisNetwork.get(0).network.netId);
+                    } else {
+                        // If no VPN, then make sure the keepalive is running.
+                        mKeepaliveTracker.handleMaybeResumeKeepalive(network, slot);
+                    }
+                    break;
+                }
                 // Sent by KeepaliveTracker to process an app request on the state machine thread.
                 case NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE: {
                     NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj);
@@ -6217,9 +6245,7 @@
         if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
             handleSetOemNetworkPreference(mOemNetworkPreferences, null);
         }
-        if (!mProfileNetworkPreferences.isEmpty()) {
-            updateProfileAllowedNetworks();
-        }
+        updateProfileAllowedNetworks();
     }
 
     private void onUserRemoved(@NonNull final UserHandle user) {
@@ -9788,20 +9814,23 @@
         enforceKeepalivePermission();
         mKeepaliveTracker.startNattKeepalive(
                 getNetworkAgentInfoForNetwork(network), null /* fd */,
-                intervalSeconds, cb,
-                srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT);
+                intervalSeconds, cb, srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT,
+                // Keep behavior of the deprecated method as it is. Set automaticOnOffKeepalives to
+                // false because there is no way and no plan to configure automaticOnOffKeepalives
+                // in this deprecated method.
+                false /* automaticOnOffKeepalives */);
     }
 
     @Override
     public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId,
             int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr,
-            String dstAddr) {
+            String dstAddr, boolean automaticOnOffKeepalives) {
         try {
             final FileDescriptor fd = pfd.getFileDescriptor();
             mKeepaliveTracker.startNattKeepalive(
                     getNetworkAgentInfoForNetwork(network), fd, resourceId,
                     intervalSeconds, cb,
-                    srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT);
+                    srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT, automaticOnOffKeepalives);
         } finally {
             // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
             // startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately.
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
new file mode 100644
index 0000000..27be545
--- /dev/null
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.SUCCESS;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.INetd;
+import android.net.ISocketKeepaliveCallback;
+import android.net.MarkMaskParcel;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.SocketKeepalive;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.SocketUtils;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.StructNlAttr;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.SocketException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages automatic on/off socket keepalive requests.
+ *
+ * Provides methods to stop and start automatic keepalive requests, and keeps track of keepalives
+ * across all networks. For non-automatic on/off keepalive request, this class just forwards the
+ * requests to KeepaliveTracker. This class is tightly coupled to ConnectivityService. It is not
+ * thread-safe and its handle* methods must be called only from the ConnectivityService handler
+ * thread.
+ */
+public class AutomaticOnOffKeepaliveTracker {
+    private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+    private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
+    private static final String ACTION_TCP_POLLING_ALARM =
+            "com.android.server.connectivity.KeepaliveTracker.TCP_POLLING_ALARM";
+    private static final String EXTRA_NETWORK = "network_id";
+    private static final String EXTRA_SLOT = "slot";
+    private static final long DEFAULT_TCP_POLLING_INTERVAL_MS = 120_000L;
+    private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
+            "automatic_on_off_keepalive_version";
+    /**
+     * States for {@code #AutomaticOnOffKeepalive}.
+     *
+     * A new AutomaticOnOffKeepalive starts with STATE_ENABLED. The system will monitor
+     * the TCP sockets on VPN networks running on top of the specified network, and turn off
+     * keepalive if there is no TCP socket any of the VPN networks. Conversely, it will turn
+     * keepalive back on if any TCP socket is open on any of the VPN networks.
+     *
+     * When there is no TCP socket on any of the VPN networks, the state becomes STATE_SUSPENDED.
+     * The {@link KeepaliveTracker.KeepaliveInfo} object is kept to remember the parameters so it
+     * is possible to resume keepalive later with the same parameters.
+     *
+     * When the system detects some TCP socket is open on one of the VPNs while in STATE_SUSPENDED,
+     * this AutomaticOnOffKeepalive goes to STATE_ENABLED again.
+     *
+     * When finishing keepalive, this object is deleted.
+     */
+    private static final int STATE_ENABLED = 0;
+    private static final int STATE_SUSPENDED = 1;
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_ENABLED,
+            STATE_SUSPENDED
+    })
+    private @interface AutomaticOnOffState {}
+
+    @NonNull
+    private final Handler mConnectivityServiceHandler;
+    @NonNull
+    private final KeepaliveTracker mKeepaliveTracker;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final AlarmManager mAlarmManager;
+
+    /**
+     * The {@code inetDiagReqV2} messages for different IP family.
+     *
+     *   Key: Ip family type.
+     * Value: Bytes array represent the {@code inetDiagReqV2}.
+     *
+     * This should only be accessed in the connectivity service handler thread.
+     */
+    private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
+    private final Dependencies mDependencies;
+    private final INetd mNetd;
+    /**
+     * Keeps track of automatic on/off keepalive requests.
+     * This should be only updated in ConnectivityService handler thread.
+     */
+    private final ArrayList<AutomaticOnOffKeepalive> mAutomaticOnOffKeepalives = new ArrayList<>();
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_TCP_POLLING_ALARM.equals(intent.getAction())) {
+                Log.d(TAG, "Received TCP polling intent");
+                final Network network = intent.getParcelableExtra(EXTRA_NETWORK);
+                final int slot = intent.getIntExtra(EXTRA_SLOT, -1);
+                mConnectivityServiceHandler.obtainMessage(
+                        NetworkAgent.CMD_MONITOR_AUTOMATIC_KEEPALIVE,
+                        slot, 0 , network).sendToTarget();
+            }
+        }
+    };
+
+    private static class AutomaticOnOffKeepalive {
+        @NonNull
+        private final KeepaliveTracker.KeepaliveInfo mKi;
+        @NonNull
+        private final FileDescriptor mFd;
+        @NonNull
+        private final PendingIntent mTcpPollingAlarm;
+        private final int mSlot;
+        @AutomaticOnOffState
+        private int mAutomaticOnOffState = STATE_ENABLED;
+
+        AutomaticOnOffKeepalive(@NonNull KeepaliveTracker.KeepaliveInfo ki,
+                @NonNull Context context) throws InvalidSocketException {
+            this.mKi = Objects.requireNonNull(ki);
+            // A null fd is acceptable in KeepaliveInfo for backward compatibility of
+            // PacketKeepalive API, but it should not happen here because legacy API cannot setup
+            // automatic keepalive.
+            Objects.requireNonNull(ki.mFd);
+
+            // Get the slot from keepalive because the slot information may be missing when the
+            // keepalive is stopped.
+            this.mSlot = ki.getSlot();
+            try {
+                this.mFd = Os.dup(ki.mFd);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot dup fd: ", e);
+                throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+            }
+            mTcpPollingAlarm = createTcpPollingAlarmIntent(
+                    context, ki.getNai().network(), ki.getSlot());
+        }
+
+        public boolean match(Network network, int slot) {
+            return this.mKi.getNai().network().equals(network) && this.mSlot == slot;
+        }
+
+        private static PendingIntent createTcpPollingAlarmIntent(@NonNull Context context,
+                @NonNull Network network, int slot) {
+            final Intent intent = new Intent(ACTION_TCP_POLLING_ALARM);
+            intent.putExtra(EXTRA_NETWORK, network);
+            intent.putExtra(EXTRA_SLOT, slot);
+            return PendingIntent.getBroadcast(
+                    context, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
+        }
+    }
+
+    public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler) {
+        this(context, handler, new Dependencies(context));
+    }
+
+    @VisibleForTesting
+    public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler,
+            @NonNull Dependencies dependencies) {
+        mContext = Objects.requireNonNull(context);
+        mDependencies = Objects.requireNonNull(dependencies);
+        mConnectivityServiceHandler = Objects.requireNonNull(handler);
+        mNetd = mDependencies.getNetd();
+        mKeepaliveTracker = mDependencies.newKeepaliveTracker(
+                mContext, mConnectivityServiceHandler);
+
+        if (SdkLevel.isAtLeastU()) {
+            mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_TCP_POLLING_ALARM),
+                    null, handler);
+        }
+        mAlarmManager = mContext.getSystemService(AlarmManager.class);
+    }
+
+    private void startTcpPollingAlarm(@NonNull PendingIntent alarm) {
+        final long triggerAtMillis =
+                SystemClock.elapsedRealtime() + DEFAULT_TCP_POLLING_INTERVAL_MS;
+        // Setup a non-wake up alarm.
+        mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, triggerAtMillis, alarm);
+    }
+
+    /**
+     * Determine if any state transition is needed for the specific automatic keepalive.
+     */
+    public void handleMonitorAutomaticKeepalive(@NonNull Network network, int slot, int vpnNetId) {
+        final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(network, slot);
+        // This may happen if the keepalive is removed by the app, and the alarm is fired at the
+        // same time.
+        if (autoKi == null) return;
+
+        handleMonitorTcpConnections(autoKi, vpnNetId);
+    }
+
+    /**
+     * Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
+     */
+    private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
+        if (!isAnyTcpSocketConnected(vpnNetId)) {
+            // No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
+            // SUSPENDED.
+            if (ki.mAutomaticOnOffState == STATE_ENABLED) {
+                ki.mAutomaticOnOffState = STATE_SUSPENDED;
+                handleSuspendKeepalive(ki.mKi.mNai, ki.mSlot, SUCCESS);
+            }
+        } else {
+            handleMaybeResumeKeepalive(ki);
+        }
+        // TODO: listen to socket status instead of periodically check.
+        startTcpPollingAlarm(ki.mTcpPollingAlarm);
+    }
+
+    /**
+     * Resume keepalive for this slot on this network, if it wasn't already resumed.
+     */
+    public void handleMaybeResumeKeepalive(@NonNull final Network network, final int slot) {
+        final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(network, slot);
+        // This may happen if the keepalive is removed by the app, and the alarm is fired at
+        // the same time.
+        if (autoKi == null) return;
+        handleMaybeResumeKeepalive(autoKi);
+    }
+
+    private void handleMaybeResumeKeepalive(@NonNull AutomaticOnOffKeepalive autoKi) {
+        if (autoKi.mAutomaticOnOffState == STATE_ENABLED) return;
+        KeepaliveTracker.KeepaliveInfo newKi;
+        try {
+            // Get fd from AutomaticOnOffKeepalive since the fd in the original
+            // KeepaliveInfo should be closed.
+            newKi = autoKi.mKi.withFd(autoKi.mFd);
+        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+            Log.e(TAG, "Fail to construct keepalive", e);
+            mKeepaliveTracker.notifyErrorCallback(autoKi.mKi.mCallback, ERROR_INVALID_SOCKET);
+            return;
+        }
+        autoKi.mAutomaticOnOffState = STATE_ENABLED;
+        handleResumeKeepalive(mConnectivityServiceHandler.obtainMessage(
+                NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
+                autoKi.mAutomaticOnOffState, 0, newKi));
+    }
+
+    private int findAutomaticOnOffKeepaliveIndex(@NonNull Network network, int slot) {
+        ensureRunningOnHandlerThread();
+
+        int index = 0;
+        for (AutomaticOnOffKeepalive ki : mAutomaticOnOffKeepalives) {
+            if (ki.match(network, slot)) {
+                return index;
+            }
+            index++;
+        }
+        return -1;
+    }
+
+    @Nullable
+    private AutomaticOnOffKeepalive findAutomaticOnOffKeepalive(@NonNull Network network,
+            int slot) {
+        ensureRunningOnHandlerThread();
+
+        final int index = findAutomaticOnOffKeepaliveIndex(network, slot);
+        return (index >= 0) ? mAutomaticOnOffKeepalives.get(index) : null;
+    }
+
+    /**
+     * Handle keepalive events from lower layer.
+     *
+     * Forward to KeepaliveTracker.
+     */
+    public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
+        mKeepaliveTracker.handleEventSocketKeepalive(nai, slot, reason);
+    }
+
+    /**
+     * Handle stop all keepalives on the specific network.
+     */
+    public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
+        mKeepaliveTracker.handleStopAllKeepalives(nai, reason);
+        final List<AutomaticOnOffKeepalive> matches =
+                CollectionUtils.filter(mAutomaticOnOffKeepalives, it -> it.mKi.getNai() == nai);
+        for (final AutomaticOnOffKeepalive ki : matches) {
+            cleanupAutoOnOffKeepalive(ki);
+        }
+    }
+
+    /**
+     * Handle start keepalive contained within a message.
+     *
+     * The message is expected to contain a KeepaliveTracker.KeepaliveInfo.
+     */
+    public void handleStartKeepalive(Message message) {
+        mKeepaliveTracker.handleStartKeepalive(message);
+
+        // Add automatic on/off request into list to track its life cycle.
+        final boolean automaticOnOff = message.arg1 != 0
+                && mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION);
+        if (automaticOnOff) {
+            final KeepaliveTracker.KeepaliveInfo ki = (KeepaliveTracker.KeepaliveInfo) message.obj;
+            AutomaticOnOffKeepalive autoKi;
+            try {
+                // CAREFUL : mKeepaliveTracker.handleStartKeepalive will assign |ki.mSlot| after
+                // pulling |ki| from the message. The constructor below will read this member
+                // (through ki.getSlot()) and therefore actively relies on handleStartKeepalive
+                // having assigned this member before this is called.
+                // TODO : clean this up by assigning the slot at the start of this method instead
+                // and ideally removing the mSlot member from KeepaliveInfo.
+                autoKi = new AutomaticOnOffKeepalive(ki, mContext);
+            } catch (SocketKeepalive.InvalidSocketException | IllegalArgumentException e) {
+                Log.e(TAG, "Fail to construct keepalive", e);
+                mKeepaliveTracker.notifyErrorCallback(ki.mCallback, ERROR_INVALID_SOCKET);
+                return;
+            }
+            mAutomaticOnOffKeepalives.add(autoKi);
+            startTcpPollingAlarm(autoKi.mTcpPollingAlarm);
+        }
+    }
+
+    private void handleResumeKeepalive(Message message) {
+        mKeepaliveTracker.handleStartKeepalive(message);
+    }
+
+    private void handleSuspendKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+        mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+    }
+
+    /**
+     * Handle stop keepalives on the specific network with given slot.
+     */
+    public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+        final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(nai.network, slot);
+
+        // Let the original keepalive do the stop first, and then clean up the keepalive if it's an
+        // automatic keepalive.
+        if (autoKi == null || autoKi.mAutomaticOnOffState == STATE_ENABLED) {
+            mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+        }
+
+        // Not an AutomaticOnOffKeepalive.
+        if (autoKi == null) return;
+
+        cleanupAutoOnOffKeepalive(autoKi);
+    }
+
+    private void cleanupAutoOnOffKeepalive(@NonNull final AutomaticOnOffKeepalive autoKi) {
+        ensureRunningOnHandlerThread();
+        mAlarmManager.cancel(autoKi.mTcpPollingAlarm);
+        // Close the duplicated fd that maintains the lifecycle of socket.
+        FileUtils.closeQuietly(autoKi.mFd);
+        mAutomaticOnOffKeepalives.remove(autoKi);
+    }
+
+    /**
+     * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+     * {@link android.net.SocketKeepalive}.
+     *
+     * Forward to KeepaliveTracker.
+     **/
+    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+            @Nullable FileDescriptor fd,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb,
+            @NonNull String srcAddrString,
+            int srcPort,
+            @NonNull String dstAddrString,
+            int dstPort, boolean automaticOnOffKeepalives) {
+        final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd,
+                intervalSeconds, cb, srcAddrString, srcPort, dstAddrString, dstPort);
+        if (null != ki) {
+            mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
+                    // TODO : move ConnectivityService#encodeBool to a static lib.
+                    automaticOnOffKeepalives ? 1 : 0, 0, ki).sendToTarget();
+        }
+    }
+
+    /**
+     * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+     * {@link android.net.SocketKeepalive}.
+     *
+     * Forward to KeepaliveTracker.
+     **/
+    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+            @Nullable FileDescriptor fd,
+            int resourceId,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb,
+            @NonNull String srcAddrString,
+            @NonNull String dstAddrString,
+            int dstPort,
+            boolean automaticOnOffKeepalives) {
+        final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd,
+                resourceId, intervalSeconds, cb, srcAddrString, dstAddrString, dstPort);
+        if (null != ki) {
+            mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
+                    // TODO : move ConnectivityService#encodeBool to a static lib.
+                    automaticOnOffKeepalives ? 1 : 0, 0, ki).sendToTarget();
+        }
+    }
+
+    /**
+     * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+     *
+     * In order to offload keepalive for application correctly, sequence number, ack number and
+     * other fields are needed to form the keepalive packet. Thus, this function synchronously
+     * puts the socket into repair mode to get the necessary information. After the socket has been
+     * put into repair mode, the application cannot access the socket until reverted to normal.
+     * See {@link android.net.SocketKeepalive}.
+     *
+     * Forward to KeepaliveTracker.
+     **/
+    public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+            @NonNull FileDescriptor fd,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb) {
+        final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeTcpKeepaliveInfo(nai, fd,
+                intervalSeconds, cb);
+        if (null != ki) {
+            mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki)
+                    .sendToTarget();
+        }
+    }
+
+    /**
+     * Dump AutomaticOnOffKeepaliveTracker state.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        // TODO: Dump the necessary information for automatic on/off keepalive.
+        mKeepaliveTracker.dump(pw);
+    }
+
+    /**
+     * Check all keepalives on the network are still valid.
+     *
+     * Forward to KeepaliveTracker.
+     */
+    public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
+        mKeepaliveTracker.handleCheckKeepalivesStillValid(nai);
+    }
+
+    @VisibleForTesting
+    boolean isAnyTcpSocketConnected(int netId) {
+        FileDescriptor fd = null;
+
+        try {
+            fd = mDependencies.createConnectedNetlinkSocket();
+
+            // Get network mask
+            final MarkMaskParcel parcel = mNetd.getFwmarkForNetwork(netId);
+            final int networkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
+            final int networkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
+
+            // Send request for each IP family
+            for (final int family : ADDRESS_FAMILIES) {
+                if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
+                    return true;
+                }
+            }
+        } catch (ErrnoException | SocketException | InterruptedIOException | RemoteException e) {
+            Log.e(TAG, "Fail to get socket info via netlink.", e);
+        } finally {
+            SocketUtils.closeSocketQuietly(fd);
+        }
+
+        return false;
+    }
+
+    private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
+            int networkMask) throws ErrnoException, InterruptedIOException {
+        ensureRunningOnHandlerThread();
+        // Build SocketDiag messages and cache it.
+        if (mSockDiagMsg.get(family) == null) {
+            mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
+        }
+        mDependencies.sendRequest(fd, mSockDiagMsg.get(family));
+
+        // Iteration limitation as a protection to avoid possible infinite loops.
+        // DEFAULT_RECV_BUFSIZE could read more than 20 sockets per time. Max iteration
+        // should be enough to go through reasonable TCP sockets in the device.
+        final int maxIteration = 100;
+        int parsingIteration = 0;
+        while (parsingIteration < maxIteration) {
+            final ByteBuffer bytes = mDependencies.recvSockDiagResponse(fd);
+
+            try {
+                while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
+                    final int startPos = bytes.position();
+
+                    final int nlmsgLen = bytes.getInt();
+                    final int nlmsgType = bytes.getShort();
+                    if (isEndOfMessageOrError(nlmsgType)) return false;
+                    // TODO: Parse InetDiagMessage to get uid and dst address information to filter
+                    //  socket via NetlinkMessage.parse.
+
+                    // Skip the header to move to data part.
+                    bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
+
+                    if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
+                        return true;
+                    }
+                }
+            } catch (BufferUnderflowException e) {
+                // The exception happens in random place in either header position or any data
+                // position. Partial bytes from the middle of the byte buffer may not be enough to
+                // clarify, so print out the content before the error to possibly prevent printing
+                // the whole 8K buffer.
+                final int exceptionPos = bytes.position();
+                final String hex = HexDump.dumpHexString(bytes.array(), 0, exceptionPos);
+                Log.e(TAG, "Unexpected socket info parsing: " + hex, e);
+            }
+
+            parsingIteration++;
+        }
+        return false;
+    }
+
+    private boolean isEndOfMessageOrError(int nlmsgType) {
+        return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
+    }
+
+    private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
+            int networkMask) {
+        final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+        return (mark & networkMask) == networkMark;
+    }
+
+    private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
+        final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+        int mark = NetlinkUtils.INIT_MARK_VALUE;
+        // Get socket mark
+        // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
+        //  data.
+        while (bytes.position() < nextMsgOffset) {
+            final StructNlAttr nlattr = StructNlAttr.parse(bytes);
+            if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+                mark = nlattr.getValueAsInteger();
+            }
+        }
+        return mark;
+    }
+
+    private void ensureRunningOnHandlerThread() {
+        if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
+            throw new IllegalStateException(
+                    "Not running on handler thread: " + Thread.currentThread().getName());
+        }
+    }
+
+    /**
+     * Dependencies class for testing.
+     */
+    @VisibleForTesting
+    public static class Dependencies {
+        private final Context mContext;
+
+        public Dependencies(final Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Create a netlink socket connected to the kernel.
+         *
+         * @return fd the fileDescriptor of the socket.
+         */
+        public FileDescriptor createConnectedNetlinkSocket()
+                throws ErrnoException, SocketException {
+            final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
+            NetlinkUtils.connectSocketToNetlink(fd);
+            Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
+                    StructTimeval.fromMillis(IO_TIMEOUT_MS));
+            return fd;
+        }
+
+        /**
+         * Send composed message request to kernel.
+         *
+         * The given FileDescriptor is expected to be created by
+         * {@link #createConnectedNetlinkSocket} or equivalent way.
+         *
+         * @param fd a netlink socket {@code FileDescriptor} connected to the kernel.
+         * @param msg the byte array representing the request message to write to kernel.
+         */
+        public void sendRequest(@NonNull final FileDescriptor fd,
+                @NonNull final byte[] msg)
+                throws ErrnoException, InterruptedIOException {
+            Os.write(fd, msg, 0 /* byteOffset */, msg.length);
+        }
+
+        /**
+         * Get an INetd connector.
+         */
+        public INetd getNetd() {
+            return INetd.Stub.asInterface(
+                    (IBinder) mContext.getSystemService(Context.NETD_SERVICE));
+        }
+
+        /**
+         * Receive the response message from kernel via given {@code FileDescriptor}.
+         * The usage should follow the {@code #sendRequest} call with the same
+         * FileDescriptor.
+         *
+         * The overall response may be large but the individual messages should not be
+         * excessively large(8-16kB) because trying to get the kernel to return
+         * everything in one big buffer is inefficient as it forces the kernel to allocate
+         * large chunks of linearly physically contiguous memory. The usage should iterate the
+         * call of this method until the end of the overall message.
+         *
+         * The default receiving buffer size should be small enough that it is always
+         * processed within the {@link NetlinkUtils#IO_TIMEOUT_MS} timeout.
+         */
+        public ByteBuffer recvSockDiagResponse(@NonNull final FileDescriptor fd)
+                throws ErrnoException, InterruptedIOException {
+            return NetlinkUtils.recvMessage(
+                    fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, NetlinkUtils.IO_TIMEOUT_MS);
+        }
+
+        /**
+         * Construct a new KeepaliveTracker.
+         */
+        public KeepaliveTracker newKeepaliveTracker(@NonNull Context context,
+                @NonNull Handler connectivityserviceHander) {
+            return new KeepaliveTracker(mContext, connectivityserviceHander);
+        }
+
+        /**
+         * Find out if a feature is enabled from DeviceConfig.
+         *
+         * @param name The name of the property to look up.
+         * @return whether the feature is enabled
+         */
+        public boolean isFeatureEnabled(@NonNull final String name) {
+            return DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_CONNECTIVITY, name);
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index 9c36760..03f8f3e 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -18,7 +18,6 @@
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.net.NattSocketKeepalive.NATT_PORT;
-import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
 import static android.net.SocketKeepalive.BINDER_DIED;
 import static android.net.SocketKeepalive.DATA_RECEIVED;
 import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES;
@@ -33,27 +32,15 @@
 import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
 import static android.net.SocketKeepalive.NO_KEEPALIVE;
 import static android.net.SocketKeepalive.SUCCESS;
-import static android.system.OsConstants.AF_INET;
-import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.SOL_SOCKET;
-import static android.system.OsConstants.SO_SNDTIMEO;
-
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
-import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.ConnectivityResources;
-import android.net.INetd;
 import android.net.ISocketKeepaliveCallback;
 import android.net.InetAddresses;
 import android.net.InvalidPacketException;
 import android.net.KeepalivePacketData;
-import android.net.MarkMaskParcel;
 import android.net.NattKeepalivePacketData;
 import android.net.NetworkAgent;
 import android.net.SocketKeepalive.InvalidSocketException;
@@ -67,29 +54,18 @@
 import android.os.RemoteException;
 import android.system.ErrnoException;
 import android.system.Os;
-import android.system.StructTimeval;
 import android.util.Log;
 import android.util.Pair;
-import android.util.SparseArray;
 
 import com.android.connectivity.resources.R;
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.net.module.util.HexDump;
 import com.android.net.module.util.IpUtils;
-import com.android.net.module.util.SocketUtils;
-import com.android.net.module.util.netlink.InetDiagMessage;
-import com.android.net.module.util.netlink.NetlinkUtils;
-import com.android.net.module.util.netlink.StructNlAttr;
 
 import java.io.FileDescriptor;
-import java.io.InterruptedIOException;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
-import java.net.SocketException;
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -107,12 +83,10 @@
     private static final boolean DBG = false;
 
     public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
-    private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
 
     /** Keeps track of keepalive requests. */
     private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
             new HashMap<> ();
-    private final Handler mConnectivityServiceHandler;
     @NonNull
     private final TcpKeepaliveController mTcpController;
     @NonNull
@@ -131,35 +105,17 @@
     // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
     // the number of remaining keepalive slots is less than or equal to the threshold.
     private final int mAllowedUnprivilegedSlotsForUid;
-    /**
-     * The {@code inetDiagReqV2} messages for different IP family.
-     *
-     *   Key: Ip family type.
-     * Value: Bytes array represent the {@code inetDiagReqV2}.
-     *
-     * This should only be accessed in the connectivity service handler thread.
-     */
-    private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
-    private final Dependencies mDependencies;
-    private final INetd mNetd;
 
     public KeepaliveTracker(Context context, Handler handler) {
-        this(context, handler, new Dependencies(context));
-    }
-
-    @VisibleForTesting
-    public KeepaliveTracker(Context context, Handler handler, Dependencies dependencies) {
-        mConnectivityServiceHandler = handler;
         mTcpController = new TcpKeepaliveController(handler);
         mContext = context;
-        mDependencies = dependencies;
-        mSupportedKeepalives = mDependencies.getSupportedKeepalives();
-        mNetd = mDependencies.getNetd();
 
-        final Resources res = mDependencies.newConnectivityResources();
-        mReservedPrivilegedSlots = res.getInteger(
+        mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
+
+        final ConnectivityResources res = new ConnectivityResources(mContext);
+        mReservedPrivilegedSlots = res.get().getInteger(
                 R.integer.config_reservedPrivilegedKeepaliveSlots);
-        mAllowedUnprivilegedSlotsForUid = res.getInteger(
+        mAllowedUnprivilegedSlotsForUid = res.get().getInteger(
                 R.integer.config_allowedUnprivilegedKeepalivePerUid);
     }
 
@@ -171,13 +127,13 @@
      */
     class KeepaliveInfo implements IBinder.DeathRecipient {
         // Bookkeeping data.
-        private final ISocketKeepaliveCallback mCallback;
+        public final ISocketKeepaliveCallback mCallback;
         private final int mUid;
         private final int mPid;
         private final boolean mPrivileged;
-        private final NetworkAgentInfo mNai;
+        public final NetworkAgentInfo mNai;
         private final int mType;
-        private final FileDescriptor mFd;
+        public final FileDescriptor mFd;
 
         public static final int TYPE_NATT = 1;
         public static final int TYPE_TCP = 2;
@@ -285,6 +241,10 @@
             }
         }
 
+        public int getSlot() {
+            return mSlot;
+        }
+
         private int checkNetworkConnected() {
             if (!mNai.networkInfo.isConnectedOrConnecting()) {
                 return ERROR_INVALID_NETWORK;
@@ -457,6 +417,13 @@
         void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) {
             handleStopKeepalive(mNai, mSlot, socketKeepaliveReason);
         }
+
+        /**
+         * Construct a new KeepaliveInfo from existing KeepaliveInfo with a new fd.
+         */
+        public KeepaliveInfo withFd(@NonNull FileDescriptor fd) throws InvalidSocketException {
+            return new KeepaliveInfo(mCallback, mNai, mPacket, mInterval, mType, fd);
+        }
     }
 
     void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
@@ -486,6 +453,9 @@
         return slot;
     }
 
+    /**
+     * Handle start keepalives with the message.
+     */
     public void handleStartKeepalive(Message message) {
         KeepaliveInfo ki = (KeepaliveInfo) message.obj;
         NetworkAgentInfo nai = ki.getNai();
@@ -646,7 +616,8 @@
      * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
      * {@link android.net.SocketKeepalive}.
      **/
-    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+    @Nullable
+    public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai,
             @Nullable FileDescriptor fd,
             int intervalSeconds,
             @NonNull ISocketKeepaliveCallback cb,
@@ -656,7 +627,7 @@
             int dstPort) {
         if (nai == null) {
             notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
-            return;
+            return null;
         }
 
         InetAddress srcAddress, dstAddress;
@@ -665,7 +636,7 @@
             dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
         } catch (IllegalArgumentException e) {
             notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
-            return;
+            return null;
         }
 
         KeepalivePacketData packet;
@@ -674,7 +645,7 @@
                     srcAddress, srcPort, dstAddress, NATT_PORT);
         } catch (InvalidPacketException e) {
             notifyErrorCallback(cb, e.getError());
-            return;
+            return null;
         }
         KeepaliveInfo ki = null;
         try {
@@ -683,15 +654,14 @@
         } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
             Log.e(TAG, "Fail to construct keepalive", e);
             notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
-            return;
+            return null;
         }
-        Log.d(TAG, "Created keepalive: " + ki.toString());
-        mConnectivityServiceHandler.obtainMessage(
-                NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+        Log.d(TAG, "Created keepalive: " + ki);
+        return ki;
     }
 
     /**
-     * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+     * Make a KeepaliveInfo for a TCP socket.
      *
      * In order to offload keepalive for application correctly, sequence number, ack number and
      * other fields are needed to form the keepalive packet. Thus, this function synchronously
@@ -700,13 +670,14 @@
      *
      * See {@link android.net.SocketKeepalive}.
      **/
-    public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+    @Nullable
+    public KeepaliveInfo makeTcpKeepaliveInfo(@Nullable NetworkAgentInfo nai,
             @NonNull FileDescriptor fd,
             int intervalSeconds,
             @NonNull ISocketKeepaliveCallback cb) {
         if (nai == null) {
             notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
-            return;
+            return null;
         }
 
         final TcpKeepalivePacketData packet;
@@ -714,10 +685,10 @@
             packet = TcpKeepaliveController.getTcpKeepalivePacket(fd);
         } catch (InvalidSocketException e) {
             notifyErrorCallback(cb, e.error);
-            return;
+            return null;
         } catch (InvalidPacketException e) {
             notifyErrorCallback(cb, e.getError());
-            return;
+            return null;
         }
         KeepaliveInfo ki = null;
         try {
@@ -726,20 +697,22 @@
         } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
             Log.e(TAG, "Fail to construct keepalive e=" + e);
             notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
-            return;
+            return null;
         }
         Log.d(TAG, "Created keepalive: " + ki.toString());
-        mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+        return ki;
     }
 
-   /**
-    * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is
-    * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the
-    * resource index bound to the {@link UdpEncapsulationSocket} when creating by
-    * {@link com.android.server.IpSecService} to verify whether the given
-    * {@link UdpEncapsulationSocket} is legitimate.
-    **/
-    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+    /**
+     * Make a KeepaliveInfo for an IPSec NAT-T socket.
+     *
+     * This function is identical to {@link #makeNattKeepaliveInfo}, but also takes a
+     * {@code resourceId}, which is the resource index bound to the {@link UdpEncapsulationSocket}
+     * when creating by {@link com.android.server.IpSecService} to verify whether the given
+     * {@link UdpEncapsulationSocket} is legitimate.
+     **/
+    @Nullable
+    public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai,
             @Nullable FileDescriptor fd,
             int resourceId,
             int intervalSeconds,
@@ -750,6 +723,7 @@
         // Ensure that the socket is created by IpSecService.
         if (!isNattKeepaliveSocketValid(fd, resourceId)) {
             notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+            return null;
         }
 
         // Get src port to adopt old API.
@@ -759,10 +733,11 @@
             srcPort = ((InetSocketAddress) srcSockAddr).getPort();
         } catch (ErrnoException e) {
             notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+            return null;
         }
 
         // Forward request to old API.
-        startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
+        return makeNattKeepaliveInfo(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
                 dstAddrString, dstPort);
     }
 
@@ -801,196 +776,4 @@
         }
         pw.decreaseIndent();
     }
-
-    /**
-     * Dependencies class for testing.
-     */
-    @VisibleForTesting
-    public static class Dependencies {
-        private final Context mContext;
-
-        public Dependencies(final Context context) {
-            mContext = context;
-        }
-
-        /**
-         * Create a netlink socket connected to the kernel.
-         *
-         * @return fd the fileDescriptor of the socket.
-         */
-        public FileDescriptor createConnectedNetlinkSocket()
-                throws ErrnoException, SocketException {
-            final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
-            NetlinkUtils.connectSocketToNetlink(fd);
-            Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
-                    StructTimeval.fromMillis(IO_TIMEOUT_MS));
-            return fd;
-        }
-
-        /**
-         * Send composed message request to kernel.
-         *
-         * The given FileDescriptor is expected to be created by
-         * {@link #createConnectedNetlinkSocket} or equivalent way.
-         *
-         * @param fd a netlink socket {@code FileDescriptor} connected to the kernel.
-         * @param msg the byte array representing the request message to write to kernel.
-         */
-        public void sendRequest(@NonNull final FileDescriptor fd,
-                @NonNull final byte[] msg)
-                throws ErrnoException, InterruptedIOException {
-            Os.write(fd, msg, 0 /* byteOffset */, msg.length);
-        }
-
-        /**
-         * Get an INetd connector.
-         */
-        public INetd getNetd() {
-            return INetd.Stub.asInterface(
-                    (IBinder) mContext.getSystemService(Context.NETD_SERVICE));
-        }
-
-        /**
-         * Receive the response message from kernel via given {@code FileDescriptor}.
-         * The usage should follow the {@code #sendRequest} call with the same
-         * FileDescriptor.
-         *
-         * The overall response may be large but the individual messages should not be
-         * excessively large(8-16kB) because trying to get the kernel to return
-         * everything in one big buffer is inefficient as it forces the kernel to allocate
-         * large chunks of linearly physically contiguous memory. The usage should iterate the
-         * call of this method until the end of the overall message.
-         *
-         * The default receiving buffer size should be small enough that it is always
-         * processed within the {@link NetlinkUtils#IO_TIMEOUT_MS} timeout.
-         */
-        public ByteBuffer recvSockDiagResponse(@NonNull final FileDescriptor fd)
-                throws ErrnoException, InterruptedIOException {
-            return NetlinkUtils.recvMessage(
-                    fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, NetlinkUtils.IO_TIMEOUT_MS);
-        }
-
-        /**
-         * Read supported keepalive count for each transport type from overlay resource.
-         */
-        public int[] getSupportedKeepalives() {
-            return KeepaliveUtils.getSupportedKeepalives(mContext);
-        }
-
-        /**
-         * Construct a new Resource from a new ConnectivityResources.
-         */
-        public Resources newConnectivityResources() {
-            final ConnectivityResources resources = new ConnectivityResources(mContext);
-            return resources.get();
-        }
-    }
-
-    private void ensureRunningOnHandlerThread() {
-        if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
-            throw new IllegalStateException(
-                    "Not running on handler thread: " + Thread.currentThread().getName());
-        }
-    }
-
-    @VisibleForTesting
-    boolean isAnyTcpSocketConnected(int netId) {
-        FileDescriptor fd = null;
-
-        try {
-            fd = mDependencies.createConnectedNetlinkSocket();
-
-            // Get network mask
-            final MarkMaskParcel parcel = mNetd.getFwmarkForNetwork(netId);
-            final int networkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
-            final int networkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
-
-            // Send request for each IP family
-            for (final int family : ADDRESS_FAMILIES) {
-                if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
-                    return true;
-                }
-            }
-        } catch (ErrnoException | SocketException | InterruptedIOException | RemoteException e) {
-            Log.e(TAG, "Fail to get socket info via netlink.", e);
-        } finally {
-            SocketUtils.closeSocketQuietly(fd);
-        }
-
-        return false;
-    }
-
-    private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
-            int networkMask) throws ErrnoException, InterruptedIOException {
-        ensureRunningOnHandlerThread();
-        // Build SocketDiag messages and cache it.
-        if (mSockDiagMsg.get(family) == null) {
-            mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
-        }
-        mDependencies.sendRequest(fd, mSockDiagMsg.get(family));
-
-        // Iteration limitation as a protection to avoid possible infinite loops.
-        // DEFAULT_RECV_BUFSIZE could read more than 20 sockets per time. Max iteration
-        // should be enough to go through reasonable TCP sockets in the device.
-        final int maxIteration = 100;
-        int parsingIteration = 0;
-        while (parsingIteration < maxIteration) {
-            final ByteBuffer bytes = mDependencies.recvSockDiagResponse(fd);
-
-            try {
-                while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
-                    final int startPos = bytes.position();
-
-                    final int nlmsgLen = bytes.getInt();
-                    final int nlmsgType = bytes.getShort();
-                    if (isEndOfMessageOrError(nlmsgType)) return false;
-                    // TODO: Parse InetDiagMessage to get uid and dst address information to filter
-                    //  socket via NetlinkMessage.parse.
-
-                    // Skip the header to move to data part.
-                    bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
-                    if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
-                        return true;
-                    }
-                }
-            } catch (BufferUnderflowException e) {
-                // The exception happens in random place in either header position or any data
-                // position. Partial bytes from the middle of the byte buffer may not be enough to
-                // clarify, so print out the content before the error to possibly prevent printing
-                // the whole 8K buffer.
-                final int exceptionPos = bytes.position();
-                final String hex = HexDump.dumpHexString(bytes.array(), 0, exceptionPos);
-                Log.e(TAG, "Unexpected socket info parsing: " + hex, e);
-            }
-
-            parsingIteration++;
-        }
-        return false;
-    }
-
-    private boolean isEndOfMessageOrError(int nlmsgType) {
-        return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
-    }
-
-    private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
-            int networkMask) {
-        final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
-        return (mark & networkMask) == networkMark;
-    }
-
-    private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
-        final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
-        int mark = NetlinkUtils.INIT_MARK_VALUE;
-        // Get socket mark
-        // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
-        //  data.
-        while (bytes.position() < nextMsgOffset) {
-            final StructNlAttr nlattr = StructNlAttr.parse(bytes);
-            if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
-                mark = nlattr.getValueAsInteger();
-            }
-        }
-        return mark;
-    }
 }
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 61b597a..f596b79 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -96,6 +96,7 @@
 import static com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
+import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.MiscAsserts.assertThrows;
@@ -1131,7 +1132,8 @@
 
         final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
                 mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
-        mContext.registerReceiver(receiver, filter);
+        final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
+        mContext.registerReceiver(receiver, filter, flags);
 
         // Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
         final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
@@ -1225,7 +1227,8 @@
                     networkFuture.complete(intent.getParcelableExtra(EXTRA_NETWORK));
                 }
             };
-            mContext.registerReceiver(receiver, filter);
+            final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
+            mContext.registerReceiver(receiver, filter, flags);
 
             final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
             try {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 2b5c305..b7eb009 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -41,7 +41,9 @@
 import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
 import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
 import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveStopped
 import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.StopResolutionFailed
 import android.net.nsd.NsdManager
 import android.net.nsd.NsdManager.DiscoveryListener
 import android.net.nsd.NsdManager.RegistrationListener
@@ -66,15 +68,6 @@
 import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
 import com.android.testutils.waitForIdle
-import org.junit.After
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
 import java.io.File
 import java.net.ServerSocket
 import java.nio.charset.StandardCharsets
@@ -86,6 +79,15 @@
 import kotlin.test.assertNull
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
 
 private const val TAG = "NsdManagerTest"
 private const val TIMEOUT_MS = 2000L
@@ -182,10 +184,10 @@
                 val errorCode: Int
             ) : RegistrationEvent()
 
-            data class ServiceRegistered(override val serviceInfo: NsdServiceInfo)
-                : RegistrationEvent()
-            data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo)
-                : RegistrationEvent()
+            data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
+                    RegistrationEvent()
+            data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
+                    RegistrationEvent()
         }
 
         override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
@@ -208,11 +210,11 @@
     private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
             DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
         sealed class DiscoveryEvent : NsdEvent {
-            data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
-                : DiscoveryEvent()
+            data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+                    DiscoveryEvent()
 
-            data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int)
-                : DiscoveryEvent()
+            data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+                    DiscoveryEvent()
 
             data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
             data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
@@ -259,10 +261,13 @@
     private class NsdResolveRecord : ResolveListener,
             NsdRecord<NsdResolveRecord.ResolveEvent>() {
         sealed class ResolveEvent : NsdEvent {
-            data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int)
-                : ResolveEvent()
+            data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+                    ResolveEvent()
 
             data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+            data class ResolveStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+            data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+                    ResolveEvent()
         }
 
         override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
@@ -272,6 +277,14 @@
         override fun onServiceResolved(si: NsdServiceInfo) {
             add(ServiceResolved(si))
         }
+
+        override fun onResolveStopped(si: NsdServiceInfo) {
+            add(ResolveStopped(si))
+        }
+
+        override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
+            add(StopResolutionFailed(si, err))
+        }
     }
 
     @Before
@@ -739,6 +752,26 @@
                 NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
     }
 
+    @Test
+    fun testStopServiceResolution() {
+        // This test requires shims supporting U+ APIs (NsdManager.stopServiceResolution)
+        assumeTrue(TestUtils.shouldTestUApis())
+
+        val si = NsdServiceInfo()
+        si.serviceType = this@NsdManagerTest.serviceType
+        si.serviceName = this@NsdManagerTest.serviceName
+        si.port = 12345 // Test won't try to connect so port does not matter
+
+        val resolveRecord = NsdResolveRecord()
+        // Try to resolve an unknown service then stop it immediately.
+        // Expected ResolveStopped callback.
+        nsdShim.resolveService(nsdManager, si, { it.run() }, resolveRecord)
+        nsdShim.stopServiceResolution(nsdManager, resolveRecord)
+        val stoppedCb = resolveRecord.expectCallback<ResolveStopped>()
+        assertEquals(si.serviceName, stoppedCb.serviceInfo.serviceName)
+        assertEquals(si.serviceType, stoppedCb.serviceInfo.serviceType)
+    }
+
     /**
      * Register a service and return its registration record.
      */
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index 64355ed..9ce0693 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.net.InetAddresses;
 import android.net.Network;
 import android.os.Build;
 import android.os.Bundle;
@@ -38,6 +39,7 @@
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 @RunWith(DevSdkIgnoreRunner.class)
@@ -45,6 +47,8 @@
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 public class NsdServiceInfoTest {
 
+    private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+    private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
     public final static InetAddress LOCALHOST;
     static {
         // Because test.
@@ -124,6 +128,7 @@
         fullInfo.setServiceType("_kitten._tcp");
         fullInfo.setPort(4242);
         fullInfo.setHost(LOCALHOST);
+        fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
         fullInfo.setNetwork(new Network(123));
         fullInfo.setInterfaceIndex(456);
         checkParcelable(fullInfo);
@@ -139,6 +144,7 @@
         attributedInfo.setServiceType("_kitten._tcp");
         attributedInfo.setPort(4242);
         attributedInfo.setHost(LOCALHOST);
+        fullInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
         attributedInfo.setAttribute("color", "pink");
         attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
         attributedInfo.setAttribute("adorable", (String) null);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 17e769c..c9783ba 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -15760,6 +15760,39 @@
     }
 
     @Test
+    public void testProfileNetworkPreferenceBlocking_addUser() throws Exception {
+        final InOrder inOrder = inOrder(mMockNetd);
+        doReturn(asList(PRIMARY_USER_HANDLE)).when(mUserManager).getUserHandles(anyBoolean());
+
+        // Only one network
+        mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellAgent.connect(true);
+
+        // Verify uid ranges 0~99999 are allowed
+        final ArraySet<UidRange> allowedRanges = new ArraySet<>();
+        allowedRanges.add(PRIMARY_UIDRANGE);
+        final NativeUidRangeConfig config1User = new NativeUidRangeConfig(
+                mCellAgent.getNetwork().netId,
+                toUidRangeStableParcels(allowedRanges),
+                0 /* subPriority */);
+        inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[] { config1User });
+
+        doReturn(asList(PRIMARY_USER_HANDLE, SECONDARY_USER_HANDLE))
+                .when(mUserManager).getUserHandles(anyBoolean());
+        final Intent addedIntent = new Intent(ACTION_USER_ADDED);
+        addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(SECONDARY_USER));
+        processBroadcast(addedIntent);
+
+        // Make sure the allow list has been updated.
+        allowedRanges.add(UidRange.createForUser(SECONDARY_USER_HANDLE));
+        final NativeUidRangeConfig config2Users = new NativeUidRangeConfig(
+                mCellAgent.getNetwork().netId,
+                toUidRangeStableParcels(allowedRanges),
+                0 /* subPriority */);
+        inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[] { config2Users });
+    }
+
+    @Test
     public void testProfileNetworkPreferenceBlocking_changePreference() throws Exception {
         final InOrder inOrder = inOrder(mMockNetd);
         final UserHandle testHandle = setupEnterpriseNetwork();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 1bd49a5..98a8ed2 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -16,7 +16,10 @@
 
 package com.android.server;
 
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
 
 import static com.android.testutils.ContextUtils.mockService;
 
@@ -27,6 +30,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -47,7 +51,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.net.INetd;
-import android.net.InetAddresses;
 import android.net.Network;
 import android.net.mdns.aidl.DiscoveryInfo;
 import android.net.mdns.aidl.GetAddressInfo;
@@ -69,11 +72,13 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.RemoteException;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.server.NsdService.Dependencies;
+import com.android.server.connectivity.mdns.MdnsAdvertiser;
 import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
 import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
 import com.android.server.connectivity.mdns.MdnsServiceInfo;
@@ -96,6 +101,7 @@
 import java.net.UnknownHostException;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Queue;
 
 // TODOs:
@@ -129,6 +135,7 @@
     @Mock MDnsManager mMockMDnsM;
     @Mock Dependencies mDeps;
     @Mock MdnsDiscoveryManager mDiscoveryManager;
+    @Mock MdnsAdvertiser mAdvertiser;
     @Mock MdnsSocketProvider mSocketProvider;
     HandlerThread mThread;
     TestHandler mHandler;
@@ -399,13 +406,42 @@
         final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
         assertEquals(SERVICE_NAME, resolvedService.getServiceName());
         assertEquals("." + SERVICE_TYPE, resolvedService.getServiceType());
-        assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
+        assertEquals(parseNumericAddress(serviceAddress), resolvedService.getHost());
         assertEquals(servicePort, resolvedService.getPort());
         assertNull(resolvedService.getNetwork());
         assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
     }
 
     @Test
+    public void testDiscoverOnBlackholeNetwork() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final DiscoveryListener discListener = mock(DiscoveryListener.class);
+        client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE),
+                eq(0) /* interfaceIdx */);
+        // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
+        // this needs to use a timeout
+        verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+
+        final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
+                discIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_FOUND,
+                SERVICE_NAME,
+                SERVICE_TYPE,
+                DOMAIN_NAME,
+                123 /* interfaceIdx */,
+                INetd.DUMMY_NET_ID); // netId of the blackhole network
+        eventListener.onServiceDiscoveryStatus(discoveryInfo);
+        waitForIdle();
+
+        verify(discListener, never()).onServiceFound(any());
+    }
+
+    @Test
     public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
         final NsdManager client = connectClient(mService);
         final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -572,6 +608,222 @@
                 anyInt()/* interfaceIdx */);
     }
 
+    @Test
+    public void testStopServiceResolution() {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        client.resolveService(request, resolveListener);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        final int resolveId = resolvIdCaptor.getValue();
+        client.stopServiceResolution(resolveListener);
+        waitForIdle();
+
+        verify(mMockMDnsM).stopOperation(resolveId);
+        verify(resolveListener, timeout(TIMEOUT_MS)).onResolveStopped(argThat(ns ->
+                request.getServiceName().equals(ns.getServiceName())
+                        && request.getServiceType().equals(ns.getServiceType())));
+    }
+
+    @Test
+    public void testStopResolutionFailed() {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        client.resolveService(request, resolveListener);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        final int resolveId = resolvIdCaptor.getValue();
+        doReturn(false).when(mMockMDnsM).stopOperation(anyInt());
+        client.stopServiceResolution(resolveListener);
+        waitForIdle();
+
+        verify(mMockMDnsM).stopOperation(resolveId);
+        verify(resolveListener, timeout(TIMEOUT_MS)).onStopResolutionFailed(argThat(ns ->
+                        request.getServiceName().equals(ns.getServiceName())
+                                && request.getServiceType().equals(ns.getServiceType())),
+                eq(FAILURE_OPERATION_NOT_RUNNING));
+    }
+
+    @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testStopResolutionDuringGettingAddress() throws RemoteException {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final ResolveListener resolveListener = mock(ResolveListener.class);
+        client.resolveService(request, resolveListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        // Resolve service successfully.
+        final ResolutionInfo resolutionInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLVED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                SERVICE_FULL_NAME,
+                DOMAIN_NAME,
+                PORT,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+        eventListener.onServiceResolutionStatus(resolutionInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
+                eq(IFACE_IDX_ANY));
+
+        final int getAddrId = getAddrIdCaptor.getValue();
+        client.stopServiceResolution(resolveListener);
+        waitForIdle();
+
+        verify(mMockMDnsM).stopOperation(getAddrId);
+        verify(resolveListener, timeout(TIMEOUT_MS)).onResolveStopped(argThat(ns ->
+                request.getServiceName().equals(ns.getServiceName())
+                        && request.getServiceType().equals(ns.getServiceType())));
+    }
+
+    private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
+            String serviceType, String address, int port, int interfaceIndex, Network network) {
+        assertEquals(serviceName, info.getServiceName());
+        assertEquals(serviceType, info.getServiceType());
+        assertTrue(info.getHostAddresses().contains(parseNumericAddress(address)));
+        assertEquals(port, info.getPort());
+        assertEquals(network, info.getNetwork());
+        assertEquals(interfaceIndex, info.getInterfaceIndex());
+    }
+
+    @Test
+    public void testRegisterAndUnregisterServiceInfoCallback() throws RemoteException {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
+                NsdManager.ServiceInfoCallback.class);
+        client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        // Resolve service successfully.
+        final ResolutionInfo resolutionInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLVED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                SERVICE_FULL_NAME,
+                DOMAIN_NAME,
+                PORT,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+        eventListener.onServiceResolutionStatus(resolutionInfo);
+        waitForIdle();
+
+        final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
+                eq(IFACE_IDX_ANY));
+
+        // First address info
+        final String v4Address = "192.0.2.1";
+        final String v6Address = "2001:db8::";
+        final GetAddressInfo addressInfo1 = new GetAddressInfo(
+                getAddrIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+                SERVICE_FULL_NAME,
+                v4Address,
+                IFACE_IDX_ANY,
+                999 /* netId */);
+        eventListener.onGettingServiceAddressStatus(addressInfo1);
+        waitForIdle();
+
+        final ArgumentCaptor<NsdServiceInfo> updateInfoCaptor =
+                ArgumentCaptor.forClass(NsdServiceInfo.class);
+        verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(1))
+                .onServiceUpdated(updateInfoCaptor.capture());
+        verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(0) /* info */, SERVICE_NAME,
+                "." + SERVICE_TYPE, v4Address, PORT, IFACE_IDX_ANY, new Network(999));
+
+        // Second address info
+        final GetAddressInfo addressInfo2 = new GetAddressInfo(
+                getAddrIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+                SERVICE_FULL_NAME,
+                v6Address,
+                IFACE_IDX_ANY,
+                999 /* netId */);
+        eventListener.onGettingServiceAddressStatus(addressInfo2);
+        waitForIdle();
+
+        verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(2))
+                .onServiceUpdated(updateInfoCaptor.capture());
+        verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(1) /* info */, SERVICE_NAME,
+                "." + SERVICE_TYPE, v6Address, PORT, IFACE_IDX_ANY, new Network(999));
+
+        client.unregisterServiceInfoCallback(serviceInfoCallback);
+        waitForIdle();
+
+        verify(serviceInfoCallback, timeout(TIMEOUT_MS)).onServiceInfoCallbackUnregistered();
+    }
+
+    @Test
+    public void testRegisterServiceCallbackFailed() throws Exception {
+        final NsdManager client = connectClient(mService);
+        final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        final NsdManager.ServiceInfoCallback subscribeListener = mock(
+                NsdManager.ServiceInfoCallback.class);
+        client.registerServiceInfoCallback(request, Runnable::run, subscribeListener);
+        waitForIdle();
+
+        final IMDnsEventListener eventListener = getEventListener();
+        final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+                eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+        // Fail to resolve service.
+        final ResolutionInfo resolutionFailedInfo = new ResolutionInfo(
+                resolvIdCaptor.getValue(),
+                IMDnsEventListener.SERVICE_RESOLUTION_FAILED,
+                null /* serviceName */,
+                null /* serviceType */,
+                null /* domain */,
+                null /* serviceFullName */,
+                null /* domainName */,
+                0 /* port */,
+                new byte[0] /* txtRecord */,
+                IFACE_IDX_ANY);
+        eventListener.onServiceResolutionStatus(resolutionFailedInfo);
+        verify(subscribeListener, timeout(TIMEOUT_MS))
+                .onServiceInfoCallbackRegistrationFailed(eq(FAILURE_BAD_PARAMETERS));
+    }
+
+    @Test
+    public void testUnregisterNotRegisteredCallback() {
+        final NsdManager client = connectClient(mService);
+        final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
+                NsdManager.ServiceInfoCallback.class);
+
+        assertThrows(IllegalArgumentException.class, () ->
+                client.unregisterServiceInfoCallback(serviceInfoCallback));
+    }
+
     private void makeServiceWithMdnsDiscoveryManagerEnabled() {
         doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
         doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any());
@@ -582,6 +834,16 @@
         verify(mDeps).makeMdnsSocketProvider(any(), any());
     }
 
+    private void makeServiceWithMdnsAdvertiserEnabled() {
+        doReturn(true).when(mDeps).isMdnsAdvertiserEnabled(any(Context.class));
+        doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any());
+        doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any());
+
+        mService = makeService();
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), any());
+        verify(mDeps).makeMdnsSocketProvider(any(), any());
+    }
+
     @Test
     public void testMdnsDiscoveryManagerFeature() {
         // Create NsdService w/o feature enabled.
@@ -733,7 +995,7 @@
         assertTrue(info.getAttributes().containsKey("key"));
         assertEquals(1, info.getAttributes().size());
         assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFE}, info.getAttributes().get("key"));
-        assertEquals(InetAddresses.parseNumericAddress(IPV4_ADDRESS), info.getHost());
+        assertEquals(parseNumericAddress(IPV4_ADDRESS), info.getHost());
         assertEquals(network, info.getNetwork());
 
         // Verify the listener has been unregistered.
@@ -742,6 +1004,102 @@
         verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).stopMonitoringSockets();
     }
 
+    @Test
+    public void testAdvertiseWithMdnsAdvertiser() {
+        makeServiceWithMdnsAdvertiserEnabled();
+
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+        regInfo.setHost(parseNumericAddress("192.0.2.123"));
+        regInfo.setPort(12345);
+        regInfo.setAttribute("testattr", "testvalue");
+        regInfo.setNetwork(new Network(999));
+
+        client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+        waitForIdle();
+        verify(mSocketProvider).startMonitoringSockets();
+        final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
+                matches(info, regInfo)));
+
+        // Verify onServiceRegistered callback
+        final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+        cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
+
+        verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(argThat(info -> matches(info,
+                new NsdServiceInfo(regInfo.getServiceName(), null))));
+
+        client.unregisterService(regListener);
+        waitForIdle();
+        verify(mAdvertiser).removeService(idCaptor.getValue());
+        verify(regListener, timeout(TIMEOUT_MS)).onServiceUnregistered(
+                argThat(info -> matches(info, regInfo)));
+        verify(mSocketProvider, timeout(TIMEOUT_MS)).stopMonitoringSockets();
+    }
+
+    @Test
+    public void testAdvertiseWithMdnsAdvertiser_FailedWithInvalidServiceType() {
+        makeServiceWithMdnsAdvertiserEnabled();
+
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
+        regInfo.setHost(parseNumericAddress("192.0.2.123"));
+        regInfo.setPort(12345);
+        regInfo.setAttribute("testattr", "testvalue");
+        regInfo.setNetwork(new Network(999));
+
+        client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+        waitForIdle();
+        verify(mAdvertiser, never()).addService(anyInt(), any());
+
+        verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
+                argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
+    }
+
+    @Test
+    public void testAdvertiseWithMdnsAdvertiser_LongServiceName() {
+        makeServiceWithMdnsAdvertiserEnabled();
+
+        final NsdManager client = connectClient(mService);
+        final RegistrationListener regListener = mock(RegistrationListener.class);
+        // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+        final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+                ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+        verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+
+        final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
+        regInfo.setHost(parseNumericAddress("192.0.2.123"));
+        regInfo.setPort(12345);
+        regInfo.setAttribute("testattr", "testvalue");
+        regInfo.setNetwork(new Network(999));
+
+        client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+        waitForIdle();
+        final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+        // Service name is truncated to 63 characters
+        verify(mAdvertiser).addService(idCaptor.capture(),
+                argThat(info -> info.getServiceName().equals("a".repeat(63))));
+
+        // Verify onServiceRegistered callback
+        final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+        cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
+
+        verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+                argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+    }
+
     private void waitForIdle() {
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
     }
@@ -786,6 +1144,19 @@
         verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon();
     }
 
+    /**
+     * Return true if two service info are the same.
+     *
+     * Useful for argument matchers as {@link NsdServiceInfo} does not implement equals.
+     */
+    private boolean matches(NsdServiceInfo a, NsdServiceInfo b) {
+        return Objects.equals(a.getServiceName(), b.getServiceName())
+                && Objects.equals(a.getServiceType(), b.getServiceType())
+                && Objects.equals(a.getHost(), b.getHost())
+                && Objects.equals(a.getNetwork(), b.getNetwork())
+                && Objects.equals(a.getAttributes(), b.getAttributes());
+    }
+
     public static class TestHandler extends Handler {
         public Message lastMessage;
 
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
index c8a93a6..deb56ef 100644
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -131,6 +131,11 @@
                 Vpn vpn, VpnProfile profile) {
             return mLockdownVpnTracker;
         }
+
+        @Override
+        public @UserIdInt int getMainUserId() {
+            return UserHandle.USER_SYSTEM;
+        }
     }
 
     @Before
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
similarity index 86%
rename from tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java
rename to tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index b55ee67..6c29d6e 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -24,14 +24,12 @@
 import static org.mockito.Mockito.doReturn;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.net.INetd;
 import android.net.MarkMaskParcel;
 import android.os.Build;
 import android.os.HandlerThread;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.connectivity.resources.R;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
@@ -48,21 +46,19 @@
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class KeepaliveTrackerTest {
-    private static final int[] TEST_SUPPORTED_KEEPALIVES = {1, 3, 0, 0, 0, 0, 0, 0, 0};
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+public class AutomaticOnOffKeepaliveTrackerTest {
     private static final int TEST_NETID = 0xA85;
     private static final int TEST_NETID_FWMARK = 0x0A85;
     private static final int OTHER_NETID = 0x1A85;
     private static final int NETID_MASK = 0xffff;
-    private static final int SUPPORTED_SLOT_COUNT = 2;
-    private KeepaliveTracker mKeepaliveTracker;
+    private AutomaticOnOffKeepaliveTracker mAOOKeepaliveTracker;
     private HandlerThread mHandlerThread;
 
     @Mock INetd mNetd;
-    @Mock KeepaliveTracker.Dependencies mDependencies;
+    @Mock AutomaticOnOffKeepaliveTracker.Dependencies mDependencies;
     @Mock Context mCtx;
-    @Mock Resources mResources;
+    @Mock KeepaliveTracker mKeepaliveTracker;
 
     // Hexadecimal representation of a SOCK_DIAG response with tcp info.
     private static final String SOCK_DIAG_TCP_INET_HEX =
@@ -169,51 +165,43 @@
         doReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID_FWMARK)).when(mNetd)
                 .getFwmarkForNetwork(TEST_NETID);
 
-        doReturn(TEST_SUPPORTED_KEEPALIVES).when(mDependencies).getSupportedKeepalives();
-        doReturn(mResources).when(mDependencies).newConnectivityResources();
-        mockResource();
         doNothing().when(mDependencies).sendRequest(any(), any());
 
         mHandlerThread = new HandlerThread("KeepaliveTrackerTest");
         mHandlerThread.start();
-
-        mKeepaliveTracker = new KeepaliveTracker(mCtx, mHandlerThread.getThreadHandler(),
-                mDependencies);
-    }
-
-    private void mockResource() {
-        doReturn(SUPPORTED_SLOT_COUNT).when(mResources).getInteger(
-                R.integer.config_reservedPrivilegedKeepaliveSlots);
-        doReturn(SUPPORTED_SLOT_COUNT).when(mResources).getInteger(
-                R.integer.config_allowedUnprivilegedKeepalivePerUid);
+        doReturn(mKeepaliveTracker).when(mDependencies).newKeepaliveTracker(
+                mCtx, mHandlerThread.getThreadHandler());
+        doReturn(true).when(mDependencies).isFeatureEnabled(any());
+        mAOOKeepaliveTracker = new AutomaticOnOffKeepaliveTracker(
+                mCtx, mHandlerThread.getThreadHandler(), mDependencies);
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
         setupResponseWithSocketExisting();
         assertThrows(IllegalStateException.class,
-                () -> mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
+                () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
         setupResponseWithSocketExisting();
         mHandlerThread.getThreadHandler().post(
-                () -> assertTrue(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+                () -> assertTrue(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
         setupResponseWithSocketExisting();
         mHandlerThread.getThreadHandler().post(
-                () -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
+                () -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
     }
 
     @Test
     public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
         setupResponseWithoutSocketExisting();
         mHandlerThread.getThreadHandler().post(
-                () -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+                () -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
     }
 
     private void setupResponseWithSocketExisting() throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 02b3976..4a806b1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -203,6 +203,59 @@
         verify(replySender).queueReply(mockReply)
     }
 
+    @Test
+    fun testConflict() {
+        addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        doReturn(setOf(TEST_SERVICE_ID_1)).`when`(repository).getConflictingServices(any())
+
+        // Reply obtained with:
+        // scapy.raw(scapy.DNS(
+        //    qd = None,
+        //    an = scapy.DNSRR(type='TXT', rrname='_testservice._tcp.local'))
+        // ).hex().upper()
+        val query = HexDump.hexStringToByteArray("0000010000000001000000000C5F7465737473657276696" +
+                "365045F746370056C6F63616C0000100001000000000000")
+        val src = InetSocketAddress(parseNumericAddress("2001:db8::456"), MdnsConstants.MDNS_PORT)
+        packetHandler.handlePacket(query, query.size, src)
+
+        val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
+        verify(repository).getConflictingServices(packetCaptor.capture())
+
+        packetCaptor.value.let {
+            assertEquals(0, it.questions.size)
+            assertEquals(1, it.answers.size)
+            assertEquals(0, it.authorityRecords.size)
+            assertEquals(0, it.additionalRecords.size)
+
+            assertTrue(it.answers[0] is MdnsTextRecord)
+            assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.answers[0].name)
+        }
+
+        thread.waitForIdle(TIMEOUT_MS)
+        verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1)
+    }
+
+    @Test
+    fun testRestartProbingForConflict() {
+        val mockProbingInfo = mock(ProbingInfo::class.java)
+        doReturn(mockProbingInfo).`when`(repository).setServiceProbing(TEST_SERVICE_ID_1)
+
+        advertiser.restartProbingForConflict(TEST_SERVICE_ID_1)
+
+        verify(prober).restartForConflict(mockProbingInfo)
+    }
+
+    @Test
+    fun testRenameServiceForConflict() {
+        val mockProbingInfo = mock(ProbingInfo::class.java)
+        doReturn(mockProbingInfo).`when`(repository).renameServiceForConflict(
+                TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        advertiser.renameServiceForConflict(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+        verify(prober).restartForConflict(mockProbingInfo)
+    }
+
     private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
             AnnouncementInfo {
         val testProbingInfo = mock(ProbingInfo::class.java)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 597663c..ecc11ec 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -24,6 +24,7 @@
 import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
 import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
 import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import java.net.InetSocketAddress
@@ -400,6 +401,63 @@
                     intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
         ), reply.additionalAnswers)
     }
+
+    @Test
+    fun testGetConflictingServices() {
+        val repository = MdnsRecordRepository(thread.looper, deps)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                    MdnsServiceRecord(
+                            arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+                            0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+                            0 /* servicePriority */, 0 /* serviceWeight */,
+                            TEST_SERVICE_1.port + 1,
+                            TEST_HOSTNAME),
+                    MdnsTextRecord(
+                            arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+                            0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+                            listOf(TextEntry.fromString("somedifferent=entry"))),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
+                repository.getConflictingServices(packet))
+    }
+
+    @Test
+    fun testGetConflictingServices_IdenticalService() {
+        val repository = MdnsRecordRepository(thread.looper, deps)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+
+        val otherTtlMillis = 1234L
+        val packet = MdnsPacket(
+                0 /* flags */,
+                emptyList() /* questions */,
+                listOf(
+                        MdnsServiceRecord(
+                                arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis, 0 /* servicePriority */, 0 /* serviceWeight */,
+                                TEST_SERVICE_1.port,
+                                TEST_HOSTNAME),
+                        MdnsTextRecord(
+                                arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+                                0L /* receiptTimeMillis */, true /* cacheFlush */,
+                                otherTtlMillis, emptyList()),
+                ) /* answers */,
+                emptyList() /* authorityRecords */,
+                emptyList() /* additionalRecords */)
+
+        // Above records are identical to the actual registrations: no conflict
+        assertEquals(emptySet(), repository.getConflictingServices(packet))
+    }
 }
 
 private fun MdnsRecordRepository.initWithService(serviceId: Int, serviceInfo: NsdServiceInfo):
diff --git a/tools/gn2bp/Android.bp.swp b/tools/gn2bp/Android.bp.swp
index ebf1a9b..c881767 100644
--- a/tools/gn2bp/Android.bp.swp
+++ b/tools/gn2bp/Android.bp.swp
@@ -14,6 +14,13 @@
 //
 // This file is automatically generated by gen_android_bp. Do not edit.
 
+// GN: PACKAGE
+package {
+    default_applicable_licenses: [
+        "external_cronet_license",
+    ],
+}
+
 // GN: //components/cronet/android:cronet_api_java
 java_library {
     name: "cronet_aml_api_java",
@@ -22,11 +29,13 @@
     ],
     libs: [
         "androidx.annotation_annotation",
+        "framework-annotations-lib",
     ],
     sdk_version: "module_current",
 }
 
 // GN: //components/cronet/android:cronet_api_java
+// TODO(danstahr): add the API helpers separately after the main API is checked in and thoroughly reviewed
 filegroup {
     name: "cronet_aml_api_sources",
     srcs: [
@@ -52,18 +61,6 @@
         "components/cronet/android/api/src/android/net/http/UploadDataSink.java",
         "components/cronet/android/api/src/android/net/http/UrlRequest.java",
         "components/cronet/android/api/src/android/net/http/UrlResponseInfo.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/ByteArrayCallback.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/ContentTypeParametersParser.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/HttpResponse.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/ImplicitFlowControlCallback.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/InMemoryTransformCallback.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/JsonCallback.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/RedirectHandler.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/RedirectHandlers.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/RequestCompletionListener.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/StringCallback.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/UploadDataProviders.java",
-        "components/cronet/android/api/src/android/net/http/apihelpers/UrlRequestCallbacks.java",
     ],
 }
 
@@ -2462,8 +2459,8 @@
         "third_party/protobuf/src/",
     ],
     cpp_std: "c++17",
-    linker_scripts: [
-        "base/android/library_loader/anchor_functions.lds",
+    ldflags: [
+        "-Wl,--script,external/cronet/base/android/library_loader/anchor_functions.lds",
     ],
     stem: "libcronet.108.0.5359.128",
     target: {
@@ -10645,3 +10642,55 @@
     ],
 }
 
+// GN: LICENSE
+license {
+    name: "external_cronet_license",
+    license_kinds: [
+        "SPDX-license-identifier-AFL-2.0",
+        "SPDX-license-identifier-Apache-2.0",
+        "SPDX-license-identifier-BSD",
+        "SPDX-license-identifier-BSL-1.0",
+        "SPDX-license-identifier-GPL",
+        "SPDX-license-identifier-GPL-2.0",
+        "SPDX-license-identifier-GPL-3.0",
+        "SPDX-license-identifier-ICU",
+        "SPDX-license-identifier-ISC",
+        "SPDX-license-identifier-LGPL",
+        "SPDX-license-identifier-LGPL-2.1",
+        "SPDX-license-identifier-MIT",
+        "SPDX-license-identifier-MPL",
+        "SPDX-license-identifier-MPL-2.0",
+        "SPDX-license-identifier-NCSA",
+        "SPDX-license-identifier-OpenSSL",
+        "SPDX-license-identifier-Unicode-DFS",
+        "legacy_unencumbered",
+    ],
+    license_text: [
+        "LICENSE",
+        "base/third_party/double_conversion/LICENSE",
+        "base/third_party/dynamic_annotations/LICENSE",
+        "base/third_party/icu/LICENSE",
+        "base/third_party/nspr/LICENSE",
+        "base/third_party/superfasthash/LICENSE",
+        "base/third_party/symbolize/LICENSE",
+        "base/third_party/valgrind/LICENSE",
+        "base/third_party/xdg_user_dirs/LICENSE",
+        "net/third_party/quiche/src/LICENSE",
+        "net/third_party/uri_template/LICENSE",
+        "third_party/abseil-cpp/LICENSE",
+        "third_party/ashmem/LICENSE",
+        "third_party/boringssl/src/LICENSE",
+        "third_party/boringssl/src/third_party/fiat/LICENSE",
+        "third_party/boringssl/src/third_party/googletest/LICENSE",
+        "third_party/boringssl/src/third_party/wycheproof_testvectors/LICENSE",
+        "third_party/brotli/LICENSE",
+        "third_party/icu/LICENSE",
+        "third_party/icu/scripts/LICENSE",
+        "third_party/libevent/LICENSE",
+        "third_party/metrics_proto/LICENSE",
+        "third_party/modp_b64/LICENSE",
+        "third_party/protobuf/LICENSE",
+        "third_party/protobuf/third_party/utf8_range/LICENSE",
+    ],
+}
+
diff --git a/tools/gn2bp/gen_android_bp b/tools/gn2bp/gen_android_bp
index e10c415..6ae3609 100755
--- a/tools/gn2bp/gen_android_bp
+++ b/tools/gn2bp/gen_android_bp
@@ -128,12 +128,17 @@
   "-msse4.2",
 ]
 
+def get_linker_script_ldflag(script_path):
+  return f'-Wl,--script,{tree_path}/{script_path}'
+
 # Additional arguments to apply to Android.bp rules.
 additional_args = {
     # TODO: remove if not needed.
     'cronet_aml_components_cronet_android_cronet': [
-        ('linker_scripts', {
-            'base/android/library_loader/anchor_functions.lds',
+        # linker_scripts property is not available in tm-mainline-prod.
+        # So use ldflags to specify linker script.
+        ('ldflags',{
+          get_linker_script_ldflag('base/android/library_loader/anchor_functions.lds'),
         }),
     ],
     'cronet_aml_net_net': [
@@ -370,6 +375,7 @@
     self.min_sdk_version = None
     self.proto = dict()
     self.linker_scripts = set()
+    self.ldflags = set()
     # The genrule_XXX below are properties that must to be propagated back
     # on the module(s) that depend on the genrule.
     self.genrule_headers = set()
@@ -391,6 +397,9 @@
     self.processor_class = None
     self.sdk_version = None
     self.javacflags = set()
+    self.license_kinds = set()
+    self.license_text = set()
+    self.default_applicable_licenses = set()
 
   def to_string(self, output):
     if self.comment:
@@ -437,6 +446,7 @@
     self._output_field(output, 'stubs')
     self._output_field(output, 'proto')
     self._output_field(output, 'linker_scripts')
+    self._output_field(output, 'ldflags')
     self._output_field(output, 'cppflags')
     self._output_field(output, 'libs')
     self._output_field(output, 'stem')
@@ -446,6 +456,9 @@
     self._output_field(output, 'processor_class')
     self._output_field(output, 'sdk_version')
     self._output_field(output, 'javacflags')
+    self._output_field(output, 'license_kinds')
+    self._output_field(output, 'license_text')
+    self._output_field(output, 'default_applicable_licenses')
     if self.rtti:
       self._output_field(output, 'rtti')
 
@@ -1551,8 +1564,12 @@
 
 def create_java_api_module(blueprint, gn):
   source_module = Module('filegroup', module_prefix + 'api_sources', java_api_target_name)
+  # TODO add the API helpers separately after the main API is checked in and thoroughly reviewed
   source_module.srcs.update([gn_utils.label_to_path(source)
-                             for source in get_api_java_sources(gn)])
+                             for source in get_api_java_sources(gn)
+                             if "apihelpers" not in source])
+  source_module.comment += "\n// TODO(danstahr): add the API helpers separately after the main" \
+                           " API is checked in and thoroughly reviewed"
   source_module.srcs.update([
     ':' + create_action_module(blueprint, gn.get_target(dep), 'java_genrule').name
     for dep in get_api_java_actions(gn)])
@@ -1563,6 +1580,7 @@
   java_module.sdk_version = "module_current"
   java_module.libs = {
       "androidx.annotation_annotation",
+      "framework-annotations-lib",
     }
   blueprint.add_module(java_module)
   return java_module
@@ -1647,6 +1665,59 @@
 
   return blueprint
 
+def create_license_module(blueprint):
+  module = Module("license", "external_cronet_license", "LICENSE")
+  module.license_kinds.update({
+      'SPDX-license-identifier-LGPL-2.1',
+      'SPDX-license-identifier-GPL-2.0',
+      'SPDX-license-identifier-MPL',
+      'SPDX-license-identifier-ISC',
+      'SPDX-license-identifier-GPL',
+      'SPDX-license-identifier-AFL-2.0',
+      'SPDX-license-identifier-MPL-2.0',
+      'SPDX-license-identifier-BSD',
+      'SPDX-license-identifier-Apache-2.0',
+      'SPDX-license-identifier-BSL-1.0',
+      'SPDX-license-identifier-LGPL',
+      'SPDX-license-identifier-GPL-3.0',
+      'SPDX-license-identifier-Unicode-DFS',
+      'SPDX-license-identifier-NCSA',
+      'SPDX-license-identifier-OpenSSL',
+      'SPDX-license-identifier-MIT',
+      "SPDX-license-identifier-ICU",
+      'legacy_unencumbered', # public domain
+  })
+  module.license_text.update({
+      "LICENSE",
+      "net/third_party/uri_template/LICENSE",
+      "net/third_party/quiche/src/LICENSE",
+      "base/third_party/symbolize/LICENSE",
+      "base/third_party/superfasthash/LICENSE",
+      "base/third_party/xdg_user_dirs/LICENSE",
+      "base/third_party/double_conversion/LICENSE",
+      "base/third_party/nspr/LICENSE",
+      "base/third_party/dynamic_annotations/LICENSE",
+      "base/third_party/icu/LICENSE",
+      "base/third_party/valgrind/LICENSE",
+      "third_party/brotli/LICENSE",
+      "third_party/protobuf/LICENSE",
+      "third_party/protobuf/third_party/utf8_range/LICENSE",
+      "third_party/metrics_proto/LICENSE",
+      "third_party/boringssl/src/LICENSE",
+      "third_party/boringssl/src/third_party/googletest/LICENSE",
+      "third_party/boringssl/src/third_party/wycheproof_testvectors/LICENSE",
+      "third_party/boringssl/src/third_party/fiat/LICENSE",
+      "third_party/libevent/LICENSE",
+      "third_party/ashmem/LICENSE",
+      "third_party/icu/LICENSE",
+      "third_party/icu/scripts/LICENSE",
+      "third_party/abseil-cpp/LICENSE",
+      "third_party/modp_b64/LICENSE",
+  })
+  default_license = Module("package", "", "PACKAGE")
+  default_license.default_applicable_licenses.add(module.name)
+  blueprint.add_module(module)
+  blueprint.add_module(default_license)
 
 def main():
   parser = argparse.ArgumentParser(
@@ -1698,7 +1769,7 @@
   # Add any proto groups to the blueprint.
   for l_name, t_names in proto_groups.items():
     create_proto_group_modules(blueprint, gn, l_name, t_names)
-
+  create_license_module(blueprint)
   output = [
       """// Copyright (C) 2022 The Android Open Source Project
 //