Merge "Rename FIRST_SDK_INT to DEVICE_INITIAL_SDK_INT" into sc-dev
diff --git a/framework/api/current.txt b/framework/api/current.txt
index ac5cb70..13ecb12 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -411,6 +411,8 @@
   }
 
   public class ParseException extends java.lang.RuntimeException {
+    ctor public ParseException(@NonNull String);
+    ctor public ParseException(@NonNull String, @NonNull Throwable);
     field public String response;
   }
 
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 8dfdd61..4719772 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -133,21 +133,6 @@
     method @NonNull public android.net.NetworkRequest.Builder setUids(@Nullable java.util.Set<android.util.Range<java.lang.Integer>>);
   }
 
-  public class ParseException extends java.lang.RuntimeException {
-    ctor public ParseException(@NonNull String);
-    ctor public ParseException(@NonNull String, @NonNull Throwable);
-  }
-
-  public final class TcpRepairWindow {
-    ctor public TcpRepairWindow(int, int, int, int, int, int);
-    field public final int maxWindow;
-    field public final int rcvWnd;
-    field public final int rcvWndScale;
-    field public final int rcvWup;
-    field public final int sndWl1;
-    field public final int sndWnd;
-  }
-
   public final class TestNetworkInterface implements android.os.Parcelable {
     ctor public TestNetworkInterface(@NonNull android.os.ParcelFileDescriptor, @NonNull String);
     method public int describeContents();
@@ -158,10 +143,10 @@
   }
 
   public class TestNetworkManager {
-    method @NonNull public android.net.TestNetworkInterface createTapInterface();
-    method @NonNull public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>);
-    method public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder);
-    method public void teardownTestNetwork(@NonNull android.net.Network);
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTapInterface();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public android.net.TestNetworkInterface createTunInterface(@NonNull java.util.Collection<android.net.LinkAddress>);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void setupTestNetwork(@NonNull String, @NonNull android.os.IBinder);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_TEST_NETWORKS) public void teardownTestNetwork(@NonNull android.net.Network);
     field public static final String TEST_TAP_PREFIX = "testtap";
   }
 
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 847bcbc..63e0fe9 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -260,15 +260,15 @@
   public static final class NetworkAgentConfig.Builder {
     ctor public NetworkAgentConfig.Builder();
     method @NonNull public android.net.NetworkAgentConfig build();
-    method @NonNull public android.net.NetworkAgentConfig.Builder disableNat64Detection();
-    method @NonNull public android.net.NetworkAgentConfig.Builder disableProvisioningNotification();
     method @NonNull public android.net.NetworkAgentConfig.Builder setExplicitlySelected(boolean);
     method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyExtraInfo(@NonNull String);
     method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubType(int);
     method @NonNull public android.net.NetworkAgentConfig.Builder setLegacySubTypeName(@NonNull String);
     method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyType(int);
     method @NonNull public android.net.NetworkAgentConfig.Builder setLegacyTypeName(@NonNull String);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setNat64DetectionEnabled(boolean);
     method @NonNull public android.net.NetworkAgentConfig.Builder setPartialConnectivityAcceptable(boolean);
+    method @NonNull public android.net.NetworkAgentConfig.Builder setProvisioningNotificationEnabled(boolean);
     method @NonNull public android.net.NetworkAgentConfig.Builder setUnvalidatedConnectivityAcceptable(boolean);
   }
 
@@ -471,14 +471,14 @@
   public final class TcpKeepalivePacketData extends android.net.KeepalivePacketData implements android.os.Parcelable {
     ctor public TcpKeepalivePacketData(@NonNull java.net.InetAddress, int, @NonNull java.net.InetAddress, int, @NonNull byte[], int, int, int, int, int, int) throws android.net.InvalidPacketException;
     method public int describeContents();
+    method public int getIpTos();
+    method public int getIpTtl();
+    method public int getTcpAck();
+    method public int getTcpSeq();
+    method public int getTcpWindow();
+    method public int getTcpWindowScale();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TcpKeepalivePacketData> CREATOR;
-    field public final int ipTos;
-    field public final int ipTtl;
-    field public final int tcpAck;
-    field public final int tcpSeq;
-    field public final int tcpWindow;
-    field public final int tcpWindowScale;
   }
 
 }
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 3a9731f..c4a0d69 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -44,6 +44,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
 import android.net.IpSecManager.UdpEncapsulationSocket;
 import android.net.SocketKeepalive.Callback;
 import android.net.TetheringManager.StartTetheringCallback;
@@ -5159,10 +5160,13 @@
      *
      * <p>This method should only be used for tests.
      *
-     * <p>The caller must be the owner of the specified Network.
+     * <p>The caller must be the owner of the specified Network. This simulates a data stall to
+     * have the system behave as if it had happened, but does not actually stall connectivity.
      *
      * @param detectionMethod The detection method used to identify the Data Stall.
-     * @param timestampMillis The timestamp at which the stall 'occurred', in milliseconds.
+     *                        See ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_*.
+     * @param timestampMillis The timestamp at which the stall 'occurred', in milliseconds, as per
+     *                        SystemClock.elapsedRealtime.
      * @param network The Network for which a Data Stall is being simluated.
      * @param extras The PersistableBundle of extras included in the Data Stall notification.
      * @throws SecurityException if the caller is not the owner of the given network.
