Move NetworkMonitor to NetworkStack

Bug: b/112869080
Test: atest FrameworksNetTests NetworkStackTests
Change-Id: I1b8b6a3f4390adbabf92fb9e48da61c47b08b2ec
diff --git a/Android.bp b/Android.bp
index 565f4e0..8b3e259 100644
--- a/Android.bp
+++ b/Android.bp
@@ -824,8 +824,11 @@
     name: "networkstack-aidl-interfaces",
     local_include_dir: "core/java",
     srcs: [
+        "core/java/android/net/INetworkMonitor.aidl",
+        "core/java/android/net/INetworkMonitorCallbacks.aidl",
         "core/java/android/net/INetworkStackConnector.aidl",
         "core/java/android/net/INetworkStackStatusCallback.aidl",
+        "core/java/android/net/PrivateDnsConfigParcel.aidl",
         "core/java/android/net/dhcp/DhcpServingParamsParcel.aidl",
         "core/java/android/net/dhcp/IDhcpServer.aidl",
         "core/java/android/net/dhcp/IDhcpServerCallbacks.aidl",
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 436b4a1..abc00fe 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -2051,6 +2051,16 @@
         return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
     }
 
+    /** @hide */
+    public NetworkRequest getDefaultRequest() {
+        try {
+            // This is not racy as the default request is final in ConnectivityService.
+            return mService.getDefaultRequest();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     /* TODO: These permissions checks don't belong in client-side code. Move them to
      * services.jar, possibly in com.android.server.net. */
 
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index e7d441d..da5d96e 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -167,6 +167,8 @@
 
     int getMultipathPreference(in Network Network);
 
+    NetworkRequest getDefaultRequest();
+
     int getRestoreDefaultNetworkDelay(int networkType);
 
     boolean addVpnAddress(String address, int prefixLength);
diff --git a/core/java/android/net/INetworkMonitor.aidl b/core/java/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..41f969a
--- /dev/null
+++ b/core/java/android/net/INetworkMonitor.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.net.PrivateDnsConfigParcel;
+
+/** @hide */
+oneway interface INetworkMonitor {
+    // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
+    // The network should be used as a default internet connection.  It was found to be:
+    // 1. a functioning network providing internet access, or
+    // 2. a captive portal and the user decided to use it as is.
+    const int NETWORK_TEST_RESULT_VALID = 0;
+
+    // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
+    // The network should not be used as a default internet connection.  It was found to be:
+    // 1. a captive portal and the user is prompted to sign-in, or
+    // 2. a captive portal and the user did not want to use it, or
+    // 3. a broken network (e.g. DNS failed, connect failed, HTTP request failed).
+    const int NETWORK_TEST_RESULT_INVALID = 1;
+
+    void start();
+    void launchCaptivePortalApp();
+    void forceReevaluation(int uid);
+    void notifyPrivateDnsChanged(in PrivateDnsConfigParcel config);
+    void notifyDnsResponse(int returnCode);
+    void notifySystemReady();
+    void notifyNetworkConnected();
+    void notifyNetworkDisconnected();
+    void notifyLinkPropertiesChanged();
+    void notifyNetworkCapabilitiesChanged();
+}
\ No newline at end of file
diff --git a/core/java/android/net/INetworkMonitorCallbacks.aidl b/core/java/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..0bc2575
--- /dev/null
+++ b/core/java/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.INetworkMonitor;
+import android.net.PrivateDnsConfigParcel;
+
+/** @hide */
+oneway interface INetworkMonitorCallbacks {
+    void onNetworkMonitorCreated(in INetworkMonitor networkMonitor);
+    void notifyNetworkTested(int testResult, @nullable String redirectUrl);
+    void notifyPrivateDnsConfigResolved(in PrivateDnsConfigParcel config);
+    void showProvisioningNotification(String action);
+    void hideProvisioningNotification();
+}
\ No newline at end of file
diff --git a/core/java/android/net/INetworkStackConnector.aidl b/core/java/android/net/INetworkStackConnector.aidl
index be0dc07..2df8ab7 100644
--- a/core/java/android/net/INetworkStackConnector.aidl
+++ b/core/java/android/net/INetworkStackConnector.aidl
@@ -15,6 +15,7 @@
  */
 package android.net;
 
+import android.net.INetworkMonitorCallbacks;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpServerCallbacks;
 
@@ -22,4 +23,5 @@
 oneway interface INetworkStackConnector {
     void makeDhcpServer(in String ifName, in DhcpServingParamsParcel params,
         in IDhcpServerCallbacks cb);
+    void makeNetworkMonitor(int netId, String name, in INetworkMonitorCallbacks cb);
 }
\ No newline at end of file
diff --git a/core/java/android/net/NetworkStack.java b/core/java/android/net/NetworkStack.java
index d4a0ec63..2eac6de 100644
--- a/core/java/android/net/NetworkStack.java
+++ b/core/java/android/net/NetworkStack.java
@@ -48,14 +48,16 @@
 public class NetworkStack {
     private static final String TAG = NetworkStack.class.getSimpleName();
 
+    public static final String NETWORKSTACK_PACKAGE_NAME = "com.android.mainline.networkstack";
+
     @NonNull
     @GuardedBy("mPendingNetStackRequests")
-    private final ArrayList<NetworkStackRequest> mPendingNetStackRequests = new ArrayList<>();
+    private final ArrayList<NetworkStackCallback> mPendingNetStackRequests = new ArrayList<>();
     @Nullable
     @GuardedBy("mPendingNetStackRequests")
     private INetworkStackConnector mConnector;
 
-    private interface NetworkStackRequest {
+    private interface NetworkStackCallback {
         void onNetworkStackConnected(INetworkStackConnector connector);
     }
 
@@ -77,6 +79,21 @@
         });
     }
 
+    /**
+     * Create a NetworkMonitor.
+     *
+     * <p>The INetworkMonitor will be returned asynchronously through the provided callbacks.
+     */
+    public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb) {
+        requestConnector(connector -> {
+            try {
+                connector.makeNetworkMonitor(network.netId, name, cb);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
+    }
+
     private class NetworkStackConnection implements ServiceConnection {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
@@ -96,14 +113,14 @@
         ServiceManager.addService(Context.NETWORK_STACK_SERVICE, service, false /* allowIsolated */,
                 DUMP_FLAG_PRIORITY_HIGH | DUMP_FLAG_PRIORITY_NORMAL);
 
-        final ArrayList<NetworkStackRequest> requests;
+        final ArrayList<NetworkStackCallback> requests;
         synchronized (mPendingNetStackRequests) {
             requests = new ArrayList<>(mPendingNetStackRequests);
             mPendingNetStackRequests.clear();
             mConnector = connector;
         }
 
-        for (NetworkStackRequest r : requests) {
+        for (NetworkStackCallback r : requests) {
             r.onNetworkStackConnected(connector);
         }
     }
@@ -124,7 +141,8 @@
                     "com.android.server.NetworkStackService",
                     true /* initialize */,
                     context.getClassLoader());
-            connector = (IBinder) service.getMethod("makeConnector").invoke(null);
+            connector = (IBinder) service.getMethod("makeConnector", Context.class)
+                    .invoke(null, context);
         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
             Slog.wtf(TAG, "Could not create network stack connector from NetworkStackService");
             // TODO: crash/reboot system here ?
@@ -153,7 +171,7 @@
     }
 
     // TODO: use this method to obtain the connector when implementing network stack operations
-    private void requestConnector(@NonNull NetworkStackRequest request) {
+    private void requestConnector(@NonNull NetworkStackCallback request) {
         // TODO: PID check.
         if (Binder.getCallingUid() != Process.SYSTEM_UID) {
             // Don't even attempt to obtain the connector and give a nice error message
diff --git a/core/java/android/net/PrivateDnsConfigParcel.aidl b/core/java/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..b52fce6
--- /dev/null
+++ b/core/java/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+parcelable PrivateDnsConfigParcel {
+    String hostname;
+    String[] ips;
+}
diff --git a/packages/NetworkStack/Android.bp b/packages/NetworkStack/Android.bp
index 4688848..2f7d599 100644
--- a/packages/NetworkStack/Android.bp
+++ b/packages/NetworkStack/Android.bp
@@ -21,10 +21,10 @@
     installable: true,
     srcs: [
         "src/**/*.java",
+        ":services-networkstack-shared-srcs",
     ],
     static_libs: [
         "dhcp-packet-lib",
-        "frameworks-net-shared-utils",
     ]
 }
 
diff --git a/packages/NetworkStack/AndroidManifest.xml b/packages/NetworkStack/AndroidManifest.xml
index 8516d94..0b0f1ec 100644
--- a/packages/NetworkStack/AndroidManifest.xml
+++ b/packages/NetworkStack/AndroidManifest.xml
@@ -22,8 +22,11 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
+    <uses-permission android:name="android.permission.NETWORK_SETTINGS" />
     <!-- Launch captive portal app as specific user -->
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.NETWORK_STACK" />
     <application
         android:label="NetworkStack"
         android:defaultToDeviceProtectedStorage="true"
diff --git a/packages/NetworkStack/src/android/net/util/SharedLog.java b/packages/NetworkStack/src/android/net/util/SharedLog.java
index 74bc147..4fabf10 100644
--- a/packages/NetworkStack/src/android/net/util/SharedLog.java
+++ b/packages/NetworkStack/src/android/net/util/SharedLog.java
@@ -69,6 +69,10 @@
         mComponent = component;
     }
 
+    public String getTag() {
+        return mTag;
+    }
+
     /**
      * Create a SharedLog based on this log with an additional component prefix on each logged line.
      */
diff --git a/services/net/java/android/net/util/Stopwatch.java b/packages/NetworkStack/src/android/net/util/Stopwatch.java
similarity index 78%
rename from services/net/java/android/net/util/Stopwatch.java
rename to packages/NetworkStack/src/android/net/util/Stopwatch.java
index cb15ee5..c316699 100644
--- a/services/net/java/android/net/util/Stopwatch.java
+++ b/packages/NetworkStack/src/android/net/util/Stopwatch.java
@@ -38,9 +38,9 @@
         return (isStarted() && !isStopped());
     }
 
-    // Returning |this| makes possible the following usage pattern:
-    //
-    //     Stopwatch s = new Stopwatch().start();
+    /**
+     * Start the Stopwatch.
+     */
     public Stopwatch start() {
         if (!isStarted()) {
             mStartTimeMs = SystemClock.elapsedRealtime();
@@ -48,7 +48,10 @@
         return this;
     }
 
-    // Returns the total time recorded, in milliseconds, or 0 if not started.
+    /**
+     * Stop the Stopwatch.
+     * @return the total time recorded, in milliseconds, or 0 if not started.
+     */
     public long stop() {
         if (isRunning()) {
             mStopTimeMs = SystemClock.elapsedRealtime();
@@ -57,9 +60,11 @@
         return (mStopTimeMs - mStartTimeMs);
     }
 
-    // Returns the total time recorded to date, in milliseconds.
-    // If the Stopwatch is not running, returns the same value as stop(),
-    // i.e. either the total time recorded before stopping or 0.
+    /**
+     * Return the total time recorded to date, in milliseconds.
+     * If the Stopwatch is not running, returns the same value as stop(),
+     * i.e. either the total time recorded before stopping or 0.
+     */
     public long lap() {
         if (isRunning()) {
             return (SystemClock.elapsedRealtime() - mStartTimeMs);
@@ -68,6 +73,9 @@
         }
     }
 
+    /**
+     * Reset the Stopwatch. It will be stopped when this method returns.
+     */
     public void reset() {
         mStartTimeMs = 0;
         mStopTimeMs = 0;
diff --git a/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
index 7fea1e0..057012d 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
@@ -25,18 +25,31 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
 import android.net.INetworkStackConnector;
+import android.net.Network;
+import android.net.NetworkRequest;
+import android.net.PrivateDnsConfigParcel;
 import android.net.dhcp.DhcpServer;
 import android.net.dhcp.DhcpServingParams;
 import android.net.dhcp.DhcpServingParamsParcel;
 import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.shared.PrivateDnsConfig;
 import android.net.util.SharedLog;
 import android.os.IBinder;
 import android.os.RemoteException;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.connectivity.NetworkMonitor;
+
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.ArrayDeque;
 
 /**
  * Android service used to start the network stack when bound to via an intent.
@@ -52,17 +65,41 @@
      * <p>On platforms where the network stack runs in the system server process, this method may
      * be called directly instead of obtaining the connector by binding to the service.
      */
-    public static IBinder makeConnector() {
-        return new NetworkStackConnector();
+    public static IBinder makeConnector(Context context) {
+        return new NetworkStackConnector(context);
     }
 
     @NonNull
     @Override
     public IBinder onBind(Intent intent) {
-        return makeConnector();
+        return makeConnector(this);
     }
 
     private static class NetworkStackConnector extends INetworkStackConnector.Stub {
+        private static final int NUM_VALIDATION_LOG_LINES = 20;
+        private final Context mContext;
+        private final ConnectivityManager mCm;
+
+        private static final int MAX_VALIDATION_LOGS = 10;
+        @GuardedBy("mValidationLogs")
+        private final ArrayDeque<SharedLog> mValidationLogs = new ArrayDeque<>(MAX_VALIDATION_LOGS);
+
+        private SharedLog addValidationLogs(Network network, String name) {
+            final SharedLog log = new SharedLog(NUM_VALIDATION_LOG_LINES, network + " - " + name);
+            synchronized (mValidationLogs) {
+                while (mValidationLogs.size() >= MAX_VALIDATION_LOGS) {
+                    mValidationLogs.removeLast();
+                }
+                mValidationLogs.addFirst(log);
+            }
+            return log;
+        }
+
+        NetworkStackConnector(Context context) {
+            mContext = context;
+            mCm = context.getSystemService(ConnectivityManager.class);
+        }
+
         @NonNull
         private final SharedLog mLog = new SharedLog(TAG);
 
@@ -89,11 +126,102 @@
         }
 
         @Override
+        public void makeNetworkMonitor(int netId, String name, INetworkMonitorCallbacks cb)
+                throws RemoteException {
+            final Network network = new Network(netId, false /* privateDnsBypass */);
+            final NetworkRequest defaultRequest = mCm.getDefaultRequest();
+            final SharedLog log = addValidationLogs(network, name);
+            final NetworkMonitor nm = new NetworkMonitor(
+                    mContext, cb, network, defaultRequest, log);
+            cb.onNetworkMonitorCreated(new NetworkMonitorImpl(nm));
+        }
+
+        @Override
         protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
                 @Nullable String[] args) {
             checkNetworkStackCallingPermission();
-            fout.println("NetworkStack logs:");
-            mLog.dump(fd, fout, args);
+            final IndentingPrintWriter pw = new IndentingPrintWriter(fout, "  ");
+            pw.println("NetworkStack logs:");
+            mLog.dump(fd, pw, args);
+
+            pw.println();
+            pw.println("Validation logs (most recent first):");
+            synchronized (mValidationLogs) {
+                for (SharedLog p : mValidationLogs) {
+                    pw.println(p.getTag());
+                    pw.increaseIndent();
+                    p.dump(fd, pw, args);
+                    pw.decreaseIndent();
+                }
+            }
+        }
+    }
+
+    private static class NetworkMonitorImpl extends INetworkMonitor.Stub {
+        private final NetworkMonitor mNm;
+
+        NetworkMonitorImpl(NetworkMonitor nm) {
+            mNm = nm;
+        }
+
+        @Override
+        public void start() {
+            checkNetworkStackCallingPermission();
+            mNm.start();
+        }
+
+        @Override
+        public void launchCaptivePortalApp() {
+            checkNetworkStackCallingPermission();
+            mNm.launchCaptivePortalApp();
+        }
+
+        @Override
+        public void forceReevaluation(int uid) {
+            checkNetworkStackCallingPermission();
+            mNm.forceReevaluation(uid);
+        }
+
+        @Override
+        public void notifyPrivateDnsChanged(PrivateDnsConfigParcel config) {
+            checkNetworkStackCallingPermission();
+            mNm.notifyPrivateDnsSettingsChanged(PrivateDnsConfig.fromParcel(config));
+        }
+
+        @Override
+        public void notifyDnsResponse(int returnCode) {
+            checkNetworkStackCallingPermission();
+            mNm.notifyDnsResponse(returnCode);
+        }
+
+        @Override
+        public void notifySystemReady() {
+            checkNetworkStackCallingPermission();
+            mNm.notifySystemReady();
+        }
+
+        @Override
+        public void notifyNetworkConnected() {
+            checkNetworkStackCallingPermission();
+            mNm.notifyNetworkConnected();
+        }
+
+        @Override
+        public void notifyNetworkDisconnected() {
+            checkNetworkStackCallingPermission();
+            mNm.notifyNetworkDisconnected();
+        }
+
+        @Override
+        public void notifyLinkPropertiesChanged() {
+            checkNetworkStackCallingPermission();
+            mNm.notifyLinkPropertiesChanged();
+        }
+
+        @Override
+        public void notifyNetworkCapabilitiesChanged() {
+            checkNetworkStackCallingPermission();
+            mNm.notifyNetworkCapabilitiesChanged();
         }
     }
 }
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
similarity index 84%
rename from services/core/java/com/android/server/connectivity/NetworkMonitor.java
rename to packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
index 2a00025..94ea1b9 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java
@@ -21,6 +21,11 @@
 import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS;
 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC;
 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.metrics.ValidationProbeEvent.DNS_FAILURE;
 import static android.net.metrics.ValidationProbeEvent.DNS_SUCCESS;
 import static android.net.metrics.ValidationProbeEvent.PROBE_FALLBACK;
@@ -35,6 +40,9 @@
 import android.net.CaptivePortal;
 import android.net.ConnectivityManager;
 import android.net.ICaptivePortal;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
+import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
@@ -46,11 +54,14 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.metrics.ValidationProbeEvent;
+import android.net.shared.NetworkMonitorUtils;
+import android.net.shared.PrivateDnsConfig;
+import android.net.util.SharedLog;
 import android.net.util.Stopwatch;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
-import android.os.Handler;
 import android.os.Message;
+import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -65,8 +76,6 @@
 import android.telephony.CellInfoWcdma;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.LocalLog;
-import android.util.LocalLog.ReadOnlyLocalLog;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -75,7 +84,6 @@
 import com.android.internal.util.RingBufferIndices;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
-import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
@@ -104,9 +112,7 @@
     // Default configuration values for captive portal detection probes.
     // TODO: append a random length parameter to the default HTTPS url.
     // TODO: randomize browser version ids in the default User-Agent String.
-    private static final String DEFAULT_HTTPS_URL     = "https://www.google.com/generate_204";
-    private static final String DEFAULT_HTTP_URL      =
-            "http://connectivitycheck.gstatic.com/generate_204";
+    private static final String DEFAULT_HTTPS_URL = "https://www.google.com/generate_204";
     private static final String DEFAULT_FALLBACK_URL  = "http://www.google.com/gen_204";
     private static final String DEFAULT_OTHER_FALLBACK_URLS =
             "http://play.googleapis.com/generate_204";
@@ -144,33 +150,12 @@
         }
     }
 
-    // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
-    // The network should be used as a default internet connection.  It was found to be:
-    // 1. a functioning network providing internet access, or
-    // 2. a captive portal and the user decided to use it as is.
-    public static final int NETWORK_TEST_RESULT_VALID = 0;
-    // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
-    // The network should not be used as a default internet connection.  It was found to be:
-    // 1. a captive portal and the user is prompted to sign-in, or
-    // 2. a captive portal and the user did not want to use it, or
-    // 3. a broken network (e.g. DNS failed, connect failed, HTTP request failed).
-    public static final int NETWORK_TEST_RESULT_INVALID = 1;
-
     private static final int BASE = Protocol.BASE_NETWORK_MONITOR;
-
     /**
-     * Inform NetworkMonitor that their network is connected.
+     * ConnectivityService has sent a notification to indicate that network has connected.
      * Initiates Network Validation.
      */
-    public static final int CMD_NETWORK_CONNECTED = BASE + 1;
-
-    /**
-     * Inform ConnectivityService that the network has been tested.
-     * obj = String representing URL that Internet probe was redirect to, if it was redirected.
-     * arg1 = One of the NETWORK_TESTED_RESULT_* constants.
-     * arg2 = NetID.
-     */
-    public static final int EVENT_NETWORK_TESTED = BASE + 2;
+    private static final int CMD_NETWORK_CONNECTED = BASE + 1;
 
     /**
      * Message to self indicating it's time to evaluate a network's connectivity.
@@ -179,9 +164,9 @@
     private static final int CMD_REEVALUATE = BASE + 6;
 
     /**
-     * Inform NetworkMonitor that the network has disconnected.
+     * ConnectivityService has sent a notification to indicate that network has disconnected.
      */
-    public static final int CMD_NETWORK_DISCONNECTED = BASE + 7;
+    private static final int CMD_NETWORK_DISCONNECTED = BASE + 7;
 
     /**
      * Force evaluation even if it has succeeded in the past.
@@ -199,21 +184,13 @@
     private static final int CMD_CAPTIVE_PORTAL_APP_FINISHED = BASE + 9;
 
     /**
-     * Request ConnectivityService display provisioning notification.
-     * arg1    = Whether to make the notification visible.
-     * arg2    = NetID.
-     * obj     = Intent to be launched when notification selected by user, null if !arg1.
-     */
-    public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 10;
-
-    /**
      * Message indicating sign-in app should be launched.
      * Sent by mLaunchCaptivePortalAppBroadcastReceiver when the
      * user touches the sign in notification, or sent by
      * ConnectivityService when the user touches the "sign into
      * network" button in the wifi access point detail page.
      */
-    public static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = BASE + 11;
+    private static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = BASE + 11;
 
     /**
      * Retest network to see if captive portal is still in place.
@@ -234,7 +211,6 @@
      * validation phase is completed.
      */
     private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = BASE + 13;
-    public static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = BASE + 14;
     private static final int CMD_EVALUATE_PRIVATE_DNS = BASE + 15;
 
     /**
@@ -263,23 +239,16 @@
     // Delay between reevaluations once a captive portal has been found.
     private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 10 * 60 * 1000;
 
-    private static final int NUM_VALIDATION_LOG_LINES = 20;
-
     private String mPrivateDnsProviderHostname = "";
 
-    public static boolean isValidationRequired(
-            NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
-        // TODO: Consider requiring validation for DUN networks.
-        return dfltNetCap.satisfiedByNetworkCapabilities(nc);
-    }
-
     private final Context mContext;
-    private final Handler mConnectivityServiceHandler;
-    private final NetworkAgentInfo mNetworkAgentInfo;
+    private final INetworkMonitorCallbacks mCallback;
     private final Network mNetwork;
+    private final Network mNonPrivateDnsBypassNetwork;
     private final int mNetId;
     private final TelephonyManager mTelephonyManager;
     private final WifiManager mWifiManager;
+    private final ConnectivityManager mCm;
     private final NetworkRequest mDefaultRequest;
     private final IpConnectivityLog mMetricsLog;
     private final Dependencies mDependencies;
@@ -292,6 +261,9 @@
     @Nullable
     private final CaptivePortalProbeSpec[] mCaptivePortalFallbackSpecs;
 
+    private NetworkCapabilities mNetworkCapabilities;
+    private LinkProperties mLinkProperties;
+
     @VisibleForTesting
     protected boolean mIsCaptivePortalCheckEnabled;
 
@@ -304,7 +276,7 @@
     // Avoids surfacing "Sign in to network" notification.
     private boolean mDontDisplaySigninNotification = false;
 
-    public boolean systemReady = false;
+    private volatile boolean mSystemReady = false;
 
     private final State mDefaultState = new DefaultState();
     private final State mValidatedState = new ValidatedState();
@@ -317,7 +289,7 @@
 
     private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null;
 
-    private final LocalLog validationLogs = new LocalLog(NUM_VALIDATION_LOG_LINES);
+    private final SharedLog mValidationLogs;
 
     private final Stopwatch mEvaluationTimer = new Stopwatch();
 
@@ -328,6 +300,7 @@
     private final Random mRandom;
     private int mNextFallbackUrlIndex = 0;
 
+
     private int mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS;
     private int mEvaluateAttempts = 0;
     private volatile int mProbeToken = 0;
@@ -338,17 +311,18 @@
     private final DnsStallDetector mDnsStallDetector;
     private long mLastProbeTime;
 
-    public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
-            NetworkRequest defaultRequest) {
-        this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog(),
+    public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
+            NetworkRequest defaultRequest, SharedLog validationLog) {
+        this(context, cb, network, defaultRequest, new IpConnectivityLog(), validationLog,
                 Dependencies.DEFAULT);
     }
 
     @VisibleForTesting
-    protected NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
-            NetworkRequest defaultRequest, IpConnectivityLog logger, Dependencies deps) {
+    protected NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network,
+            NetworkRequest defaultRequest, IpConnectivityLog logger, SharedLog validationLogs,
+            Dependencies deps) {
         // Add suffix indicating which NetworkMonitor we're talking about.
-        super(TAG + networkAgentInfo.name());
+        super(TAG + "/" + network.netId);
 
         // Logs with a tag of the form given just above, e.g.
         //     <timestamp>   862  2402 D NetworkMonitor/NetworkAgentInfo [WIFI () - 100]: ...
@@ -356,15 +330,18 @@
 
         mContext = context;
         mMetricsLog = logger;
-        mConnectivityServiceHandler = handler;
+        mValidationLogs = validationLogs;
+        mCallback = cb;
         mDependencies = deps;
-        mNetworkAgentInfo = networkAgentInfo;
-        mNetwork = deps.getNetwork(networkAgentInfo).getPrivateDnsBypassingCopy();
+        mNonPrivateDnsBypassNetwork = network;
+        mNetwork = deps.getPrivateDnsBypassNetwork(network);
         mNetId = mNetwork.netId;
         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
         mDefaultRequest = defaultRequest;
 
+        // CHECKSTYLE:OFF IndentationCheck
         addState(mDefaultState);
         addState(mMaybeNotifyState, mDefaultState);
             addState(mEvaluatingState, mMaybeNotifyState);
@@ -374,12 +351,13 @@
         addState(mEvaluatingPrivateDnsState, mDefaultState);
         addState(mValidatedState, mDefaultState);
         setInitialState(mDefaultState);
+        // CHECKSTYLE:ON IndentationCheck
 
         mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled();
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalHttpsUrl = makeURL(getCaptivePortalServerHttpsUrl());
-        mCaptivePortalHttpUrl = makeURL(getCaptivePortalServerHttpUrl(deps, context));
+        mCaptivePortalHttpUrl = makeURL(deps.getCaptivePortalServerHttpUrl(context));
         mCaptivePortalFallbackUrls = makeCaptivePortalFallbackUrls();
         mCaptivePortalFallbackSpecs = makeCaptivePortalFallbackProbeSpecs();
         mRandom = deps.getRandom();
@@ -390,7 +368,13 @@
         mDataStallValidDnsTimeThreshold = getDataStallValidDnsTimeThreshold();
         mDataStallEvaluationType = getDataStallEvalutionType();
 
-        start();
+        // mLinkProperties and mNetworkCapbilities must never be null or we will NPE.
+        // Provide empty objects in case we are started and the network disconnects before
+        // we can ever fetch them.
+        // TODO: Delete ASAP.
+        mLinkProperties = new LinkProperties();
+        mNetworkCapabilities = new NetworkCapabilities();
+        mNetworkCapabilities.clearAll();
     }
 
     /**
@@ -401,6 +385,14 @@
     }
 
     /**
+     * Send a notification to NetworkMonitor indicating that there was a DNS query response event.
+     * @param returnCode the DNS return code of the response.
+     */
+    public void notifyDnsResponse(int returnCode) {
+        sendMessage(EVENT_DNS_NOTIFICATION, returnCode);
+    }
+
+    /**
      * Send a notification to NetworkMonitor indicating that private DNS settings have changed.
      * @param newCfg The new private DNS configuration.
      */
@@ -411,9 +403,75 @@
         sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
     }
 
