Merge "Implement conflict detection"
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 51e1e2a..68e3cf1 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -26,10 +26,28 @@
 // as cronet_test_java_defaults may have different values
 // depending on the branch
 
+java_defaults {
+    name: "CronetTestJavaDefaultsEnabled",
+    enabled: true,
+}
+
+java_defaults {
+    name: "CronetTestJavaDefaultsDisabled",
+    enabled: false,
+}
+
+java_defaults {
+    name: "CronetTestJavaDefaults",
+    defaults: [cronet_test_java_defaults],
+}
+
 android_test {
     name: "CtsNetHttpTestCases",
     compile_multilib: "both", // Include both the 32 and 64 bit versions
-    defaults: ["cts_defaults"],
+    defaults: [
+        "CronetTestJavaDefaults",
+        "cts_defaults",
+    ],
     sdk_version: "test_current",
     srcs: [
         "src/**/*.java",
@@ -47,7 +65,7 @@
         "android.test.base",
         "android.test.mock",
         "androidx.annotation_annotation",
-        "framework-cronet",
+        "framework-tethering",
         "org.apache.http.legacy",
     ],
 
diff --git a/Cronet/tests/cts/AndroidManifest.xml b/Cronet/tests/cts/AndroidManifest.xml
index 5a92dea..eaa24aa 100644
--- a/Cronet/tests/cts/AndroidManifest.xml
+++ b/Cronet/tests/cts/AndroidManifest.xml
@@ -25,7 +25,6 @@
 
     <application android:networkSecurityConfig="@xml/network_security_config">
         <uses-library android:name="android.test.runner"/>
-        <uses-library android:name="framework-cronet"/>
     </application>
 
     <instrumentation
diff --git a/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
index 09f880b..598be0e 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
@@ -22,6 +22,10 @@
 
 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.TestStatusListener;
 import android.net.http.cts.util.TestUrlRequestCallback;
@@ -31,10 +35,6 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import org.chromium.net.CronetEngine;
-import org.chromium.net.UrlRequest;
-import org.chromium.net.UrlRequest.Status;
-import org.chromium.net.UrlResponseInfo;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -44,7 +44,7 @@
 public class CronetUrlRequestTest {
     private static final String TAG = CronetUrlRequestTest.class.getSimpleName();
 
-    @NonNull private CronetEngine mCronetEngine;
+    @NonNull private HttpEngine mHttpEngine;
     @NonNull private TestUrlRequestCallback mCallback;
     @NonNull private ConnectivityManager mCm;
     @NonNull private CronetCtsTestServer mTestServer;
@@ -53,19 +53,19 @@
     public void setUp() throws Exception {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
-        CronetEngine.Builder builder = new CronetEngine.Builder(context);
-        builder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 100 * 1024)
-                .enableHttp2(true)
-                // .enableBrotli(true)
-                .enableQuic(true);
-        mCronetEngine = builder.build();
+        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);
     }
 
     @After
     public void tearDown() throws Exception {
-        mCronetEngine.shutdown();
+        mHttpEngine.shutdown();
         mTestServer.shutdown();
     }
 
@@ -78,7 +78,7 @@
     }
 
     private UrlRequest buildUrlRequest(String url) {
-        return mCronetEngine.newUrlRequestBuilder(url, mCallback, mCallback.getExecutor()).build();
+        return mHttpEngine.newUrlRequestBuilder(url, mCallback, mCallback.getExecutor()).build();
     }
 
     @Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt b/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
index 4d26ec0..e526c7d 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt
@@ -16,9 +16,9 @@
 
 package android.net.http.cts.util
 
+import android.net.http.UrlRequest.StatusListener
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
-import org.chromium.net.UrlRequest.StatusListener
 import org.junit.Assert.assertSame
 
 private const val TIMEOUT_MS = 12000L
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java b/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java
index c7143f5..0b9e90f 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java
@@ -24,15 +24,14 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import android.net.http.CallbackException;
+import android.net.http.HttpException;
+import android.net.http.InlineExecutionProhibitedException;
+import android.net.http.UrlRequest;
+import android.net.http.UrlResponseInfo;
 import android.os.ConditionVariable;
 import android.os.StrictMode;
 
-import org.chromium.net.CallbackException;
-import org.chromium.net.CronetException;
-import org.chromium.net.InlineExecutionProhibitedException;
-import org.chromium.net.UrlRequest;
-import org.chromium.net.UrlResponseInfo;
-
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.concurrent.ExecutorService;
@@ -50,7 +49,7 @@
     public ArrayList<UrlResponseInfo> mRedirectResponseInfoList = new ArrayList<>();
     public ArrayList<String> mRedirectUrlList = new ArrayList<>();
     public UrlResponseInfo mResponseInfo;
-    public CronetException mError;
+    public HttpException mError;
 
     public ResponseStep mResponseStep = ResponseStep.NOTHING;
 
@@ -89,7 +88,7 @@
     // Signaled on each step when mAutoAdvance is false.
     private final ConditionVariable mStepBlock = new ConditionVariable();
 
-    // Executor Service for Cronet callbacks.
+    // Executor Service for Http callbacks.
     private final ExecutorService mExecutorService;
     private Thread mExecutorThread;
 
@@ -349,7 +348,7 @@
     }
 
     @Override