@@ -5171,7 +5175,7 @@
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_TEST_NETWORKS,
             android.Manifest.permission.NETWORK_STACK})
-    public void simulateDataStall(int detectionMethod, long timestampMillis,
+    public void simulateDataStall(@DetectionMethod int detectionMethod, long timestampMillis,
             @NonNull Network network, @NonNull PersistableBundle extras) {
         try {
             mService.simulateDataStall(detectionMethod, timestampMillis, network, extras);
diff --git a/framework/src/android/net/NetworkAgentConfig.java b/framework/src/android/net/NetworkAgentConfig.java
index 3f058d8..ad8396b 100644
--- a/framework/src/android/net/NetworkAgentConfig.java
+++ b/framework/src/android/net/NetworkAgentConfig.java
@@ -311,26 +311,28 @@
         }
 
         /**
-         * Disables active detection of NAT64 (e.g., via RFC 7050 DNS lookups). Used to save power
-         * and reduce idle traffic on networks that are known to be IPv6-only without a NAT64.
+         * Enables or disables active detection of NAT64 (e.g., via RFC 7050 DNS lookups). Used to
+         * save power and reduce idle traffic on networks that are known to be IPv6-only without a
+         * NAT64. By default, NAT64 detection is enabled.
          *
          * @return this builder, to facilitate chaining.
          */
         @NonNull
-        public Builder disableNat64Detection() {
-            mConfig.skip464xlat = true;
+        public Builder setNat64DetectionEnabled(boolean enabled) {
+            mConfig.skip464xlat = !enabled;
             return this;
         }
 
         /**
-         * Disables the "Sign in to network" notification. Used if the network transport will
-         * perform its own carrier-specific provisioning procedure.
+         * Enables or disables the "Sign in to network" notification. Used if the network transport
+         * will perform its own carrier-specific provisioning procedure. By default, the
+         * notification is enabled.
          *
          * @return this builder, to facilitate chaining.
          */
         @NonNull
-        public Builder disableProvisioningNotification() {
-            mConfig.provisioningNotificationDisabled = true;
+        public Builder setProvisioningNotificationEnabled(boolean enabled) {
+            mConfig.provisioningNotificationDisabled = !enabled;
             return this;
         }
 
diff --git a/framework/src/android/net/NetworkScore.java b/framework/src/android/net/NetworkScore.java
index 0dee225..b0752e9 100644
--- a/framework/src/android/net/NetworkScore.java
+++ b/framework/src/android/net/NetworkScore.java
@@ -93,6 +93,10 @@
         mKeepConnectedReason = in.readInt();
     }
 
+    /**
+     * Get the legacy int score embedded in this NetworkScore.
+     * @see Builder#setLegacyInt(int)
+     */
     public int getLegacyInt() {
         return mLegacyInt;
     }
@@ -212,7 +216,9 @@
         /**
          * Sets the legacy int for this score.
          *
-         * Do not rely on this. It will be gone by the time S is released.
+         * This will be used for measurements and logs, but will no longer be used for ranking
+         * networks against each other. Callers that existed before Android S should send what
+         * they used to send as the int score.
          *
          * @param score the legacy int
          * @return this
diff --git a/framework/src/android/net/OemNetworkPreferences.java b/framework/src/android/net/OemNetworkPreferences.java
index 5a76cd6..2bb006d 100644
--- a/framework/src/android/net/OemNetworkPreferences.java
+++ b/framework/src/android/net/OemNetworkPreferences.java
@@ -40,6 +40,23 @@
  */
 @SystemApi
 public final class OemNetworkPreferences implements Parcelable {
+    // Valid production preferences must be > 0, negative values reserved for testing
+    /**
+     * This preference is only to be used for testing and nothing else.
+     * Use only TRANSPORT_TEST transport networks.
+     * @hide
+     */
+    public static final int OEM_NETWORK_PREFERENCE_TEST_ONLY = -2;
+
+    /**
+     * This preference is only to be used for testing and nothing else.
+     * If an unmetered network is available, use it.
+     * Otherwise, if a network with the TRANSPORT_TEST transport is available, use it.
+     * Otherwise, use the general default network.
+     * @hide
+     */
+    public static final int OEM_NETWORK_PREFERENCE_TEST = -1;
+
     /**
      * Default in case this value is not set. Using it will result in an error.
      */
@@ -69,6 +86,12 @@
      */
     public static final int OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY = 4;
 
+    /**
+     * The max allowed value for an OEM network preference.
+     * @hide
+     */
+    public static final int OEM_NETWORK_PREFERENCE_MAX = OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY;
+
     @NonNull
     private final Bundle mNetworkMappings;
 
@@ -96,7 +119,7 @@
 
     @Override
     public String toString() {
-        return "OemNetworkPreferences{" + "mNetworkMappings=" + mNetworkMappings + '}';
+        return "OemNetworkPreferences{" + "mNetworkMappings=" + getNetworkPreferences() + '}';
     }
 
     @Override
@@ -185,6 +208,8 @@
 
     /** @hide */
     @IntDef(prefix = "OEM_NETWORK_PREFERENCE_", value = {
+            OEM_NETWORK_PREFERENCE_TEST_ONLY,
+            OEM_NETWORK_PREFERENCE_TEST,
             OEM_NETWORK_PREFERENCE_UNINITIALIZED,
             OEM_NETWORK_PREFERENCE_OEM_PAID,
             OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK,
@@ -205,6 +230,10 @@
     @NonNull
     public static String oemNetworkPreferenceToString(@OemNetworkPreference int value) {
         switch (value) {
+            case OEM_NETWORK_PREFERENCE_TEST_ONLY:
+                return "OEM_NETWORK_PREFERENCE_TEST_ONLY";
+            case OEM_NETWORK_PREFERENCE_TEST:
+                return "OEM_NETWORK_PREFERENCE_TEST";
             case OEM_NETWORK_PREFERENCE_UNINITIALIZED:
                 return "OEM_NETWORK_PREFERENCE_UNINITIALIZED";
             case OEM_NETWORK_PREFERENCE_OEM_PAID:
diff --git a/framework/src/android/net/ParseException.java b/framework/src/android/net/ParseException.java
index ca6d012..9d4727a 100644
--- a/framework/src/android/net/ParseException.java
+++ b/framework/src/android/net/ParseException.java
@@ -17,7 +17,6 @@
 package android.net;
 
 import android.annotation.NonNull;
-import android.annotation.SystemApi;
 
 /**
  * Thrown when parsing failed.
@@ -26,15 +25,11 @@
 public class ParseException extends RuntimeException {
     public String response;
 
-    /** @hide */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public ParseException(@NonNull String response) {
         super(response);
         this.response = response;
     }
 
-    /** @hide */
-    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public ParseException(@NonNull String response, @NonNull Throwable cause) {
         super(response, cause);
         this.response = response;
diff --git a/framework/src/android/net/TcpKeepalivePacketData.java b/framework/src/android/net/TcpKeepalivePacketData.java
index ddb3a6a..c2c4f32 100644
--- a/framework/src/android/net/TcpKeepalivePacketData.java
+++ b/framework/src/android/net/TcpKeepalivePacketData.java
@@ -32,22 +32,39 @@
 public final class TcpKeepalivePacketData extends KeepalivePacketData implements Parcelable {
     private static final String TAG = "TcpKeepalivePacketData";
 
-    /** TCP sequence number. */
+    /**
+     * TCP sequence number.
+     * @hide
+     */
     public final int tcpSeq;
 
-    /** TCP ACK number. */
+    /**
+     * TCP ACK number.
+     * @hide
+     */
     public final int tcpAck;
 
-    /** TCP RCV window. */
+    /**
+     * TCP RCV window.
+     * @hide
+     */
     public final int tcpWindow;
 
-    /** TCP RCV window scale. */
+    /** TCP RCV window scale.
+     * @hide
+     */
     public final int tcpWindowScale;
 
-    /** IP TOS. */
+    /**
+     * IP TOS.
+     * @hide
+     */
     public final int ipTos;
 
-    /** IP TTL. */
+    /**
+     * IP TTL.
+     * @hide
+     */
     public final int ipTtl;
 
     public TcpKeepalivePacketData(@NonNull final InetAddress srcAddress, int srcPort,
@@ -63,6 +80,56 @@
         this.ipTtl = ipTtl;
     }
 
+    /**
+     * Get the TCP sequence number.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpSeq() {
+        return tcpSeq;
+    }
+
+    /**
+     * Get the TCP ACK number.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpAck() {
+        return tcpAck;
+    }
+
+    /**
+     * Get the TCP RCV window.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpWindow() {
+        return tcpWindow;
+    }
+
+    /**
+     * Get the TCP RCV window scale.
+     *
+     * See https://tools.ietf.org/html/rfc793#page-15.
+     */
+    public int getTcpWindowScale() {
+        return tcpWindowScale;
+    }
+
+    /**
+     * Get the IP type of service.
+     */
+    public int getIpTos() {
+        return ipTos;
+    }
+
+    /**
+     * Get the IP TTL.
+     */
+    public int getIpTtl() {
+        return ipTtl;
+    }
+
     @Override
     public boolean equals(@Nullable final Object o) {
         if (!(o instanceof TcpKeepalivePacketData)) return false;
diff --git a/framework/src/android/net/TcpRepairWindow.java b/framework/src/android/net/TcpRepairWindow.java
index f062fa9..86034f0 100644
--- a/framework/src/android/net/TcpRepairWindow.java
+++ b/framework/src/android/net/TcpRepairWindow.java
@@ -16,15 +16,12 @@
 
 package android.net;
 
-import android.annotation.SystemApi;
-
 /**
  * Corresponds to C's {@code struct tcp_repair_window} from
  * include/uapi/linux/tcp.h
  *
  * @hide
  */
-@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 public final class TcpRepairWindow {
     public final int sndWl1;
     public final int sndWnd;
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index a7a6235..9ddd2f5 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -15,8 +15,10 @@
  */
 package android.net;
 
+import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -58,6 +60,7 @@
      * @param network The test network that should be torn down
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public void teardownTestNetwork(@NonNull Network network) {
         try {
@@ -103,6 +106,7 @@
      * @param binder A binder object guarding the lifecycle of this test network.
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     public void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) {
         setupTestNetwork(iface, null, true, new int[0], binder);
@@ -145,6 +149,7 @@
      *     TUN interface.
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @NonNull
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
@@ -163,6 +168,7 @@
      *     TAP interface.
      * @hide
      */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @NonNull
     public TestNetworkInterface createTapInterface() {
diff --git a/service/Android.bp b/service/Android.bp
index 518f198..20ccf06 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -113,7 +113,6 @@
     ],
     jarjar_rules: "jarjar-rules.txt",
     apex_available: [
-        "//apex_available:platform", // For arc-services
         "com.android.tethering",
     ],
 }
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 9ff2a22..82d8e4f 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -123,4 +123,8 @@
     <string-array translatable="false" name="config_networkNotifySwitches">
     </string-array>
 
+    <!-- Whether to use an ongoing notification for signing in to captive portals, instead of a
+         notification that can be dismissed. -->
+    <bool name="config_ongoingSignInNotification">false</bool>
+
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 717d08e..568deca 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -32,6 +32,7 @@
             <item type="integer" name="config_networkWakeupPacketMask"/>
             <item type="integer" name="config_networkNotifySwitchType"/>
             <item type="array" name="config_networkNotifySwitches"/>
+            <item type="bool" name="config_ongoingSignInNotification"/>
 
         </policy>
     </overlayable>
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 922b21a..6b7c132 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -2279,7 +2279,9 @@
                 netId = nai.network.getNetId();
             }
             boolean ok = addLegacyRouteToHost(lp, addr, netId, uid);
-            if (DBG) log("requestRouteToHostAddress ok=" + ok);
+            if (DBG) {
+                log("requestRouteToHostAddress " + addr + nai.toShortString() + " ok=" + ok);
+            }
             return ok;
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -2903,13 +2905,13 @@
             pw.println();
             pw.println("mNetworkRequestInfoLogs (most recent first):");
             pw.increaseIndent();
-            mNetworkRequestInfoLogs.reverseDump(fd, pw, args);
+            mNetworkRequestInfoLogs.reverseDump(pw);
             pw.decreaseIndent();
 
             pw.println();
             pw.println("mNetworkInfoBlockingLogs (most recent first):");
             pw.increaseIndent();
-            mNetworkInfoBlockingLogs.reverseDump(fd, pw, args);
+            mNetworkInfoBlockingLogs.reverseDump(pw);
             pw.decreaseIndent();
 
             pw.println();
@@ -2923,7 +2925,7 @@
                 long duration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp;
                 pw.println("currently holding WakeLock for: " + (duration / 1000) + "s");
             }
-            mWakelockLogs.reverseDump(fd, pw, args);
+            mWakelockLogs.reverseDump(pw);
 
             pw.println();
             pw.println("bandwidth update requests (by uid):");
@@ -2935,7 +2937,12 @@
                 }
             }
             pw.decreaseIndent();
+            pw.decreaseIndent();
 
+            pw.println();
+            pw.println("mOemNetworkPreferencesLogs (most recent first):");
+            pw.increaseIndent();
+            mOemNetworkPreferencesLogs.reverseDump(pw);
             pw.decreaseIndent();
         }
 
@@ -4224,7 +4231,7 @@
             // network, we should respect the user's option and don't need to popup the
             // PARTIAL_CONNECTIVITY notification to user again.
             nai.networkAgentConfig.acceptPartialConnectivity = accept;
-            nai.updateScoreForNetworkAgentConfigUpdate();
+            nai.updateScoreForNetworkAgentUpdate();
             rematchAllNetworksAndRequests();
         }
 
@@ -4292,6 +4299,7 @@
         }
         if (!nai.avoidUnvalidated) {
             nai.avoidUnvalidated = true;
+            nai.updateScoreForNetworkAgentUpdate();
             rematchAllNetworksAndRequests();
         }
     }
@@ -4399,7 +4407,7 @@
 
     private void updateAvoidBadWifi() {
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
-            nai.updateScoreForNetworkAgentConfigUpdate();
+            nai.updateScoreForNetworkAgentUpdate();
         }
         rematchAllNetworksAndRequests();
     }
@@ -6237,6 +6245,12 @@
     @NonNull
     private ProfileNetworkPreferences mProfileNetworkPreferences = new ProfileNetworkPreferences();
 
+    // OemNetworkPreferences activity String log entries.
+    private static final int MAX_OEM_NETWORK_PREFERENCE_LOGS = 20;
+    @NonNull
+    private final LocalLog mOemNetworkPreferencesLogs =
+            new LocalLog(MAX_OEM_NETWORK_PREFERENCE_LOGS);
+
     /**
      * Determine whether a given package has a mapping in the current OemNetworkPreferences.
      * @param packageName the package name to check existence of a mapping for.
@@ -6601,7 +6615,7 @@
             @NonNull final INetworkOfferCallback callback) {
         ensureRunningOnConnectivityServiceThread();
         for (final NetworkOfferInfo noi : mNetworkOffers) {
-            if (noi.offer.callback.equals(callback)) return noi;
+            if (noi.offer.callback.asBinder().equals(callback.asBinder())) return noi;
         }
         return null;
     }
@@ -7159,6 +7173,7 @@
         final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc);
 
         updateUids(nai, prevNc, newNc);
+        nai.updateScoreForNetworkAgentUpdate();
 
         if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) {
             // If the requestable capabilities haven't changed, and the score hasn't changed, then
@@ -7685,7 +7700,7 @@
         }
 
         void addRequestReassignment(@NonNull final RequestReassignment reassignment) {
-            if (Build.IS_DEBUGGABLE) {
+            if (Build.isDebuggable()) {
                 // The code is never supposed to add two reassignments of the same request. Make
                 // sure this stays true, but without imposing this expensive check on all
                 // reassignments on all user devices.
@@ -9739,6 +9754,7 @@
             return;
         }
 
+        mOemNetworkPreferencesLogs.log("UPDATE INITIATED: " + preference);
         final ArraySet<NetworkRequestInfo> nris =
                 new OemNetworkRequestFactory().createNrisFromOemNetworkPreferences(preference);
         replaceDefaultNetworkRequestsForPreference(nris);
diff --git a/services/core/java/com/android/server/connectivity/FullScore.java b/services/core/java/com/android/server/connectivity/FullScore.java
index a8a83fc..52da566 100644
--- a/services/core/java/com/android/server/connectivity/FullScore.java
+++ b/services/core/java/com/android/server/connectivity/FullScore.java
@@ -91,17 +91,26 @@
     /** @hide */
     public static final int POLICY_IS_INVINCIBLE = 58;
 
+    // This network has been validated at least once since it was connected, but not explicitly
+    // avoided in UI.
+    // TODO : remove setAvoidUnvalidated and instead disconnect the network when the user
+    // chooses to move away from this network, and remove this flag.
+    /** @hide */
+    public static final int POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD = 57;
+
     // To help iterate when printing
     @VisibleForTesting
-    static final int MIN_CS_MANAGED_POLICY = POLICY_IS_INVINCIBLE;
+    static final int MIN_CS_MANAGED_POLICY = POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
     @VisibleForTesting
     static final int MAX_CS_MANAGED_POLICY = POLICY_IS_VALIDATED;
 
     // Mask for policies in NetworkScore. This should have all bits managed by NetworkScore set
     // and all bits managed by FullScore unset. As bits are handled from 0 up in NetworkScore and
-    // from 63 down in FullScore, cut at the 32rd bit for simplicity, but change this if some day
+    // from 63 down in FullScore, cut at the 32nd bit for simplicity, but change this if some day
     // there are more than 32 bits handled on either side.
-    private static final int EXTERNAL_POLICIES_MASK = 0x0000FFFF;
+    // YIELD_TO_BAD_WIFI is temporarily handled by ConnectivityService.
+    private static final long EXTERNAL_POLICIES_MASK =
+            0x00000000FFFFFFFFL & ~(1L << POLICY_YIELD_TO_BAD_WIFI);
 
     @VisibleForTesting
     static @NonNull String policyNameOf(final int policy) {
@@ -115,6 +124,7 @@
             case POLICY_TRANSPORT_PRIMARY: return "TRANSPORT_PRIMARY";
             case POLICY_EXITING: return "EXITING";
             case POLICY_IS_INVINCIBLE: return "INVINCIBLE";
+            case POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD: return "EVER_VALIDATED";
         }
         throw new IllegalArgumentException("Unknown policy : " + policy);
     }
@@ -137,6 +147,7 @@
      * @param score the score supplied by the agent
      * @param caps the NetworkCapabilities of the network
      * @param config the NetworkAgentConfig of the network
+     * @param everValidated whether this network has ever validated
      * @param yieldToBadWiFi whether this network yields to a previously validated wifi gone bad
      * @return a FullScore that is appropriate to use for ranking.
      */
@@ -145,12 +156,13 @@
     // connectivity for backward compatibility.
     public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
             @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config,
-            final boolean yieldToBadWiFi) {
+            final boolean everValidated, final boolean yieldToBadWiFi) {
         return withPolicies(score.getLegacyInt(), score.getPolicies(),
                 score.getKeepConnectedReason(),
                 caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
                 caps.hasCapability(NET_CAPABILITY_NOT_METERED),
+                everValidated,
                 config.explicitlySelected,
                 config.acceptUnvalidated,
                 yieldToBadWiFi,
@@ -179,6 +191,8 @@
         // Prospective scores are always unmetered, because unmetered networks are stronger
         // than metered networks, and it's not known in advance whether the network is metered.
         final boolean unmetered = true;
+        // If the offer may validate, then it should be considered to have validated at some point
+        final boolean everValidated = mayValidate;
         // The network hasn't been chosen by the user (yet, at least).
         final boolean everUserSelected = false;
         // Don't assume the user will accept unvalidated connectivity.
@@ -189,8 +203,8 @@
         // score.
         final boolean invincible = score.getLegacyInt() > NetworkRanker.LEGACY_INT_MAX;
         return withPolicies(score.getLegacyInt(), score.getPolicies(), KEEP_CONNECTED_NONE,
-                mayValidate, vpn, unmetered, everUserSelected, acceptUnvalidated, yieldToBadWiFi,
-                invincible);
+                mayValidate, vpn, unmetered, everValidated, everUserSelected, acceptUnvalidated,
+                yieldToBadWiFi, invincible);
     }
 
     /**
@@ -204,11 +218,14 @@
     // telephony factory, so that it depends on the carrier. For now this is handled by
     // connectivity for backward compatibility.
     public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
-            @NonNull final NetworkAgentConfig config, final boolean yieldToBadWifi) {
+            @NonNull final NetworkAgentConfig config,
+            final boolean everValidated,
+            final boolean yieldToBadWifi) {
         return withPolicies(mLegacyInt, mPolicies, mKeepConnectedReason,
                 caps.hasCapability(NET_CAPABILITY_VALIDATED),
                 caps.hasTransport(TRANSPORT_VPN),
                 caps.hasCapability(NET_CAPABILITY_NOT_METERED),
+                everValidated,
                 config.explicitlySelected,
                 config.acceptUnvalidated,
                 yieldToBadWifi,
@@ -224,6 +241,7 @@
             final boolean isValidated,
             final boolean isVpn,
             final boolean isUnmetered,
+            final boolean everValidated,
             final boolean everUserSelected,
             final boolean acceptUnvalidated,
             final boolean yieldToBadWiFi,
@@ -232,6 +250,7 @@
                 | (isValidated       ? 1L << POLICY_IS_VALIDATED : 0)
                 | (isVpn             ? 1L << POLICY_IS_VPN : 0)
                 | (isUnmetered       ? 1L << POLICY_IS_UNMETERED : 0)
+                | (everValidated     ? 1L << POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD : 0)
                 | (everUserSelected  ? 1L << POLICY_EVER_USER_SELECTED : 0)
                 | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0)
                 | (yieldToBadWiFi    ? 1L << POLICY_YIELD_TO_BAD_WIFI : 0)
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 5d793fd..4d310cb 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -707,7 +707,8 @@
             @NonNull final NetworkCapabilities nc) {
         final NetworkCapabilities oldNc = networkCapabilities;
         networkCapabilities = nc;
-        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, yieldToBadWiFi());
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, everValidatedForYield(),
+                yieldToBadWiFi());
         final NetworkMonitorManager nm = mNetworkMonitor;
         if (nm != null) {
             nm.notifyNetworkCapabilitiesChanged(nc);
@@ -893,7 +894,7 @@
     // Caller must not mutate. This method is called frequently and making a defensive copy
     // would be too expensive. This is used by NetworkRanker.Scoreable, so it can be compared
     // against other scoreables.
-    @Override public NetworkCapabilities getCaps() {
+    @Override public NetworkCapabilities getCapsNoCopy() {
         return networkCapabilities;
     }
 
@@ -919,7 +920,7 @@
      */
     public void setScore(final NetworkScore score) {
         mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig,
-                yieldToBadWiFi());
+                everValidatedForYield(), yieldToBadWiFi());
     }
 
     /**
@@ -927,8 +928,13 @@
      *
      * Call this after updating the network agent config.
      */