+    /**
+     * Send a notification to NetworkMonitor indicating that the system is ready.
+     */
+    public void notifySystemReady() {
+        // No need to run on the handler thread: mSystemReady is volatile and read only once on the
+        // isCaptivePortal() thread.
+        mSystemReady = true;
+    }
+
+    /**
+     * Send a notification to NetworkMonitor indicating that the network is now connected.
+     */
+    public void notifyNetworkConnected() {
+        sendMessage(CMD_NETWORK_CONNECTED);
+    }
+
+    /**
+     * Send a notification to NetworkMonitor indicating that the network is now disconnected.
+     */
+    public void notifyNetworkDisconnected() {
+        sendMessage(CMD_NETWORK_DISCONNECTED);
+    }
+
+    /**
+     * Send a notification to NetworkMonitor indicating that link properties have changed.
+     */
+    public void notifyLinkPropertiesChanged() {
+        getHandler().post(() -> {
+            updateLinkProperties();
+        });
+    }
+
+    private void updateLinkProperties() {
+        final LinkProperties lp = mCm.getLinkProperties(mNetwork);
+        // If null, we should soon get a message that the network was disconnected, and will stop.
+        if (lp != null) {
+            // TODO: send LinkProperties parceled in notifyLinkPropertiesChanged() and start().
+            mLinkProperties = lp;
+        }
+    }
+
+    /**
+     * Send a notification to NetworkMonitor indicating that network capabilities have changed.
+     */
+    public void notifyNetworkCapabilitiesChanged() {
+        getHandler().post(() -> {
+            updateNetworkCapabilities();
+        });
+    }
+
+    private void updateNetworkCapabilities() {
+        final NetworkCapabilities nc = mCm.getNetworkCapabilities(mNetwork);
+        // If null, we should soon get a message that the network was disconnected, and will stop.
+        if (nc != null) {
+            // TODO: send NetworkCapabilities parceled in notifyNetworkCapsChanged() and start().
+            mNetworkCapabilities = nc;
+        }
+    }
+
+    /**
+     * Request the captive portal application to be launched.
+     */
+    public void launchCaptivePortalApp() {
+        sendMessage(CMD_LAUNCH_CAPTIVE_PORTAL_APP);
+    }
+
     @Override
     protected void log(String s) {
-        if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s);
+        if (DBG) Log.d(TAG + "/" + mNetwork.netId, s);
     }
 
     private void validationLog(int probeType, Object url, String msg) {
@@ -423,11 +481,7 @@
 
     private void validationLog(String s) {
         if (DBG) log(s);
-        validationLogs.log(s);
-    }
-
-    public ReadOnlyLocalLog getValidationLogs() {
-        return validationLogs.readOnlyLocalLog();
+        mValidationLogs.log(s);
     }
 
     private ValidationStage validationStage() {
@@ -435,20 +489,46 @@
     }
 
     private boolean isValidationRequired() {
-        return isValidationRequired(
-                mDefaultRequest.networkCapabilities, mNetworkAgentInfo.networkCapabilities);
+        return NetworkMonitorUtils.isValidationRequired(
+                mDefaultRequest.networkCapabilities, mNetworkCapabilities);
     }
 
 
-    private void notifyNetworkTestResultInvalid(Object obj) {
-        mConnectivityServiceHandler.sendMessage(obtainMessage(
-                EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, obj));
+    private void notifyNetworkTested(int result, @Nullable String redirectUrl) {
+        try {
+            mCallback.notifyNetworkTested(result, redirectUrl);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending network test result", e);
+        }
+    }
+
+    private void showProvisioningNotification(String action) {
+        try {
+            mCallback.showProvisioningNotification(action);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error showing provisioning notification", e);
+        }
+    }
+
+    private void hideProvisioningNotification() {
+        try {
+            mCallback.hideProvisioningNotification();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error hiding provisioning notification", e);
+        }
     }
 
     // DefaultState is the parent of all States.  It exists only to handle CMD_* messages but
     // does not entail any real state (hence no enter() or exit() routines).
     private class DefaultState extends State {
         @Override
+        public void enter() {
+            // TODO: have those passed parceled in start() and remove this
+            updateLinkProperties();
+            updateNetworkCapabilities();
+        }
+
+        @Override
         public boolean processMessage(Message message) {
             switch (message.what) {
                 case CMD_NETWORK_CONNECTED:
@@ -499,7 +579,7 @@
                         case APP_RETURN_UNWANTED:
                             mDontDisplaySigninNotification = true;
                             mUserDoesNotWant = true;
-                            notifyNetworkTestResultInvalid(null);
+                            notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, null);
                             // TODO: Should teardown network.
                             mUidResponsibleForReeval = 0;
                             transitionTo(mEvaluatingState);
@@ -563,8 +643,7 @@
         public void enter() {
             maybeLogEvaluationResult(
                     networkEventType(validationStage(), EvaluationResult.VALIDATED));
-            mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
-                    NETWORK_TEST_RESULT_VALID, mNetId, null));
+            notifyNetworkTested(INetworkMonitor.NETWORK_TEST_RESULT_VALID, null);
             mValidations++;
         }
 
@@ -633,8 +712,7 @@
 
         @Override
         public void exit() {
-            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0, mNetId, null);
-            mConnectivityServiceHandler.sendMessage(message);
+            hideProvisioningNotification();
         }
     }
 