-    public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
+    public void onFailed(UrlRequest request, UrlResponseInfo info, HttpException error) {
         // If the failure is because of prohibited direct execution, the test shouldn't fail
         // since the request already did.
         if (error.getCause() instanceof InlineExecutionProhibitedException) {
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index ed841b8..5532853 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -192,14 +192,17 @@
     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
@@ -231,18 +234,27 @@
     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 669efc9..d89bfa9 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -38,4 +38,8 @@
     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 a28fd7d..5533154 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -33,4 +33,6 @@
     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 1a5a667..122e3a0 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -254,6 +254,20 @@
     /** @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;
 
@@ -282,6 +296,12 @@
         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 */
@@ -617,6 +637,26 @@
         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);
+        }
     }
 
     /**
@@ -646,6 +686,14 @@
      */
     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 = {
@@ -654,6 +702,15 @@
     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 {
 
@@ -727,6 +784,54 @@
                 @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
     class ServiceHandler extends Handler {
         ServiceHandler(Looper looper) {
@@ -827,6 +932,23 @@
                     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;
@@ -1138,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);
     }
@@ -1150,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);
@@ -1185,6 +1321,62 @@
         }
     }
 
+    /**
+     * 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/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index ce105ce..b361720 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -83,6 +83,7 @@
 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;
 
@@ -412,6 +413,13 @@
                                     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;
@@ -490,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.
@@ -793,6 +806,56 @@
                         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;
@@ -809,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);
@@ -859,6 +935,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:
@@ -895,10 +973,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);
@@ -908,9 +991,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;
                     }
@@ -918,17 +1000,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 */
@@ -945,19 +1027,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:
@@ -1343,6 +1444,20 @@
         }
 
         @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)));
@@ -1503,6 +1618,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");
@@ -1584,6 +1704,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);
@@ -1695,5 +1827,37 @@
                 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/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a7e6a2e..87ac0a8 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -101,6 +101,7 @@
 import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
 import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
+import static com.android.server.connectivity.KeepaliveTracker.PERMISSION;
 
 import static java.util.Map.Entry;
 
@@ -269,6 +270,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;
@@ -276,7 +278,6 @@
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
 import com.android.server.connectivity.DscpPolicyTracker;
 import com.android.server.connectivity.FullScore;
-import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.LingerMonitor;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.MultinetworkPolicyTracker;
@@ -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);
 
@@ -2998,7 +2999,7 @@
     }
 
     private void enforceKeepalivePermission() {
-        mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
+        mContext.enforceCallingOrSelfPermission(PERMISSION, "ConnectivityService");
     }
 
     private boolean checkLocalMacAddressPermission(int pid, int uid) {
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..85ec5e3
--- /dev/null
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -0,0 +1,385 @@
+/*
+ * 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.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.net.INetd;
+import android.net.ISocketKeepaliveCallback;
+import android.net.MarkMaskParcel;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+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.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.net.SocketException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+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 bypass the requests
+ * and send to KeepaliveTrakcer. 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};
+
+    @NonNull
+    private final Handler mConnectivityServiceHandler;
+    @NonNull
+    private final KeepaliveTracker mKeepaliveTracker;
+    @NonNull
+    private final Context mContext;
+
+    /**
+     * 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 AutomaticOnOffKeepaliveTracker(Context context, 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 = dependencies;
+        this.mConnectivityServiceHandler = Objects.requireNonNull(handler);
+        mNetd = mDependencies.getNetd();
+        mKeepaliveTracker = mDependencies.newKeepaliveTracker(
+                mContext, mConnectivityServiceHandler);
+    }
+
+    /**
+     * Handle keepalive events from lower layer.
+     */
+    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);
+    }
+
+    /**
+     *  Handle start keepalives with the message.
+     *
+     *  The message is expected to be a KeepaliveTracker.KeepaliveInfo.
+     */
+    public void handleStartKeepalive(Message message) {
+        mKeepaliveTracker.handleStartKeepalive(message);
+    }
+
+    /**
+     * Handle stop keepalives on the specific network with given slot.
+     */
+    public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+        mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+    }
+
+    /**
+     * 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 FileDescriptor fd,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb,
+            @NonNull String srcAddrString,
+            int srcPort,
+            @NonNull String dstAddrString,
+            int dstPort) {
+        mKeepaliveTracker.startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString,
+                srcPort, dstAddrString, dstPort);
+    }
+
+    /**
+     * 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 FileDescriptor fd,
+            int resourceId,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb,
+            @NonNull String srcAddrString,
+            @NonNull String dstAddrString,
+            int dstPort) {
+        mKeepaliveTracker.startNattKeepalive(nai, fd, resourceId, intervalSeconds, cb,
+                srcAddrString, dstAddrString, dstPort);
+    }
+
+    /**
+     * 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}.
+     **/
+    public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+            @NonNull FileDescriptor fd,
+            int intervalSeconds,
+            @NonNull ISocketKeepaliveCallback cb) {
+        mKeepaliveTracker.startTcpKeepalive(nai, fd, intervalSeconds, cb);
+    }
+
+    /**
+     * Dump AutomaticOnOffKeepaliveTracker state.
+     */
+    public void dump(IndentingPrintWriter pw) {
+        // TODO:  Dump the necessary information for automatic on/off keepalive.
+        mKeepaliveTracker.dump(pw);
+    }
+
+    /**
+     * Check all keeplaives on the network are still valid.
+     */
+    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);
+        }
+    }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index 9c36760..23fdfd4 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -33,27 +33,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 +55,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,7 +84,6 @@
     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 =
@@ -131,35 +107,18 @@
     // 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);
     }
 
@@ -801,196 +760,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/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/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index a8f8121..c7a6639 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -17,6 +17,7 @@
 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;
 
@@ -29,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;
@@ -666,6 +668,133 @@
                         && 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());
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..8c9cfe8 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,42 @@
         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());
+        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 {