-    public void updateScoreForNetworkAgentConfigUpdate() {
-        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig, yieldToBadWiFi());
+    public void updateScoreForNetworkAgentUpdate() {
+        mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig,
+                everValidatedForYield(), yieldToBadWiFi());
+    }
+
+    private boolean everValidatedForYield() {
+        return everValidated && !avoidUnvalidated;
     }
 
     /**
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index b57ad5d..3dc79c5 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -280,7 +280,11 @@
                 .setContentTitle(title)
                 .setContentIntent(intent)
                 .setLocalOnly(true)
-                .setOnlyAlertOnce(true);
+                .setOnlyAlertOnce(true)
+                // TODO: consider having action buttons to disconnect on the sign-in notification
+                // especially if it is ongoing
+                .setOngoing(notifyType == NotificationType.SIGN_IN
+                        && r.getBoolean(R.bool.config_ongoingSignInNotification));
 
         if (notifyType == NotificationType.NETWORK_SWITCH) {
             builder.setStyle(new Notification.BigTextStyle().bigText(details));
diff --git a/services/core/java/com/android/server/connectivity/NetworkOffer.java b/services/core/java/com/android/server/connectivity/NetworkOffer.java
index 5336593..8285e7a 100644
--- a/services/core/java/com/android/server/connectivity/NetworkOffer.java
+++ b/services/core/java/com/android/server/connectivity/NetworkOffer.java
@@ -76,7 +76,7 @@
     /**
      * Get the capabilities filter of this offer
      */