@@ -751,9 +829,7 @@
                         CMD_LAUNCH_CAPTIVE_PORTAL_APP);
             }
             // Display the sign in notification.
-            Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1, mNetId,
-                    mLaunchCaptivePortalAppBroadcastReceiver.getPendingIntent());
-            mConnectivityServiceHandler.sendMessage(message);
+            showProvisioningNotification(mLaunchCaptivePortalAppBroadcastReceiver.mAction);
             // Retest for captive portal occasionally.
             sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */,
                     CAPTIVE_PORTAL_REEVALUATE_DELAY_MS);
@@ -839,12 +915,15 @@
         }
 
         private void notifyPrivateDnsConfigResolved() {
-            mConnectivityServiceHandler.sendMessage(obtainMessage(
-                    EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId, mPrivateDnsConfig));
+            try {
+                mCallback.notifyPrivateDnsConfigResolved(mPrivateDnsConfig.toParcel());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error sending private DNS config resolved notification", e);
+            }
         }
 
         private void handlePrivateDnsEvaluationFailure() {
-            notifyNetworkTestResultInvalid(null);
+            notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, null);
 
             // Queue up a re-evaluation with backoff.
             //
@@ -865,7 +944,7 @@
                     + oneTimeHostnameSuffix;
             final Stopwatch watch = new Stopwatch().start();
             try {
-                final InetAddress[] ips = mNetworkAgentInfo.network().getAllByName(host);
+                final InetAddress[] ips = mNonPrivateDnsBypassNetwork.getAllByName(host);
                 final long time = watch.stop();
                 final String strIps = Arrays.toString(ips);
                 final boolean success = (ips != null && ips.length > 0);
@@ -915,12 +994,12 @@
                         // state (even if no Private DNS validation required).
                         transitionTo(mEvaluatingPrivateDnsState);
                     } else if (probeResult.isPortal()) {
-                        notifyNetworkTestResultInvalid(probeResult.redirectUrl);
+                        notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, probeResult.redirectUrl);
                         mLastPortalProbeResult = probeResult;
                         transitionTo(mCaptivePortalState);
                     } else {
                         logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
-                        notifyNetworkTestResultInvalid(probeResult.redirectUrl);
+                        notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, probeResult.redirectUrl);
                         transitionTo(mWaitingForNextProbeState);
                     }
                     return HANDLED;
@@ -996,18 +1075,18 @@
         }
     }
 
-    public boolean getIsCaptivePortalCheckEnabled() {
+    private boolean getIsCaptivePortalCheckEnabled() {
         String symbol = Settings.Global.CAPTIVE_PORTAL_MODE;
         int defaultValue = Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT;
         int mode = mDependencies.getSetting(mContext, symbol, defaultValue);
         return mode != Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE;
     }
 
-    public boolean getUseHttpsValidation() {
+    private boolean getUseHttpsValidation() {
         return mDependencies.getSetting(mContext, Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
     }
 
-    public boolean getWifiScansAlwaysAvailableDisabled() {
+    private boolean getWifiScansAlwaysAvailableDisabled() {
         return mDependencies.getSetting(
                 mContext, Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0;
     }
@@ -1040,15 +1119,6 @@
                 DEFAULT_DATA_STALL_EVALUATION_TYPES);
     }
 
-    // Static for direct access by ConnectivityService
-    public static String getCaptivePortalServerHttpUrl(Context context) {
-        return getCaptivePortalServerHttpUrl(Dependencies.DEFAULT, context);
-    }
-
-    public static String getCaptivePortalServerHttpUrl(Dependencies deps, Context context) {
-        return deps.getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL);
-    }
-
     private URL[] makeCaptivePortalFallbackUrls() {
         try {
             String separator = ",";
@@ -1144,7 +1214,7 @@
         // 3. PAC scripts are sometimes used to block or restrict Internet access and may in
         //    fact block fetching of the generate_204 URL which would lead to false negative
         //    results for network validation.
-        final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
+        final ProxyInfo proxyInfo = mLinkProperties.getHttpProxy();
         if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
             pacUrl = makeURL(proxyInfo.getPacFileUrl().toString());
             if (pacUrl == null) {
@@ -1416,89 +1486,86 @@
             return;
         }
 
-        if (!systemReady) {
+        if (!mSystemReady) {
             return;
         }
 
         Intent latencyBroadcast =
-                new Intent(ConnectivityConstants.ACTION_NETWORK_CONDITIONS_MEASURED);
-        switch (mNetworkAgentInfo.networkInfo.getType()) {
-            case ConnectivityManager.TYPE_WIFI:
-                WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
-                if (currentWifiInfo != null) {
-                    // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not
-                    // surrounded by double quotation marks (thus violating the Javadoc), but this
-                    // was changed to match the Javadoc in API 17. Since clients may have started
-                    // sanitizing the output of this method since API 17 was released, we should
-                    // not change it here as it would become impossible to tell whether the SSID is
-                    // simply being surrounded by quotes due to the API, or whether those quotes
-                    // are actually part of the SSID.
-                    latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_SSID,
-                            currentWifiInfo.getSSID());
-                    latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_BSSID,
-                            currentWifiInfo.getBSSID());
-                } else {
-                    if (VDBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
-                    return;
-                }
-                break;
-            case ConnectivityManager.TYPE_MOBILE:
-                latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_NETWORK_TYPE,
-                        mTelephonyManager.getNetworkType());
-                List<CellInfo> info = mTelephonyManager.getAllCellInfo();
-                if (info == null) return;
-                int numRegisteredCellInfo = 0;
-                for (CellInfo cellInfo : info) {
-                    if (cellInfo.isRegistered()) {
-                        numRegisteredCellInfo++;
-                        if (numRegisteredCellInfo > 1) {
-                            if (VDBG) {
-                                logw("more than one registered CellInfo."
-                                        + " Can't tell which is active.  Bailing.");
-                            }
-                            return;
+                new Intent(NetworkMonitorUtils.ACTION_NETWORK_CONDITIONS_MEASURED);
+        if (mNetworkCapabilities.hasTransport(TRANSPORT_WIFI)) {
+            WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
+            if (currentWifiInfo != null) {
+                // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not
+                // surrounded by double quotation marks (thus violating the Javadoc), but this
+                // was changed to match the Javadoc in API 17. Since clients may have started
+                // sanitizing the output of this method since API 17 was released, we should
+                // not change it here as it would become impossible to tell whether the SSID is
+                // simply being surrounded by quotes due to the API, or whether those quotes
+                // are actually part of the SSID.
+                latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_SSID,
+                        currentWifiInfo.getSSID());
+                latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_BSSID,
+                        currentWifiInfo.getBSSID());
+            } else {
+                if (VDBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
+                return;
+            }
+            latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CONNECTIVITY_TYPE, TYPE_WIFI);
+        } else if (mNetworkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+            latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_NETWORK_TYPE,
+                    mTelephonyManager.getNetworkType());
+            List<CellInfo> info = mTelephonyManager.getAllCellInfo();
+            if (info == null) return;
+            int numRegisteredCellInfo = 0;
+            for (CellInfo cellInfo : info) {
+                if (cellInfo.isRegistered()) {
+                    numRegisteredCellInfo++;
+                    if (numRegisteredCellInfo > 1) {
+                        if (VDBG) {
+                            logw("more than one registered CellInfo."
+                                    + " Can't tell which is active.  Bailing.");
                         }
-                        if (cellInfo instanceof CellInfoCdma) {
-                            CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
-                            latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
-                        } else if (cellInfo instanceof CellInfoGsm) {
-                            CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
-                            latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
-                        } else if (cellInfo instanceof CellInfoLte) {
-                            CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
-                            latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
-                        } else if (cellInfo instanceof CellInfoWcdma) {
-                            CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
-                            latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
-                        } else {
-                            if (VDBG) logw("Registered cellinfo is unrecognized");
-                            return;
-                        }
+                        return;
+                    }
+                    if (cellInfo instanceof CellInfoCdma) {
+                        CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
+                        latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CELL_ID, cellId);
+                    } else if (cellInfo instanceof CellInfoGsm) {
+                        CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
+                        latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CELL_ID, cellId);
+                    } else if (cellInfo instanceof CellInfoLte) {
+                        CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
+                        latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CELL_ID, cellId);
+                    } else if (cellInfo instanceof CellInfoWcdma) {
+                        CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
+                        latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CELL_ID, cellId);
+                    } else {
+                        if (VDBG) logw("Registered cellinfo is unrecognized");
+                        return;
                     }
                 }
-                break;
-            default:
-                return;
+            }
+            latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_CONNECTIVITY_TYPE, TYPE_MOBILE);
+        } else {
+            return;
         }
-        latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CONNECTIVITY_TYPE,
-                mNetworkAgentInfo.networkInfo.getType());
-        latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_RESPONSE_RECEIVED,
+        latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_RESPONSE_RECEIVED,
                 responseReceived);
-        latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_REQUEST_TIMESTAMP_MS,
+        latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_REQUEST_TIMESTAMP_MS,
                 requestTimestampMs);
 
         if (responseReceived) {
-            latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_IS_CAPTIVE_PORTAL,
+            latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_IS_CAPTIVE_PORTAL,
                     isCaptivePortal);
-            latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_RESPONSE_TIMESTAMP_MS,
+            latencyBroadcast.putExtra(NetworkMonitorUtils.EXTRA_RESPONSE_TIMESTAMP_MS,
                     responseTimestampMs);
         }
         mContext.sendBroadcastAsUser(latencyBroadcast, UserHandle.CURRENT,
-                ConnectivityConstants.PERMISSION_ACCESS_NETWORK_CONDITIONS);
+                NetworkMonitorUtils.PERMISSION_ACCESS_NETWORK_CONDITIONS);
     }
 
     private void logNetworkEvent(int evtype) {
-        int[] transports = mNetworkAgentInfo.networkCapabilities.getTransportTypes();
+        int[] transports = mNetworkCapabilities.getTransportTypes();
         mMetricsLog.log(mNetId, transports, new NetworkEvent(evtype));
     }
 
@@ -1520,14 +1587,14 @@
 
     private void maybeLogEvaluationResult(int evtype) {
         if (mEvaluationTimer.isRunning()) {
-            int[] transports = mNetworkAgentInfo.networkCapabilities.getTransportTypes();
+            int[] transports = mNetworkCapabilities.getTransportTypes();
             mMetricsLog.log(mNetId, transports, new NetworkEvent(evtype, mEvaluationTimer.stop()));
             mEvaluationTimer.reset();
         }
     }
 
     private void logValidationProbe(long durationMs, int probeType, int probeResult) {
-        int[] transports = mNetworkAgentInfo.networkCapabilities.getTransportTypes();
+        int[] transports = mNetworkCapabilities.getTransportTypes();
         boolean isFirstValidation = validationStage().mIsFirstValidation;
         ValidationProbeEvent ev = new ValidationProbeEvent();
         ev.probeType = ValidationProbeEvent.makeProbeType(probeType, isFirstValidation);
@@ -1537,9 +1604,9 @@
     }
 
     @VisibleForTesting