-    @Override @NonNull public NetworkCapabilities getCaps() {
+    @Override @NonNull public NetworkCapabilities getCapsNoCopy() {
         return caps;
     }
 
@@ -133,7 +133,7 @@
      * @param previousOffer the previous offer
      */
     public void migrateFrom(@NonNull final NetworkOffer previousOffer) {
-        if (!callback.equals(previousOffer.callback)) {
+        if (!callback.asBinder().equals(previousOffer.callback.asBinder())) {
             throw new IllegalArgumentException("Can only migrate from a previous version of"
                     + " the same offer");
         }
diff --git a/services/core/java/com/android/server/connectivity/NetworkRanker.java b/services/core/java/com/android/server/connectivity/NetworkRanker.java
index 3aaff59..c123ea7 100644
--- a/services/core/java/com/android/server/connectivity/NetworkRanker.java
+++ b/services/core/java/com/android/server/connectivity/NetworkRanker.java
@@ -26,6 +26,7 @@
 
 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
+import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD;
 import static com.android.server.connectivity.FullScore.POLICY_IS_INVINCIBLE;
 import static com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_IS_VPN;
@@ -58,7 +59,7 @@
         /** Get score of this scoreable */
         FullScore getScore();
         /** Get capabilities of this scoreable */
-        NetworkCapabilities getCaps();
+        NetworkCapabilities getCapsNoCopy();
     }
 
     private static final boolean USE_POLICY_RANKING = false;
@@ -158,11 +159,12 @@
         if (accepted.size() == 1) return accepted.get(0);
         if (accepted.size() > 0 && rejected.size() > 0) candidates = new ArrayList<>(accepted);
 
-        // Yield to bad wifi policy : if any wifi has ever been validated, keep only networks
-        // that don't yield to such a wifi network.
+        // Yield to bad wifi policy : if any wifi has ever been validated (even if it's now
+        // unvalidated), and unless it's been explicitly avoided when bad in UI, then keep only
+        // networks that don't yield to such a wifi network.
         final boolean anyWiFiEverValidated = CollectionUtils.any(candidates,
-                nai -> nai.getScore().hasPolicy(POLICY_EVER_USER_SELECTED)
-                        && nai.getCaps().hasTransport(TRANSPORT_WIFI));
+                nai -> nai.getScore().hasPolicy(POLICY_EVER_VALIDATED_NOT_AVOIDED_WHEN_BAD)
+                        && nai.getCapsNoCopy().hasTransport(TRANSPORT_WIFI));
         if (anyWiFiEverValidated) {
             partitionInto(candidates, nai -> !nai.getScore().hasPolicy(POLICY_YIELD_TO_BAD_WIFI),
                     accepted, rejected);
@@ -206,18 +208,18 @@
         for (final Scoreable defaultSubNai : accepted) {
             // Remove all networks without the DEFAULT_SUBSCRIPTION policy and the same transports
             // as a network that has it.
-            final int[] transports = defaultSubNai.getCaps().getTransportTypes();
+            final int[] transports = defaultSubNai.getCapsNoCopy().getTransportTypes();
             candidates.removeIf(nai -> !nai.getScore().hasPolicy(POLICY_TRANSPORT_PRIMARY)
-                    && Arrays.equals(transports, nai.getCaps().getTransportTypes()));
+                    && Arrays.equals(transports, nai.getCapsNoCopy().getTransportTypes()));
         }
         if (1 == candidates.size()) return candidates.get(0);
-        // It's guaranteed candidates.size() > 0 because there is at least one with DEFAULT_SUB
-        // policy and only those without it were removed.
+        // It's guaranteed candidates.size() > 0 because there is at least one with the
+        // TRANSPORT_PRIMARY policy and only those without it were removed.
 
         // If some of the networks have a better transport than others, keep only the ones with
         // the best transports.
         for (final int transport : PREFERRED_TRANSPORTS_ORDER) {
-            partitionInto(candidates, nai -> nai.getCaps().hasTransport(transport),
+            partitionInto(candidates, nai -> nai.getCapsNoCopy().hasTransport(transport),
                     accepted, rejected);
             if (accepted.size() == 1) return accepted.get(0);
             if (accepted.size() > 0 && rejected.size() > 0) {
diff --git a/tests/net/common/java/android/net/NetworkAgentConfigTest.kt b/tests/net/common/java/android/net/NetworkAgentConfigTest.kt
index 1e54093..db939f9 100644
--- a/tests/net/common/java/android/net/NetworkAgentConfigTest.kt
+++ b/tests/net/common/java/android/net/NetworkAgentConfigTest.kt
@@ -63,8 +63,8 @@
             setPartialConnectivityAcceptable(false)
             setUnvalidatedConnectivityAcceptable(true)
             setLegacyTypeName("TEST_NETWORK")
-            disableNat64Detection()
-            disableProvisioningNotification()
+            setNat64DetectionEnabled(false)
+            setProvisioningNotificationEnabled(false)
         }.build()
 
         assertTrue(config.isExplicitlySelected())
diff --git a/tests/net/common/java/android/net/TcpKeepalivePacketDataTest.kt b/tests/net/common/java/android/net/TcpKeepalivePacketDataTest.kt
index 6770066..7a18bb0 100644
--- a/tests/net/common/java/android/net/TcpKeepalivePacketDataTest.kt
+++ b/tests/net/common/java/android/net/TcpKeepalivePacketDataTest.kt
@@ -92,12 +92,12 @@
         assertTrue(str.contains(data.dstAddress.hostAddress))
         assertTrue(str.contains(data.dstPort.toString()))
         // .packet not included in toString()
-        assertTrue(str.contains(data.tcpSeq.toString()))
-        assertTrue(str.contains(data.tcpAck.toString()))
-        assertTrue(str.contains(data.tcpWindow.toString()))
-        assertTrue(str.contains(data.tcpWindowScale.toString()))
-        assertTrue(str.contains(data.ipTos.toString()))
-        assertTrue(str.contains(data.ipTtl.toString()))
+        assertTrue(str.contains(data.getTcpSeq().toString()))
+        assertTrue(str.contains(data.getTcpAck().toString()))
+        assertTrue(str.contains(data.getTcpWindow().toString()))
+        assertTrue(str.contains(data.getTcpWindowScale().toString()))
+        assertTrue(str.contains(data.getIpTos().toString()))
+        assertTrue(str.contains(data.getIpTtl().toString()))
 
         // Update above assertions if field is added
         assertFieldCountEquals(5, KeepalivePacketData::class.java)
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 93a2bb0..3865fdf 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.CHANGE_NETWORK_STATE;
 import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.DUMP;
 import static android.Manifest.permission.NETWORK_FACTORY;
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -356,6 +357,8 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import kotlin.reflect.KClass;
@@ -10049,6 +10052,7 @@
 
     @Test
     public void testDumpDoesNotCrash() {
+        mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
         // Filing a couple requests prior to testing the dump.
         final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
         final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
@@ -11992,6 +11996,33 @@
     }
 
     @Test
+    public void testSetOemNetworkPreferenceLogsRequest() throws Exception {
+        mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
+        @OemNetworkPreferences.OemNetworkPreference final int networkPref =
+                OEM_NETWORK_PREFERENCE_OEM_PAID;
+        final StringWriter stringWriter = new StringWriter();
+        final String logIdentifier = "UPDATE INITIATED: OemNetworkPreferences";
+        final Pattern pattern = Pattern.compile(logIdentifier);
+
+        final int expectedNumLogs = 2;
+        final UidRangeParcel[] uidRanges =
+                toUidRangeStableParcels(uidRangesForUids(TEST_PACKAGE_UID));
+
+        // Call twice to generate two logs.
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+        setupSetOemNetworkPreferenceForPreferenceTest(networkPref, uidRanges, TEST_PACKAGE_NAME);
+        mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+
+        final String dumpOutput = stringWriter.toString();
+        final Matcher matcher = pattern.matcher(dumpOutput);
+        int count = 0;
+        while (matcher.find()) {
+            count++;
+        }
+        assertEquals(expectedNumLogs, count);
+    }
+
+    @Test
     public void testGetAllNetworkStateSnapshot() throws Exception {
         verifyNoNetwork();
 
diff --git a/tests/net/java/com/android/server/connectivity/FullScoreTest.kt b/tests/net/java/com/android/server/connectivity/FullScoreTest.kt
index f0d7d86..45b575a 100644
--- a/tests/net/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/net/java/com/android/server/connectivity/FullScoreTest.kt
@@ -56,7 +56,7 @@
             if (vpn) addTransportType(NetworkCapabilities.TRANSPORT_VPN)
             if (validated) addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
         }.build()
-        return mixInScore(nc, nac, false /* avoidBadWifi */)
+        return mixInScore(nc, nac, validated, false /* yieldToBadWifi */)
     }
 
     @Test
diff --git a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 2f3ee68..c353cea 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -16,8 +16,16 @@
 
 package com.android.server.connectivity;
 
-import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.*;
+import static android.app.Notification.FLAG_ONGOING_EVENT;
 
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.LOST_INTERNET;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NETWORK_SWITCH;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.NO_INTERNET;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PARTIAL_CONNECTIVITY;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.PRIVATE_DNS_BROKEN;
+import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.SIGN_IN;
+
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.clearInvocations;
@@ -230,19 +238,47 @@
         verify(mNotificationManager, never()).notify(any(), anyInt(), any());
     }
 
+    private void assertNotification(NotificationType type, boolean ongoing) {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+        final ArgumentCaptor<Notification> noteCaptor = ArgumentCaptor.forClass(Notification.class);
+        mManager.showNotification(id, type, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1)).notify(eq(tag), eq(type.eventId),
+                noteCaptor.capture());
+
+        assertEquals("Notification ongoing flag should be " + (ongoing ? "set" : "unset"),
+                ongoing, (noteCaptor.getValue().flags & FLAG_ONGOING_EVENT) != 0);
+    }
+
     @Test
     public void testDuplicatedNotificationsNoInternetThenSignIn() {
         final int id = 101;
         final String tag = NetworkNotificationManager.tagFor(id);
 
         // Show first NO_INTERNET
-        mManager.showNotification(id, NO_INTERNET, mWifiNai, mCellNai, null, false);
-        verify(mNotificationManager, times(1)).notify(eq(tag), eq(NO_INTERNET.eventId), any());
+        assertNotification(NO_INTERNET, false /* ongoing */);
 
         // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
-        mManager.showNotification(id, SIGN_IN, mWifiNai, mCellNai, null, false);
+        assertNotification(SIGN_IN, false /* ongoing */);
         verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId));
-        verify(mNotificationManager, times(1)).notify(eq(tag), eq(SIGN_IN.eventId), any());
+
+        // Network disconnects
+        mManager.clearNotification(id);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(SIGN_IN.eventId));
+    }
+
+    @Test
+    public void testOngoingSignInNotification() {
+        doReturn(true).when(mResources).getBoolean(R.bool.config_ongoingSignInNotification);
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        // Show first NO_INTERNET
+        assertNotification(NO_INTERNET, false /* ongoing */);
+
+        // Captive portal detection triggers SIGN_IN a bit later, clearing the previous NO_INTERNET
+        assertNotification(SIGN_IN, true /* ongoing */);
+        verify(mNotificationManager, times(1)).cancel(eq(tag), eq(NO_INTERNET.eventId));
 
         // Network disconnects
         mManager.clearNotification(id);