-    public static class Dependencies {
-        public Network getNetwork(NetworkAgentInfo networkAgentInfo) {
-            return new OneAddressPerFamilyNetwork(networkAgentInfo.network());
+    static class Dependencies {
+        public Network getPrivateDnsBypassNetwork(Network network) {
+            return new OneAddressPerFamilyNetwork(network);
         }
 
         public Random getRandom() {
@@ -1547,6 +1614,13 @@
         }
 
         /**
+         * Get the captive portal server HTTP URL that is configured on the device.
+         */
+        public String getCaptivePortalServerHttpUrl(Context context) {
+            return NetworkMonitorUtils.getCaptivePortalServerHttpUrl(context);
+        }
+
+        /**
          * Get the value of a global integer setting.
          * @param symbol Name of the setting
          * @param defaultValue Value to return if the setting is not defined.
@@ -1666,7 +1740,7 @@
         boolean result = false;
         // Reevaluation will generate traffic. Thus, set a minimal reevaluation timer to limit the
         // possible traffic cost in metered network.
-        if (mNetworkAgentInfo.networkCapabilities.isMetered()
+        if (mNetworkCapabilities.isMetered()
                 && (SystemClock.elapsedRealtime() - getLastProbeTime()
                 < mDataStallMinEvaluateTime)) {
             return false;
diff --git a/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
similarity index 68%
rename from tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
rename to packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
index 6e07b26..d31fa77 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -16,6 +16,14 @@
 
 package com.android.server.connectivity;
 
+import static android.net.ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN;
+import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL;
+import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
+import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+
+import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 
 import static org.junit.Assert.assertTrue;
@@ -24,13 +32,21 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.content.Intent;
+import android.net.CaptivePortal;
 import android.net.ConnectivityManager;
+import android.net.INetworkMonitorCallbacks;
+import android.net.InetAddresses;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -38,9 +54,12 @@
 import android.net.NetworkRequest;
 import android.net.captiveportal.CaptivePortalProbeResult;
 import android.net.metrics.IpConnectivityLog;
+import android.net.util.SharedLog;
 import android.net.wifi.WifiManager;
+import android.os.ConditionVariable;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -50,8 +69,10 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
@@ -68,21 +89,23 @@
     private static final String LOCATION_HEADER = "location";
 
     private @Mock Context mContext;
-    private @Mock Handler mHandler;
     private @Mock IpConnectivityLog mLogger;
-    private @Mock NetworkAgentInfo mAgent;
-    private @Mock NetworkAgentInfo mNotMeteredAgent;
+    private @Mock SharedLog mValidationLogger;
     private @Mock NetworkInfo mNetworkInfo;
-    private @Mock NetworkRequest mRequest;
+    private @Mock ConnectivityManager mCm;
     private @Mock TelephonyManager mTelephony;
     private @Mock WifiManager mWifi;
-    private @Mock Network mNetwork;
     private @Mock HttpURLConnection mHttpConnection;
     private @Mock HttpURLConnection mHttpsConnection;
     private @Mock HttpURLConnection mFallbackConnection;
     private @Mock HttpURLConnection mOtherFallbackConnection;
     private @Mock Random mRandom;
     private @Mock NetworkMonitor.Dependencies mDependencies;
+    private @Mock INetworkMonitorCallbacks mCallbacks;
+    private @Spy Network mNetwork = new Network(TEST_NETID);
+    private NetworkRequest mRequest;
+
+    private static final int TEST_NETID = 4242;
 
     private static final String TEST_HTTP_URL = "http://www.google.com/gen_204";
     private static final String TEST_HTTPS_URL = "https://www.google.com/gen_204";
@@ -93,33 +116,37 @@
     private static final int RETURN_CODE_DNS_SUCCESS = 0;
     private static final int RETURN_CODE_DNS_TIMEOUT = 255;
 
+    private static final int HANDLER_TIMEOUT_MS = 1000;
+
+    private static final LinkProperties TEST_LINKPROPERTIES = new LinkProperties();
+
+    private static final NetworkCapabilities METERED_CAPABILITIES = new NetworkCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .addCapability(NET_CAPABILITY_INTERNET);
+
+    private static final NetworkCapabilities NOT_METERED_CAPABILITIES = new NetworkCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+            .addCapability(NET_CAPABILITY_INTERNET)
+            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+
+    private static final NetworkCapabilities NO_INTERNET_CAPABILITIES = new NetworkCapabilities()
+            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
+
     @Before
     public void setUp() throws IOException {
         MockitoAnnotations.initMocks(this);
-        mAgent.linkProperties = new LinkProperties();
-        mAgent.networkCapabilities = new NetworkCapabilities()
-                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
-        mAgent.networkInfo = mNetworkInfo;
-
-        mNotMeteredAgent.linkProperties = new LinkProperties();
-        mNotMeteredAgent.networkCapabilities = new NetworkCapabilities()
-            .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
-        mNotMeteredAgent.networkInfo = mNetworkInfo;
-
-        when(mAgent.network()).thenReturn(mNetwork);
-        when(mDependencies.getNetwork(any())).thenReturn(mNetwork);
+        when(mDependencies.getPrivateDnsBypassNetwork(any())).thenReturn(mNetwork);
         when(mDependencies.getRandom()).thenReturn(mRandom);
         when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt()))
                 .thenReturn(Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT);
         when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_USE_HTTPS),
                 anyInt())).thenReturn(1);
-        when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTP_URL),
-                anyString())).thenReturn(TEST_HTTP_URL);
+        when(mDependencies.getCaptivePortalServerHttpUrl(any())).thenReturn(TEST_HTTP_URL);
         when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL),
                 anyString())).thenReturn(TEST_HTTPS_URL);
-        when(mNetwork.getPrivateDnsBypassingCopy()).thenReturn(mNetwork);
+        doReturn(mNetwork).when(mNetwork).getPrivateDnsBypassingCopy();
 
+        when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mCm);
         when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephony);
         when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifi);
 
@@ -129,7 +156,7 @@
         setFallbackSpecs(null); // Test with no fallback spec by default
         when(mRandom.nextInt()).thenReturn(0);
 
-        when(mNetwork.openConnection(any())).then((invocation) -> {
+        doAnswer((invocation) -> {
             URL url = invocation.getArgument(0);
             switch(url.toString()) {
                 case TEST_HTTP_URL:
@@ -144,12 +171,20 @@
                     fail("URL not mocked: " + url.toString());
                     return null;
             }
-        });
+        }).when(mNetwork).openConnection(any());
         when(mHttpConnection.getRequestProperties()).thenReturn(new ArrayMap<>());
         when(mHttpsConnection.getRequestProperties()).thenReturn(new ArrayMap<>());
-        when(mNetwork.getAllByName(any())).thenReturn(new InetAddress[] {
-            InetAddress.parseNumericAddress("192.168.0.0")
-        });
+        doReturn(new InetAddress[] {
+                InetAddresses.parseNumericAddress("192.168.0.0")
+        }).when(mNetwork).getAllByName(any());
+
+        mRequest = new NetworkRequest.Builder()
+                .addCapability(NET_CAPABILITY_INTERNET)
+                .addCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                .build();
+        // Default values. Individual tests can override these.
+        when(mCm.getLinkProperties(any())).thenReturn(TEST_LINKPROPERTIES);
+        when(mCm.getNetworkCapabilities(any())).thenReturn(METERED_CAPABILITIES);
 
         setMinDataStallEvaluateInterval(500);
         setDataStallEvaluationType(1 << DATA_STALL_EVALUATION_TYPE_DNS);
@@ -160,10 +195,10 @@
     private class WrappedNetworkMonitor extends NetworkMonitor {
         private long mProbeTime = 0;
 
-        WrappedNetworkMonitor(Context context, Handler handler,
-                NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest,
+        WrappedNetworkMonitor(Context context, Network network, NetworkRequest defaultRequest,
                 IpConnectivityLog logger, Dependencies deps) {
-                super(context, handler, networkAgentInfo, defaultRequest, logger, deps);
+                super(context, mCallbacks, network, defaultRequest, logger,
+                        new SharedLog("test_nm"), deps);
         }
 
         @Override
@@ -176,19 +211,39 @@
         }
     }
 
-    WrappedNetworkMonitor makeMeteredWrappedNetworkMonitor() {
-        return new WrappedNetworkMonitor(
-                mContext, mHandler, mAgent, mRequest, mLogger, mDependencies);
+    private WrappedNetworkMonitor makeMeteredWrappedNetworkMonitor() {
+        final WrappedNetworkMonitor nm = new WrappedNetworkMonitor(
+                mContext, mNetwork, mRequest, mLogger, mDependencies);
+        when(mCm.getNetworkCapabilities(any())).thenReturn(METERED_CAPABILITIES);
+        nm.start();
+        waitForIdle(nm.getHandler());
+        return nm;
     }
 
-    WrappedNetworkMonitor makeNotMeteredWrappedNetworkMonitor() {
-        return new WrappedNetworkMonitor(
-                mContext, mHandler, mNotMeteredAgent, mRequest, mLogger, mDependencies);
+    private WrappedNetworkMonitor makeNotMeteredWrappedNetworkMonitor() {
+        final WrappedNetworkMonitor nm = new WrappedNetworkMonitor(
+                mContext, mNetwork, mRequest, mLogger, mDependencies);
+        when(mCm.getNetworkCapabilities(any())).thenReturn(NOT_METERED_CAPABILITIES);
+        nm.start();
+        waitForIdle(nm.getHandler());
+        return nm;
     }
 
-    NetworkMonitor makeMonitor() {
-        return new NetworkMonitor(
-                mContext, mHandler, mAgent, mRequest, mLogger, mDependencies);
+    private NetworkMonitor makeMonitor() {
+        final NetworkMonitor nm = new NetworkMonitor(
+                mContext, mCallbacks, mNetwork, mRequest, mLogger, mValidationLogger,
+                mDependencies);
+        nm.start();
+        waitForIdle(nm.getHandler());
+        return nm;
+    }
+
+    private void waitForIdle(Handler handler) {
+        final ConditionVariable cv = new ConditionVariable(false);
+        handler.post(cv::open);
+        if (!cv.block(HANDLER_TIMEOUT_MS)) {
+            fail("Timed out waiting for handler");
+        }
     }
 
     @Test
@@ -319,6 +374,15 @@
     }
 
     @Test
+    public void testIsCaptivePortal_IgnorePortals() throws IOException {
+        setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE);
+        setSslException(mHttpsConnection);
+        setPortal302(mHttpConnection);
+
+        assertNotPortal(makeMonitor().isCaptivePortal());
+    }
+
+    @Test
     public void testIsDataStall_EvaluationDisabled() {
         setDataStallEvaluationType(0);
         WrappedNetworkMonitor wrappedMonitor = makeMeteredWrappedNetworkMonitor();
@@ -390,6 +454,63 @@
         assertFalse(wrappedMonitor.isDataStall());
     }
 
+    @Test
+    public void testBrokenNetworkNotValidated() throws Exception {
+        setSslException(mHttpsConnection);
+        setStatus(mHttpConnection, 500);
+        setStatus(mFallbackConnection, 404);
+        when(mCm.getNetworkCapabilities(any())).thenReturn(METERED_CAPABILITIES);
+
+        final NetworkMonitor nm = makeMonitor();
+        nm.notifyNetworkConnected();
+
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .notifyNetworkTested(NETWORK_TEST_RESULT_INVALID, null);
+    }
+
+    @Test
+    public void testNoInternetCapabilityValidated() throws Exception {
+        when(mCm.getNetworkCapabilities(any())).thenReturn(NO_INTERNET_CAPABILITIES);
+
+        final NetworkMonitor nm = makeMonitor();
+        nm.notifyNetworkConnected();
+
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .notifyNetworkTested(NETWORK_TEST_RESULT_VALID, null);
+        verify(mNetwork, never()).openConnection(any());
+    }
+
+    @Test
+    public void testLaunchCaptivePortalApp() throws Exception {
+        setSslException(mHttpsConnection);
+        setPortal302(mHttpConnection);
+
+        final NetworkMonitor nm = makeMonitor();
+        nm.notifyNetworkConnected();
+
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .showProvisioningNotification(any());
+
+        // Check that startCaptivePortalApp sends the expected intent.
+        nm.launchCaptivePortalApp();
+
+        final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .startActivityAsUser(intentCaptor.capture(), eq(UserHandle.CURRENT));
+        final Intent intent = intentCaptor.getValue();
+        assertEquals(ACTION_CAPTIVE_PORTAL_SIGN_IN, intent.getAction());
+        final Network network = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK);
+        assertEquals(TEST_NETID, network.netId);
+
+        // Have the app report that the captive portal is dismissed, and check that we revalidate.
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+        final CaptivePortal captivePortal = intent.getParcelableExtra(EXTRA_CAPTIVE_PORTAL);
+        captivePortal.reportCaptivePortalDismissed();
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
+                .notifyNetworkTested(NETWORK_TEST_RESULT_VALID, null);
+    }
+
     private void makeDnsTimeoutEvent(WrappedNetworkMonitor wrappedMonitor, int count) {
         for (int i = 0; i < count; i++) {
             wrappedMonitor.getDnsStallDetector().accumulateConsecutiveDnsTimeoutCount(
@@ -440,6 +561,11 @@
                 eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS), any())).thenReturn(specs);
     }
 
+    private void setCaptivePortalMode(int mode) {
+        when(mDependencies.getSetting(any(),
+                eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt())).thenReturn(mode);
+    }
+
     private void assertPortal(CaptivePortalProbeResult result) {
         assertTrue(result.isPortal());
         assertFalse(result.isFailed());
@@ -459,12 +585,12 @@
     }
 
     private void setSslException(HttpURLConnection connection) throws IOException {
-        when(connection.getResponseCode()).thenThrow(new SSLHandshakeException("Invalid cert"));
+        doThrow(new SSLHandshakeException("Invalid cert")).when(connection).getResponseCode();
     }
 
     private void set302(HttpURLConnection connection, String location) throws IOException {
         setStatus(connection, 302);
-        when(connection.getHeaderField(LOCATION_HEADER)).thenReturn(location);
+        doReturn(location).when(connection).getHeaderField(LOCATION_HEADER);
     }
 
     private void setPortal302(HttpURLConnection connection) throws IOException {
@@ -472,7 +598,7 @@
     }
 
     private void setStatus(HttpURLConnection connection, int status) throws IOException {
-        when(connection.getResponseCode()).thenReturn(status);
+        doReturn(status).when(connection).getResponseCode();
     }
 }
 
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 66ceae4..d0666b9 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -25,6 +25,7 @@
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
+import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
@@ -37,6 +38,8 @@
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkPolicyManager.RULE_NONE;
 import static android.net.NetworkPolicyManager.uidRulesToString;
+import static android.net.NetworkStack.NETWORKSTACK_PACKAGE_NAME;
+import static android.net.shared.NetworkMonitorUtils.isValidationRequired;
 import static android.os.Process.INVALID_UID;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -62,6 +65,8 @@
 import android.net.INetd;
 import android.net.INetdEventCallback;
 import android.net.INetworkManagementEventObserver;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
 import android.net.INetworkPolicyListener;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
@@ -79,9 +84,11 @@
 import android.net.NetworkQuotaInfo;
 import android.net.NetworkRequest;
 import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
 import android.net.NetworkState;
 import android.net.NetworkUtils;
 import android.net.NetworkWatchlistManager;
+import android.net.PrivateDnsConfigParcel;
 import android.net.ProxyInfo;
 import android.net.RouteInfo;
 import android.net.UidRange;
@@ -90,12 +97,13 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.netlink.InetDiagMessage;
+import android.net.shared.NetworkMonitorUtils;
+import android.net.shared.PrivateDnsConfig;
 import android.net.util.MultinetworkPolicyTracker;
 import android.net.util.NetdService;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
@@ -123,8 +131,8 @@
 import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.LocalLog;
-import android.util.LocalLog.ReadOnlyLocalLog;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -149,7 +157,6 @@
 import com.android.server.am.BatteryStatsService;
 import com.android.server.connectivity.DataConnectionStats;
 import com.android.server.connectivity.DnsManager;
-import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
 import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
 import com.android.server.connectivity.IpConnectivityMetrics;
 import com.android.server.connectivity.KeepaliveTracker;
@@ -158,7 +165,6 @@
 import com.android.server.connectivity.MultipathPolicyTracker;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkDiagnostics;
-import com.android.server.connectivity.NetworkMonitor;
 import com.android.server.connectivity.NetworkNotificationManager;
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
 import com.android.server.connectivity.PermissionMonitor;
@@ -186,7 +192,6 @@
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -442,6 +447,43 @@
      */
     private static final int EVENT_DATA_SAVER_CHANGED = 40;
 
+     /**
+      * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
+      * been tested.
+      * obj = String representing URL that Internet probe was redirect to, if it was redirected.
+      * arg1 = One of the NETWORK_TESTED_RESULT_* constants.
+      * arg2 = NetID.
+      */
+    public static final int EVENT_NETWORK_TESTED = 41;
+
+    /**
+     * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the private DNS
+     * config was resolved.
+     * obj = PrivateDnsConfig
+     * arg2 = netid
+     */
+    public static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = 42;
+
+    /**
+     * Request ConnectivityService display provisioning notification.
+     * arg1    = Whether to make the notification visible.
+     * arg2    = NetID.
+     * obj     = Intent to be launched when notification selected by user, null if !arg1.
+     */
+    public static final int EVENT_PROVISIONING_NOTIFICATION = 43;
+
+    /**
+     * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
+     * should be shown.
+     */
+    public static final int PROVISIONING_NOTIFICATION_SHOW = 1;
+
+    /**
+     * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
+     * should be hidden.
+     */
+    public static final int PROVISIONING_NOTIFICATION_HIDE = 0;
+
     private static String eventName(int what) {
         return sMagicDecoderRing.get(what, Integer.toString(what));
     }
@@ -506,30 +548,6 @@
     private long mMaxWakelockDurationMs = 0;
     private long mLastWakeLockAcquireTimestamp = 0;
 
-    // Array of <Network,ReadOnlyLocalLogs> tracking network validation and results
-    private static final int MAX_VALIDATION_LOGS = 10;
-    private static class ValidationLog {
-        final Network mNetwork;
-        final String mName;
-        final ReadOnlyLocalLog mLog;
-
-        ValidationLog(Network network, String name, ReadOnlyLocalLog log) {
-            mNetwork = network;
-            mName = name;
-            mLog = log;
-        }
-    }
-    private final ArrayDeque<ValidationLog> mValidationLogs = new ArrayDeque<>(MAX_VALIDATION_LOGS);
-
-    private void addValidationLogs(ReadOnlyLocalLog log, Network network, String name) {
-        synchronized (mValidationLogs) {
-            while (mValidationLogs.size() >= MAX_VALIDATION_LOGS) {
-                mValidationLogs.removeLast();
-            }
-            mValidationLogs.addFirst(new ValidationLog(network, name, log));
-        }
-    }
-
     private final IpConnectivityLog mMetricsLog;
 
     @GuardedBy("mBandwidthRequests")
@@ -1684,7 +1702,11 @@
             // caller type. Need to re-factor NetdEventListenerService to allow multiple
             // NetworkMonitor registrants.
             if (nai != null && nai.satisfies(mDefaultRequest)) {
-                nai.networkMonitor.sendMessage(NetworkMonitor.EVENT_DNS_NOTIFICATION, returnCode);
+                try {
+                    nai.networkMonitor().notifyDnsResponse(returnCode);
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
             }
         }
     };
@@ -2266,17 +2288,6 @@
 
         if (ArrayUtils.contains(args, SHORT_ARG) == false) {
             pw.println();
-            synchronized (mValidationLogs) {
-                pw.println("mValidationLogs (most recent first):");
-                for (ValidationLog p : mValidationLogs) {
-                    pw.println(p.mNetwork + " - " + p.mName);
-                    pw.increaseIndent();
-                    p.mLog.dump(fd, pw, args);
-                    pw.decreaseIndent();
-                }
-            }
-
-            pw.println();
             pw.println("mNetworkRequestInfoLogs (most recent first):");
             pw.increaseIndent();
             mNetworkRequestInfoLogs.reverseDump(fd, pw, args);
@@ -2455,11 +2466,11 @@
             switch (msg.what) {
                 default:
                     return false;
-                case NetworkMonitor.EVENT_NETWORK_TESTED: {
+                case EVENT_NETWORK_TESTED: {
                     final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                     if (nai == null) break;
 
-                    final boolean valid = (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
+                    final boolean valid = (msg.arg1 == NETWORK_TEST_RESULT_VALID);
                     final boolean wasValidated = nai.lastValidated;
                     final boolean wasDefault = isDefaultNetwork(nai);
 
@@ -2497,7 +2508,7 @@
                     }
                     break;
                 }
-                case NetworkMonitor.EVENT_PROVISIONING_NOTIFICATION: {
+                case EVENT_PROVISIONING_NOTIFICATION: {
                     final int netId = msg.arg2;
                     final boolean visible = toBool(msg.arg1);
                     final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
@@ -2530,7 +2541,7 @@
                     }
                     break;
                 }
-                case NetworkMonitor.EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
+                case EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
                     final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                     if (nai == null) break;
 
@@ -2572,8 +2583,61 @@
         }
     }
 
+    private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
+        private final NetworkAgentInfo mNai;
+
+        private NetworkMonitorCallbacks(NetworkAgentInfo nai) {
+            mNai = nai;
+        }
+
+        @Override
+        public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
+                    new Pair<>(mNai, networkMonitor)));
+        }
+
+        @Override
+        public void notifyNetworkTested(int testResult, @Nullable String redirectUrl) {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(EVENT_NETWORK_TESTED,
+                    testResult, mNai.network.netId, redirectUrl));
+        }
+
+        @Override
+        public void notifyPrivateDnsConfigResolved(PrivateDnsConfigParcel config) {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PRIVATE_DNS_CONFIG_RESOLVED,
+                    0, mNai.network.netId, PrivateDnsConfig.fromParcel(config)));
+        }
+
+        @Override
+        public void showProvisioningNotification(String action) {
+            final Intent intent = new Intent(action);
+            intent.setPackage(NETWORKSTACK_PACKAGE_NAME);
+
+            final PendingIntent pendingIntent;
+            // Only the system server can register notifications with package "android"
+            final long token = Binder.clearCallingIdentity();
+            try {
+                pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_SHOW,
+                    mNai.network.netId,
+                    pendingIntent));
+        }
+
+        @Override
+        public void hideProvisioningNotification() {
+            mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+                    EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_HIDE,
+                    mNai.network.netId));
+        }
+    }
+
     private boolean networkRequiresValidation(NetworkAgentInfo nai) {
-        return NetworkMonitor.isValidationRequired(
+        return isValidationRequired(
                 mDefaultRequest.networkCapabilities, nai.networkCapabilities);
     }
 
@@ -2603,10 +2667,14 @@
         // Internet access and therefore also require validation.
         if (!networkRequiresValidation(nai)) return;
 
-        // Notify the NetworkMonitor thread in case it needs to cancel or
+        // Notify the NetworkAgentInfo/NetworkMonitor in case NetworkMonitor needs to cancel or
         // schedule DNS resolutions. If a DNS resolution is required the
         // result will be sent back to us.
-        nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);
+        try {
+            nai.networkMonitor().notifyPrivateDnsChanged(cfg.toParcel());
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
 
         // With Private DNS bypass support, we can proceed to update the
         // Private DNS config immediately, even if we're in strict mode
@@ -2736,7 +2804,11 @@
             // Disable wakeup packet monitoring for each interface.
             wakeupModifyInterface(iface, nai.networkCapabilities, false);
         }
-        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED);
+        try {
+            nai.networkMonitor().notifyNetworkDisconnected();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
         mNetworkAgentInfos.remove(nai.messenger);
         nai.maybeStopClat();
         synchronized (mNetworkForNetId) {
@@ -3096,7 +3168,11 @@
             NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
             if (nai == null) return;
             if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return;
-            nai.networkMonitor.sendMessage(NetworkMonitor.CMD_LAUNCH_CAPTIVE_PORTAL_APP);
+            try {
+                nai.networkMonitor().launchCaptivePortalApp();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
         });
     }
 
@@ -3217,6 +3293,11 @@
         return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
     }
 
+    @Override
+    public NetworkRequest getDefaultRequest() {
+        return mDefaultRequest;
+    }
+
     private class InternalHandler extends Handler {
         public InternalHandler(Looper looper) {
             super(looper);
@@ -3247,7 +3328,9 @@
                     break;
                 }
                 case EVENT_REGISTER_NETWORK_AGENT: {
-                    handleRegisterNetworkAgent((NetworkAgentInfo)msg.obj);
+                    final Pair<NetworkAgentInfo, INetworkMonitor> arg =
+                            (Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj;
+                    handleRegisterNetworkAgent(arg.first, arg.second);
                     break;
                 }
                 case EVENT_REGISTER_NETWORK_REQUEST:
@@ -3305,7 +3388,14 @@
                 }
                 case EVENT_SYSTEM_READY: {
                     for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
-                        nai.networkMonitor.systemReady = true;
+                        // Might have been called already in handleRegisterNetworkAgent since
+                        // mSystemReady is set before sending EVENT_SYSTEM_READY, but calling
+                        // this several times is fine.
+                        try {
+                            nai.networkMonitor().notifySystemReady();
+                        } catch (RemoteException e) {
+                            e.rethrowFromSystemServer();
+                        }
                     }
                     mMultipathPolicyTracker.start();
                     break;
@@ -3577,7 +3667,11 @@
         if (isNetworkWithLinkPropertiesBlocked(lp, uid, false)) {
             return;
         }
-        nai.networkMonitor.forceReevaluation(uid);
+        try {
+            nai.networkMonitor().forceReevaluation(uid);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
     }
 
     @Override
@@ -4785,27 +4879,49 @@
         final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
         final NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(),
                 new Network(reserveNetId()), new NetworkInfo(networkInfo), lp, nc, currentScore,
-                mContext, mTrackerHandler, new NetworkMisc(networkMisc), mDefaultRequest, this);
+                mContext, mTrackerHandler, new NetworkMisc(networkMisc), this);
         // Make sure the network capabilities reflect what the agent info says.
         nai.networkCapabilities = mixInCapabilities(nai, nc);
-        synchronized (this) {
-            nai.networkMonitor.systemReady = mSystemReady;
-        }
         final String extraInfo = networkInfo.getExtraInfo();
         final String name = TextUtils.isEmpty(extraInfo)
                 ? nai.networkCapabilities.getSSID() : extraInfo;
-        addValidationLogs(nai.networkMonitor.getValidationLogs(), nai.network, name);
         if (DBG) log("registerNetworkAgent " + nai);
-        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT, nai));
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mContext.getSystemService(NetworkStack.class)
+                    .makeNetworkMonitor(nai.network, name, new NetworkMonitorCallbacks(nai));
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        // NetworkAgentInfo registration will finish when the NetworkMonitor is created.
+        // If the network disconnects or sends any other event before that, messages are deferred by
+        // NetworkAgent until nai.asyncChannel.connect(), which will be called when finalizing the
+        // registration.
         return nai.network.netId;
     }
 
-    private void handleRegisterNetworkAgent(NetworkAgentInfo nai) {
+    private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
+        nai.onNetworkMonitorCreated(networkMonitor);
         if (VDBG) log("Got NetworkAgent Messenger");
         mNetworkAgentInfos.put(nai.messenger, nai);
         synchronized (mNetworkForNetId) {
             mNetworkForNetId.put(nai.network.netId, nai);
         }
+        synchronized (this) {
+            if (mSystemReady) {
+                try {
+                    networkMonitor.notifySystemReady();
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+            }
+        }
+
+        try {
+            networkMonitor.start();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
         nai.asyncChannel.connect(mContext, mTrackerHandler, nai.messenger);
         NetworkInfo networkInfo = nai.networkInfo;
         nai.networkInfo = null;
@@ -4855,6 +4971,11 @@
             networkAgent.updateClat(mNMS);
             notifyIfacesChangedForNetworkStats();
             if (networkAgent.everConnected) {
+                try {
+                    networkAgent.networkMonitor().notifyLinkPropertiesChanged();
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
                 notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
             }
         }
@@ -5092,6 +5213,11 @@
             // If the requestable capabilities have changed or the score changed, we can't have been
             // called by rematchNetworkAndRequests, so it's safe to start a rematch.
             rematchAllNetworksAndRequests(nai, oldScore);
+            try {
+                nai.networkMonitor().notifyNetworkCapabilitiesChanged();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
             notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
         }
 
@@ -5339,6 +5465,11 @@
         }
 
         if (capabilitiesChanged) {
+            try {
+                nai.networkMonitor().notifyNetworkCapabilitiesChanged();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
             notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
         }
 
@@ -5739,7 +5870,15 @@
             updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
                     null);
 
-            networkAgent.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+            // Until parceled LinkProperties are sent directly to NetworkMonitor, the connect
+            // command must be sent after updating LinkProperties to maximize chances of
+            // NetworkMonitor seeing the correct LinkProperties when starting.
+            // TODO: pass LinkProperties to the NetworkMonitor in the notifyNetworkConnected call.
+            try {
+                networkAgent.networkMonitor().notifyNetworkConnected();
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
             scheduleUnvalidatedPrompt(networkAgent);
 
             if (networkAgent.isVPN()) {
@@ -6020,7 +6159,7 @@
     @Override
     public String getCaptivePortalServerUrl() {
         enforceConnectivityInternalPermission();
-        return NetworkMonitor.getCaptivePortalServerHttpUrl(mContext);
+        return NetworkMonitorUtils.getCaptivePortalServerHttpUrl(mContext);
     }
 
     @Override
@@ -6113,12 +6252,6 @@
     }
 
     @VisibleForTesting
-    public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
-            NetworkAgentInfo nai, NetworkRequest defaultRequest) {
-        return new NetworkMonitor(context, handler, nai, defaultRequest);
-    }
-
-    @VisibleForTesting
     MultinetworkPolicyTracker createMultinetworkPolicyTracker(Context c, Handler h, Runnable r) {
         return new MultinetworkPolicyTracker(c, h, r);
     }
diff --git a/services/core/java/com/android/server/connectivity/ConnectivityConstants.java b/services/core/java/com/android/server/connectivity/ConnectivityConstants.java
index 24865bc..6fa98b8 100644
--- a/services/core/java/com/android/server/connectivity/ConnectivityConstants.java
+++ b/services/core/java/com/android/server/connectivity/ConnectivityConstants.java
@@ -21,22 +21,6 @@
  * @hide
  */
 public class ConnectivityConstants {
-    // IPC constants
-    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
-            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
-    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
-    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
-    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
-    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
-    public static final String EXTRA_CELL_ID = "extra_cellid";
-    public static final String EXTRA_SSID = "extra_ssid";
-    public static final String EXTRA_BSSID = "extra_bssid";
-    /** real time since boot */
-    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
-    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
-
-    public static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
-            "android.permission.ACCESS_NETWORK_CONDITIONS";
 
     // Penalty applied to scores of Networks that have not been validated.
     public static final int UNVALIDATED_SCORE_PENALTY = 40;
diff --git a/services/core/java/com/android/server/connectivity/DnsManager.java b/services/core/java/com/android/server/connectivity/DnsManager.java
index b8f057d..d8bb635 100644
--- a/services/core/java/com/android/server/connectivity/DnsManager.java
+++ b/services/core/java/com/android/server/connectivity/DnsManager.java
@@ -18,10 +18,9 @@
 
 import static android.net.ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE_FALLBACK;
 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF;
-import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
-import static android.provider.Settings.Global.DNS_RESOLVER_MIN_SAMPLES;
 import static android.provider.Settings.Global.DNS_RESOLVER_MAX_SAMPLES;
+import static android.provider.Settings.Global.DNS_RESOLVER_MIN_SAMPLES;
 import static android.provider.Settings.Global.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS;
 import static android.provider.Settings.Global.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT;
 import static android.provider.Settings.Global.PRIVATE_DNS_DEFAULT_MODE;
@@ -35,6 +34,7 @@
 import android.net.Network;
 import android.net.NetworkUtils;
 import android.net.Uri;
+import android.net.shared.PrivateDnsConfig;
 import android.os.Binder;
 import android.os.INetworkManagementService;
 import android.os.UserHandle;
@@ -43,10 +43,7 @@
 import android.util.Pair;
 import android.util.Slog;
 
-import com.android.server.connectivity.MockableSystemProperties;
-
 import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -54,10 +51,8 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
 import java.util.Set;
-import java.util.StringJoiner;
+import java.util.stream.Collectors;
 
 
 /**
@@ -123,43 +118,6 @@
     private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
     private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
 
-    public static class PrivateDnsConfig {
-        public final boolean useTls;
-        public final String hostname;
-        public final InetAddress[] ips;
-
-        public PrivateDnsConfig() {
-            this(false);
-        }
-
-        public PrivateDnsConfig(boolean useTls) {
-            this.useTls = useTls;
-            this.hostname = "";
-            this.ips = new InetAddress[0];
-        }
-
-        public PrivateDnsConfig(String hostname, InetAddress[] ips) {
-            this.useTls = !TextUtils.isEmpty(hostname);
-            this.hostname = useTls ? hostname : "";
-            this.ips = (ips != null) ? ips : new InetAddress[0];
-        }
-
-        public PrivateDnsConfig(PrivateDnsConfig cfg) {
-            useTls = cfg.useTls;
-            hostname = cfg.hostname;
-            ips = cfg.ips;
-        }
-
-        public boolean inStrictMode() {
-            return useTls && !TextUtils.isEmpty(hostname);
-        }
-
-        public String toString() {
-            return PrivateDnsConfig.class.getSimpleName() +
-                    "{" + useTls + ":" + hostname + "/" + Arrays.toString(ips) + "}";
-        }
-    }
-
     public static PrivateDnsConfig getPrivateDnsConfig(ContentResolver cr) {
         final String mode = getPrivateDnsMode(cr);
 
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 262184b..54c89aa 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -16,9 +16,8 @@
 
 package com.android.server.connectivity;
 
-import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
-
 import android.content.Context;
+import android.net.INetworkMonitor;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -29,7 +28,6 @@
 import android.os.Handler;
 import android.os.INetworkManagementService;
 import android.os.Messenger;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseArray;
@@ -37,11 +35,8 @@
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.WakeupMessage;
 import com.android.server.ConnectivityService;
-import com.android.server.connectivity.NetworkMonitor;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Comparator;
 import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -126,7 +121,6 @@
     public LinkProperties linkProperties;
     // This should only be modified via ConnectivityService.updateCapabilities().
     public NetworkCapabilities networkCapabilities;
-    public final NetworkMonitor networkMonitor;
     public final NetworkMisc networkMisc;
     // Indicates if netd has been told to create this Network. From this point on the appropriate
     // routing rules are setup and routes are added so packets can begin flowing over the Network.
@@ -239,6 +233,9 @@
     // Used by ConnectivityService to keep track of 464xlat.
     public Nat464Xlat clatd;
 
+    // Set after asynchronous creation of the NetworkMonitor.
+    private volatile INetworkMonitor mNetworkMonitor;
+
     private static final String TAG = ConnectivityService.class.getSimpleName();
     private static final boolean VDBG = false;
     private final ConnectivityService mConnService;
@@ -247,7 +244,7 @@
 
     public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
             LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
-            NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) {
+            NetworkMisc misc, ConnectivityService connService) {
         this.messenger = messenger;
         asyncChannel = ac;
         network = net;
@@ -258,10 +255,16 @@
         mConnService = connService;
         mContext = context;
         mHandler = handler;
-        networkMonitor = mConnService.createNetworkMonitor(context, handler, this, defaultRequest);
         networkMisc = misc;
     }
 
+    /**
+     * Inform NetworkAgentInfo that a new NetworkMonitor was created.
+     */
+    public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+        mNetworkMonitor = networkMonitor;
+    }
+
     public ConnectivityService connService() {
         return mConnService;
     }
@@ -278,6 +281,15 @@
         return network;
     }
 
+    /**
+     * Get the INetworkMonitor in this NetworkAgentInfo.
+     *
+     * <p>This will be null before {@link #onNetworkMonitorCreated(INetworkMonitor)} is called.
+     */
+    public INetworkMonitor networkMonitor() {
+        return mNetworkMonitor;
+    }
+
     // Functions for manipulating the requests satisfied by this network.
     //
     // These functions must only called on ConnectivityService's main thread.
diff --git a/services/net/Android.bp b/services/net/Android.bp
index ae697b7..3b4d6a7 100644
--- a/services/net/Android.bp
+++ b/services/net/Android.bp
@@ -11,10 +11,10 @@
     ]
 }
 
-// TODO: move to networking module with IpNeighborMonitor/ConnectivityPacketTracker and remove lib
-java_library {
-    name: "frameworks-net-shared-utils",
+filegroup {
+    name: "services-networkstack-shared-srcs",
     srcs: [
-        "java/android/net/util/FdEventsReader.java",
+        "java/android/net/util/FdEventsReader.java", // TODO: move to NetworkStack with IpClient
+        "java/android/net/shared/*.java",
     ]
-}
\ No newline at end of file
+}
diff --git a/services/net/java/android/net/shared/NetworkMonitorUtils.java b/services/net/java/android/net/shared/NetworkMonitorUtils.java
new file mode 100644
index 0000000..463cf2a
--- /dev/null
+++ b/services/net/java/android/net/shared/NetworkMonitorUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.shared;
+
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.provider.Settings;
+
+/** @hide */
+public class NetworkMonitorUtils {
+
+    // Network conditions broadcast constants
+    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
+            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
+    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
+    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
+    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
+    public static final String EXTRA_CELL_ID = "extra_cellid";
+    public static final String EXTRA_SSID = "extra_ssid";
+    public static final String EXTRA_BSSID = "extra_bssid";
+    /** real time since boot */
+    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
+    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
+    public static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
+            "android.permission.ACCESS_NETWORK_CONDITIONS";
+
+    // TODO: once the URL is a resource overlay, remove and have the resource define the default
+    private static final String DEFAULT_HTTP_URL =
+            "http://connectivitycheck.gstatic.com/generate_204";
+
+    /**
+     * Get the captive portal server HTTP URL that is configured on the device.
+     */
+    public static String getCaptivePortalServerHttpUrl(Context context) {
+        final String settingUrl = Settings.Global.getString(
+                context.getContentResolver(),
+                Settings.Global.CAPTIVE_PORTAL_HTTP_URL);
+        return settingUrl != null ? settingUrl : DEFAULT_HTTP_URL;
+    }
+
+    /**
+     * Return whether validation is required for a network.
+     * @param dfltNetCap Default requested network capabilities.
+     * @param nc Network capabilities of the network to test.
+     */
+    public static boolean isValidationRequired(
+            NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
+        // TODO: Consider requiring validation for DUN networks.
+        return dfltNetCap.satisfiedByNetworkCapabilities(nc);
+    }
+}
diff --git a/services/net/java/android/net/shared/PrivateDnsConfig.java b/services/net/java/android/net/shared/PrivateDnsConfig.java
new file mode 100644
index 0000000..41e0bad
--- /dev/null
+++ b/services/net/java/android/net/shared/PrivateDnsConfig.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.shared;
+
+import android.net.InetAddresses;
+import android.net.PrivateDnsConfigParcel;
+import android.text.TextUtils;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+
+/** @hide */
+public class PrivateDnsConfig {
+    public final boolean useTls;
+    public final String hostname;
+    public final InetAddress[] ips;
+
+    public PrivateDnsConfig() {
+        this(false);
+    }
+
+    public PrivateDnsConfig(boolean useTls) {
+        this.useTls = useTls;
+        this.hostname = "";
+        this.ips = new InetAddress[0];
+    }
+
+    public PrivateDnsConfig(String hostname, InetAddress[] ips) {
+        this.useTls = !TextUtils.isEmpty(hostname);
+        this.hostname = useTls ? hostname : "";
+        this.ips = (ips != null) ? ips : new InetAddress[0];
+    }
+
+    public PrivateDnsConfig(PrivateDnsConfig cfg) {
+        useTls = cfg.useTls;
+        hostname = cfg.hostname;
+        ips = cfg.ips;
+    }
+
+    /**
+     * Indicates whether this is a strict mode private DNS configuration.
+     */
+    public boolean inStrictMode() {
+        return useTls && !TextUtils.isEmpty(hostname);
+    }
+
+    @Override
+    public String toString() {
+        return PrivateDnsConfig.class.getSimpleName()
+                + "{" + useTls + ":" + hostname + "/" + Arrays.toString(ips) + "}";
+    }
+
+    /**
+     * Create a stable AIDL-compatible parcel from the current instance.
+     */
+    public PrivateDnsConfigParcel toParcel() {
+        final PrivateDnsConfigParcel parcel = new PrivateDnsConfigParcel();
+        parcel.hostname = hostname;
+
+        final String[] parceledIps = new String[ips.length];
+        for (int i = 0; i < ips.length; i++) {
+            parceledIps[i] = ips[i].getHostAddress();
+        }
+        parcel.ips = parceledIps;
+
+        return parcel;
+    }
+
+    /**
+     * Build a configuration from a stable AIDL-compatible parcel.
+     */
+    public static PrivateDnsConfig fromParcel(PrivateDnsConfigParcel parcel) {
+        final InetAddress[] ips = new InetAddress[parcel.ips.length];
+        for (int i = 0; i < ips.length; i++) {
+            ips[i] = InetAddresses.parseNumericAddress(parcel.ips[i]);
+        }
+
+        return new PrivateDnsConfig(parcel.hostname, ips);
+    }
+}
diff --git a/services/net/java/android/net/util/SharedLog.java b/services/net/java/android/net/util/SharedLog.java
index 8b7b59d..2cdb2b0 100644
--- a/services/net/java/android/net/util/SharedLog.java
+++ b/services/net/java/android/net/util/SharedLog.java
@@ -70,6 +70,10 @@
         mComponent = component;
     }
 
+    public String getTag() {
+        return mTag;
+    }
+
     /**
      * Create a SharedLog based on this log with an additional component prefix on each logged line.
      */
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 71529fd..bf39644 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -26,6 +26,8 @@
 import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
 import static android.net.ConnectivityManager.TYPE_NONE;
 import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
+import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -69,17 +71,19 @@
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
-
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -89,7 +93,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Resources;
-import android.net.CaptivePortal;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.ConnectivityManager.PacketKeepalive;
@@ -97,6 +100,8 @@
 import android.net.ConnectivityManager.TooManyRequestsException;
 import android.net.ConnectivityThread;
 import android.net.INetd;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
 import android.net.INetworkPolicyListener;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
@@ -114,12 +119,14 @@
 import android.net.NetworkMisc;
 import android.net.NetworkRequest;
 import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
 import android.net.NetworkUtils;
 import android.net.RouteInfo;
 import android.net.StringNetworkSpecifier;
 import android.net.UidRange;
-import android.net.captiveportal.CaptivePortalProbeResult;
 import android.net.metrics.IpConnectivityLog;
+import android.net.shared.NetworkMonitorUtils;
+import android.net.shared.PrivateDnsConfig;
 import android.net.util.MultinetworkPolicyTracker;
 import android.os.ConditionVariable;
 import android.os.Handler;
@@ -148,12 +155,9 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.connectivity.ConnectivityConstants;
 import com.android.server.connectivity.DefaultNetworkMetrics;
-import com.android.server.connectivity.DnsManager;
 import com.android.server.connectivity.IpConnectivityMetrics;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
-import com.android.server.connectivity.NetworkAgentInfo;
-import com.android.server.connectivity.NetworkMonitor;
 import com.android.server.connectivity.Tethering;
 import com.android.server.connectivity.Vpn;
 import com.android.server.net.NetworkPinner;
@@ -168,6 +172,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
+import org.mockito.stubbing.Answer;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
@@ -230,6 +235,7 @@
     @Mock INetworkStatsService mStatsService;
     @Mock INetworkPolicyManager mNpm;
     @Mock INetd mMockNetd;
+    @Mock NetworkStack mNetworkStack;
 
     private ArgumentCaptor<String[]> mStringArrayCaptor = ArgumentCaptor.forClass(String[].class);
 
@@ -299,6 +305,7 @@
         public Object getSystemService(String name) {
             if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm;
             if (Context.NOTIFICATION_SERVICE.equals(name)) return mock(NotificationManager.class);
+            if (Context.NETWORK_STACK_SERVICE.equals(name)) return mNetworkStack;
             return super.getSystemService(name);
         }
 
@@ -386,7 +393,7 @@
     }
 
     private class MockNetworkAgent {
-        private final WrappedNetworkMonitor mWrappedNetworkMonitor;
+        private final INetworkMonitor mNetworkMonitor;
         private final NetworkInfo mNetworkInfo;
         private final NetworkCapabilities mNetworkCapabilities;
         private final HandlerThread mHandlerThread;
@@ -402,6 +409,26 @@
         // mNetworkStatusReceived.
         private String mRedirectUrl;
 
+        private INetworkMonitorCallbacks mNmCallbacks;
+        private int mNmValidationResult = NETWORK_TEST_RESULT_INVALID;
+        private String mNmValidationRedirectUrl = null;
+        private boolean mNmProvNotificationRequested = false;
+
+        void setNetworkValid() {
+            mNmValidationResult = NETWORK_TEST_RESULT_VALID;
+            mNmValidationRedirectUrl = null;
+        }
+
+        void setNetworkInvalid() {
+            mNmValidationResult = NETWORK_TEST_RESULT_INVALID;
+            mNmValidationRedirectUrl = null;
+        }
+
+        void setNetworkPortal(String redirectUrl) {
+            setNetworkInvalid();
+            mNmValidationRedirectUrl = redirectUrl;
+        }
+
         MockNetworkAgent(int transport) {
             this(transport, new LinkProperties());
         }
@@ -434,6 +461,29 @@
             }
             mHandlerThread = new HandlerThread("Mock-" + typeName);
             mHandlerThread.start();
+
+            mNetworkMonitor = mock(INetworkMonitor.class);
+            final Answer validateAnswer = inv -> {
+                new Thread(this::onValidationRequested).start();
+                return null;
+            };
+
+            try {
+                doAnswer(validateAnswer).when(mNetworkMonitor).notifyNetworkConnected();
+                doAnswer(validateAnswer).when(mNetworkMonitor).forceReevaluation(anyInt());
+            } catch (RemoteException e) {
+                fail(e.getMessage());
+            }
+
+            final ArgumentCaptor<Network> nmNetworkCaptor =
+                    ArgumentCaptor.forClass(Network.class);
+            final ArgumentCaptor<INetworkMonitorCallbacks> nmCbCaptor =
+                    ArgumentCaptor.forClass(INetworkMonitorCallbacks.class);
+            doNothing().when(mNetworkStack).makeNetworkMonitor(
+                    nmNetworkCaptor.capture(),
+                    any() /* name */,
+                    nmCbCaptor.capture());
+
             mNetworkAgent = new NetworkAgent(mHandlerThread.getLooper(), mServiceContext,
                     "Mock-" + typeName, mNetworkInfo, mNetworkCapabilities,
                     linkProperties, mScore, new NetworkMisc()) {
@@ -465,10 +515,40 @@
                     mPreventReconnectReceived.open();
                 }
             };
+
+            assertEquals(mNetworkAgent.netId, nmNetworkCaptor.getValue().netId);
+            mNmCallbacks = nmCbCaptor.getValue();
+
+            try {
+                mNmCallbacks.onNetworkMonitorCreated(mNetworkMonitor);
+            } catch (RemoteException e) {
+                fail(e.getMessage());
+            }
+
             // Waits for the NetworkAgent to be registered, which includes the creation of the
             // NetworkMonitor.
             waitForIdle();
-            mWrappedNetworkMonitor = mService.getLastCreatedWrappedNetworkMonitor();
+        }
+
+        private void onValidationRequested() {
+            try {
+                if (mNmProvNotificationRequested
+                        && mNmValidationResult == NETWORK_TEST_RESULT_VALID) {
+                    mNmCallbacks.hideProvisioningNotification();
+                    mNmProvNotificationRequested = false;
+                }
+
+                mNmCallbacks.notifyNetworkTested(
+                        mNmValidationResult, mNmValidationRedirectUrl);
+
+                if (mNmValidationRedirectUrl != null) {
+                    mNmCallbacks.showProvisioningNotification(
+                            "test_provisioning_notif_action");
+                    mNmProvNotificationRequested = true;
+                }
+            } catch (RemoteException e) {
+                fail(e.getMessage());
+            }
         }
 
         public void adjustScore(int change) {
@@ -539,7 +619,7 @@
             NetworkCallback callback = null;
             final ConditionVariable validatedCv = new ConditionVariable();
             if (validated) {
-                mWrappedNetworkMonitor.gen204ProbeResult = 204;
+                setNetworkValid();
                 NetworkRequest request = new NetworkRequest.Builder()
                         .addTransportType(mNetworkCapabilities.getTransportTypes()[0])
                         .clearCapabilities()
@@ -564,15 +644,14 @@
             if (validated) {
                 // Wait for network to validate.
                 waitFor(validatedCv);
-                mWrappedNetworkMonitor.gen204ProbeResult = 500;
+                setNetworkInvalid();
             }
 
             if (callback != null) mCm.unregisterNetworkCallback(callback);
         }
 
         public void connectWithCaptivePortal(String redirectUrl) {
-            mWrappedNetworkMonitor.gen204ProbeResult = 200;
-            mWrappedNetworkMonitor.gen204ProbeRedirectUrl = redirectUrl;
+            setNetworkPortal(redirectUrl);
             connect(false);
         }
 
@@ -603,10 +682,6 @@
             return mDisconnected;
         }
 
-        public WrappedNetworkMonitor getWrappedNetworkMonitor() {
-            return mWrappedNetworkMonitor;
-        }
-
         public void sendLinkProperties(LinkProperties lp) {
             mNetworkAgent.sendLinkProperties(lp);
         }
@@ -880,28 +955,6 @@
         }
     }
 
-    // NetworkMonitor implementation allowing overriding of Internet connectivity probe result.
-    private class WrappedNetworkMonitor extends NetworkMonitor {
-        public final Handler connectivityHandler;
-        // HTTP response code fed back to NetworkMonitor for Internet connectivity probe.
-        public int gen204ProbeResult = 500;
-        public String gen204ProbeRedirectUrl = null;
-
-        public WrappedNetworkMonitor(Context context, Handler handler,
-                NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest,
-                IpConnectivityLog log) {
-            super(context, handler, networkAgentInfo, defaultRequest, log,
-                    NetworkMonitor.Dependencies.DEFAULT);
-            connectivityHandler = handler;
-        }
-
-        @Override
-        protected CaptivePortalProbeResult isCaptivePortal() {
-            if (!mIsCaptivePortalCheckEnabled) { return new CaptivePortalProbeResult(204); }
-            return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl, null);
-        }
-    }
-
     private class WrappedMultinetworkPolicyTracker extends MultinetworkPolicyTracker {
         public volatile boolean configRestrictsAvoidBadWifi;
         public volatile int configMeteredMultipathPreference;
@@ -923,7 +976,6 @@
 
     private class WrappedConnectivityService extends ConnectivityService {
         public WrappedMultinetworkPolicyTracker wrappedMultinetworkPolicyTracker;
-        private WrappedNetworkMonitor mLastCreatedNetworkMonitor;
         private MockableSystemProperties mSystemProperties;
 
         public WrappedConnectivityService(Context context, INetworkManagementService netManager,
@@ -971,15 +1023,6 @@
             }
         }
 
-        @Override
-        public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
-                NetworkAgentInfo nai, NetworkRequest defaultRequest) {
-            final WrappedNetworkMonitor monitor = new WrappedNetworkMonitor(
-                    context, handler, nai, defaultRequest, mock(IpConnectivityLog.class));
-            mLastCreatedNetworkMonitor = monitor;
-            return monitor;
-        }
-
         public Nat464Xlat getNat464Xlat(MockNetworkAgent mna) {
             return getNetworkAgentInfoForNetwork(mna.getNetwork()).clatd;
         }
@@ -1017,10 +1060,6 @@
         protected void registerNetdEventCallback() {
         }
 
-        public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
-            return mLastCreatedNetworkMonitor;
-        }
-
         public void mockVpn(int uid) {
             synchronized (mVpns) {
                 int userId = UserHandle.getUserId(uid);
@@ -2439,7 +2478,7 @@
 
         // Make captive portal disappear then revalidate.
         // Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 204;
+        mWiFiNetworkAgent.setNetworkValid();
         mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
         captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
 
@@ -2448,13 +2487,13 @@
 
         // Break network connectivity.
         // Expect NET_CAPABILITY_VALIDATED onLost callback.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 500;
+        mWiFiNetworkAgent.setNetworkInvalid();
         mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
         validatedCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
     }
 
     @Test
-    public void testCaptivePortalApp() {
+    public void testCaptivePortalApp() throws RemoteException {
         final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
         final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
                 .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
@@ -2477,21 +2516,19 @@
         mServiceContext.expectNoStartActivityIntent(fastTimeoutMs);
 
         // Turn into a captive portal.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 302;
+        mWiFiNetworkAgent.setNetworkPortal("http://example.com");
         mCm.reportNetworkConnectivity(wifiNetwork, false);
         captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
         validatedCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
 
-        // Check that startCaptivePortalApp sends the expected intent.
+        // Check that startCaptivePortalApp sends the expected command to NetworkMonitor.
         mCm.startCaptivePortalApp(wifiNetwork);
-        Intent intent = mServiceContext.expectStartActivityIntent(TIMEOUT_MS);
-        assertEquals(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN, intent.getAction());
-        assertEquals(wifiNetwork, intent.getExtra(ConnectivityManager.EXTRA_NETWORK));
+        verify(mWiFiNetworkAgent.mNetworkMonitor, timeout(TIMEOUT_MS).times(1))
+                .launchCaptivePortalApp();
 
-        // Have the app report that the captive portal is dismissed, and check that we revalidate.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 204;
-        CaptivePortal c = (CaptivePortal) intent.getExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
-        c.reportCaptivePortalDismissed();
+        // Report that the captive portal is dismissed, and check that callbacks are fired
+        mWiFiNetworkAgent.setNetworkValid();
+        mWiFiNetworkAgent.mNetworkMonitor.forceReevaluation(Process.myUid());
         validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
         captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
 
@@ -2524,20 +2561,6 @@
         waitFor(avoidCv);
 
         assertNoCallbacks(captivePortalCallback, validatedCallback);
-
-        // Now test ignore mode.
-        setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_IGNORE);
-
-        // Bring up a network with a captive portal.
-        // Since we're ignoring captive portals, the network will validate.
-        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
-        String secondRedirectUrl = "http://example.com/secondPath";
-        mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl);
-
-        // Expect NET_CAPABILITY_VALIDATED onAvailable callback.
-        validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
-        // But there should be no CaptivePortal callback.
-        captivePortalCallback.assertNoCallback();
     }
 
     private NetworkRequest.Builder newWifiRequestBuilder() {
@@ -3169,7 +3192,7 @@
         Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
 
         // Fail validation on wifi.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+        mWiFiNetworkAgent.setNetworkInvalid();
         mCm.reportNetworkConnectivity(wifiNetwork, false);
         defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
         validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
@@ -3213,7 +3236,7 @@
         wifiNetwork = mWiFiNetworkAgent.getNetwork();
 
         // Fail validation on wifi and expect the dialog to appear.
-        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+        mWiFiNetworkAgent.setNetworkInvalid();
         mCm.reportNetworkConnectivity(wifiNetwork, false);
         defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
         validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
@@ -4002,11 +4025,9 @@
         final String TLS_SERVER6 = "2001:db8:53::53";
         final InetAddress[] TLS_IPS = new InetAddress[]{ InetAddress.getByName(TLS_SERVER6) };
         final String[] TLS_SERVERS = new String[]{ TLS_SERVER6 };
-        final Handler h = mCellNetworkAgent.getWrappedNetworkMonitor().connectivityHandler;
-        h.sendMessage(h.obtainMessage(
-                NetworkMonitor.EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0,
-                mCellNetworkAgent.getNetwork().netId,
-                new DnsManager.PrivateDnsConfig(TLS_SPECIFIER, TLS_IPS)));
+        mCellNetworkAgent.mNmCallbacks.notifyPrivateDnsConfigResolved(
+                new PrivateDnsConfig(TLS_SPECIFIER, TLS_IPS).toParcel());
+
         waitForIdle();
         verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                 anyInt(), mStringArrayCaptor.capture(), any(), any(),
@@ -4294,6 +4315,12 @@
         ranges.add(new UidRange(uid, uid));
         mMockVpn.setNetworkAgent(vpnNetworkAgent);
         mMockVpn.setUids(ranges);
+        // VPN networks do not satisfy the default request and are automatically validated
+        // by NetworkMonitor
+        assertFalse(NetworkMonitorUtils.isValidationRequired(
+                mCm.getDefaultRequest().networkCapabilities, vpnNetworkAgent.mNetworkCapabilities));
+        vpnNetworkAgent.setNetworkValid();
+
         vpnNetworkAgent.connect(false);
         mMockVpn.connect();
 
diff --git a/tests/net/java/com/android/server/connectivity/DnsManagerTest.java b/tests/net/java/com/android/server/connectivity/DnsManagerTest.java
index 01b468a..38322e9 100644
--- a/tests/net/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/net/java/com/android/server/connectivity/DnsManagerTest.java
@@ -17,7 +17,6 @@
 package com.android.server.connectivity;
 
 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF;
-import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 import static android.provider.Settings.Global.PRIVATE_DNS_DEFAULT_MODE;
 import static android.provider.Settings.Global.PRIVATE_DNS_MODE;
@@ -29,13 +28,13 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.RouteInfo;
+import android.net.shared.PrivateDnsConfig;
 import android.os.INetworkManagementService;
 import android.provider.Settings;
 import android.support.test.filters.SmallTest;
@@ -43,18 +42,16 @@
 import android.test.mock.MockContentResolver;
 
 import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
-import com.android.server.connectivity.MockableSystemProperties;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.net.InetAddress;
 import java.util.Arrays;
 
-import org.junit.runner.RunWith;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
 /**
  * Tests for {@link DnsManager}.
  *
@@ -133,7 +130,7 @@
                 PRIVATE_DNS_MODE, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
         Settings.Global.putString(mContentResolver, PRIVATE_DNS_SPECIFIER, "strictmode.com");
         mDnsManager.updatePrivateDns(new Network(TEST_NETID),
-                new DnsManager.PrivateDnsConfig("strictmode.com", new InetAddress[] {
+                new PrivateDnsConfig("strictmode.com", new InetAddress[] {
                     InetAddress.parseNumericAddress("6.6.6.6"),
                     InetAddress.parseNumericAddress("2001:db8:66:66::1")
                     }));
diff --git a/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java b/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
index 354cf2f..4c52d81 100644
--- a/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/LingerMonitorTest.java
@@ -23,10 +23,10 @@
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.reset;
 
 import android.app.PendingIntent;
 import android.content.Context;
@@ -36,18 +36,18 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
 import android.net.NetworkMisc;
-import android.support.test.runner.AndroidJUnit4;
+import android.net.NetworkStack;
 import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
 import android.text.format.DateUtils;
 
 import com.android.internal.R;
 import com.android.server.ConnectivityService;
-import com.android.server.connectivity.NetworkNotificationManager;
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
 
-import org.junit.runner.RunWith;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -70,13 +70,16 @@
     @Mock NetworkMisc mMisc;
     @Mock NetworkNotificationManager mNotifier;
     @Mock Resources mResources;
+    @Mock NetworkStack mNetworkStack;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         when(mCtx.getResources()).thenReturn(mResources);
         when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity");
-        when(mConnService.createNetworkMonitor(any(), any(), any(), any())).thenReturn(null);
+        when(mCtx.getSystemServiceName(NetworkStack.class))
+                .thenReturn(Context.NETWORK_STACK_SERVICE);
+        when(mCtx.getSystemService(Context.NETWORK_STACK_SERVICE)).thenReturn(mNetworkStack);
 
         mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT);
     }
@@ -349,7 +352,7 @@
         caps.addCapability(0);
         caps.addTransportType(transport);
         NetworkAgentInfo nai = new NetworkAgentInfo(null, null, new Network(netId), info, null,
-                caps, 50, mCtx, null, mMisc, null, mConnService);
+                caps, 50, mCtx, null, mMisc, mConnService);
         nai.everValidated = true;
         return nai;
     }