Merge "Enable R8 shrinking of standalone system server jar service-connectivity"
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 56c21eb..2196bf8 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -586,6 +586,14 @@
         }
 
         private void stop() {
+            // Unregister NetworkAgent before stopping IpClient, so destroyNativeNetwork (which
+            // deletes routes) hopefully happens before stop() finishes execution. Otherwise, it may
+            // delete the new routes when IpClient gets restarted.
+            if (mNetworkAgent != null) {
+                mNetworkAgent.unregister();
+                mNetworkAgent = null;
+            }
+
             // Invalidate all previous start requests
             if (mIpClient != null) {
                 mIpClient.shutdown();
@@ -595,10 +603,6 @@
 
             mIpClientCallback = null;
 
-            if (mNetworkAgent != null) {
-                mNetworkAgent.unregister();
-                mNetworkAgent = null;
-            }
             mLinkProperties.clear();
         }
 
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 3c9886c..1236243 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -205,7 +205,6 @@
 import android.net.resolv.aidl.Nat64PrefixEventParcel;
 import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
 import android.net.shared.PrivateDnsConfig;
-import android.net.util.MultinetworkPolicyTracker;
 import android.net.wifi.WifiInfo;
 import android.os.BatteryStatsManager;
 import android.os.Binder;
@@ -272,6 +271,7 @@
 import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.LingerMonitor;
 import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.MultinetworkPolicyTracker;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkDiagnostics;
 import com.android.server.connectivity.NetworkNotificationManager;
@@ -3748,17 +3748,6 @@
                 case EVENT_PROVISIONING_NOTIFICATION: {
                     final boolean visible = toBool(msg.arg1);
                     // If captive portal status has changed, update capabilities or disconnect.
-                    if (nai != null && (visible != nai.captivePortalDetected())) {
-                        nai.setCaptivePortalDetected(visible);
-                        if (visible && ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
-                                == getCaptivePortalMode()) {
-                            if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
-                            nai.onPreventAutomaticReconnect();
-                            teardownUnneededNetwork(nai);
-                            break;
-                        }
-                        updateCapabilitiesForNetwork(nai);
-                    }
                     if (!visible) {
                         // Only clear SIGN_IN and NETWORK_SWITCH notifications here, or else other
                         // notifications belong to the same network may be cleared unexpectedly.
@@ -3796,12 +3785,12 @@
                 @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
             final boolean valid = (testResult & NETWORK_VALIDATION_RESULT_VALID) != 0;
             final boolean partial = (testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0;
-            final boolean captive = !TextUtils.isEmpty(redirectUrl);
+            final boolean portal = !TextUtils.isEmpty(redirectUrl);
 
             // If there is any kind of working networking, then the NAI has been evaluated
             // once. {@see NetworkAgentInfo#setEvaluated}, which returns whether this is
             // the first time this ever happened.
-            final boolean someConnectivity = (valid || partial || captive);
+            final boolean someConnectivity = (valid || partial || portal);
             final boolean becameEvaluated = someConnectivity && nai.setEvaluated();
             // Because of b/245893397, if the score is updated when updateCapabilities is called,
             // any callback that receives onAvailable for that rematch receives an extra caps
@@ -3820,8 +3809,12 @@
 
             final boolean wasValidated = nai.isValidated();
             final boolean wasPartial = nai.partialConnectivity();
-            nai.setPartialConnectivity((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
-            final boolean partialConnectivityChanged = (wasPartial != nai.partialConnectivity());
+            final boolean wasPortal = nai.captivePortalDetected();
+            nai.setPartialConnectivity(partial);
+            nai.setCaptivePortalDetected(portal);
+            nai.updateScoreForNetworkAgentUpdate();
+            final boolean partialConnectivityChanged = (wasPartial != partial);
+            final boolean portalChanged = (wasPortal != portal);
 
             if (DBG) {
                 final String logMsg = !TextUtils.isEmpty(redirectUrl)
@@ -3829,7 +3822,7 @@
                         : "";
                 log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg);
             }
-            if (valid != nai.isValidated()) {
+            if (valid != wasValidated) {
                 final FullScore oldScore = nai.getScore();
                 nai.setValidated(valid);
                 updateCapabilities(oldScore, nai, nai.networkCapabilities);
@@ -3852,6 +3845,16 @@
                 }
             } else if (partialConnectivityChanged) {
                 updateCapabilitiesForNetwork(nai);
+            } else if (portalChanged) {
+                if (portal && ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
+                        == getCaptivePortalMode()) {
+                    if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
+                    nai.onPreventAutomaticReconnect();
+                    teardownUnneededNetwork(nai);
+                    return;
+                } else {
+                    updateCapabilitiesForNetwork(nai);
+                }
             } else if (becameEvaluated) {
                 // If valid or partial connectivity changed, updateCapabilities* has
                 // done the rematch.
@@ -5080,8 +5083,14 @@
     private void updateAvoidBadWifi() {
         ensureRunningOnConnectivityServiceThread();
         // Agent info scores and offer scores depend on whether cells yields to bad wifi.
+        final boolean avoidBadWifi = avoidBadWifi();
         for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
             nai.updateScoreForNetworkAgentUpdate();
+            if (avoidBadWifi) {
+                // If the device is now avoiding bad wifi, remove notifications that might have
+                // been put up when the device didn't.
+                mNotifier.clearNotification(nai.network.getNetId(), NotificationType.LOST_INTERNET);
+            }
         }
         // UpdateOfferScore will update mNetworkOffers inline, so make a copy first.
         final ArrayList<NetworkOfferInfo> offersToUpdate = new ArrayList<>(mNetworkOffers);
@@ -5229,6 +5238,16 @@
             // network was found not to have Internet access.
             nai.updateScoreForNetworkAgentUpdate();
             rematchAllNetworksAndRequests();
+
+            // Also, if this is WiFi and it should be preferred actively, now is the time to
+            // prompt the user that they walked past and connected to a bad WiFi.
+            if (nai.networkCapabilities.hasTransport(TRANSPORT_WIFI)
+                    && !avoidBadWifi()
+                    && activelyPreferBadWifi()) {
+                // The notification will be removed if the network validates or disconnects.
+                showNetworkNotification(nai, NotificationType.LOST_INTERNET);
+                return;
+            }
         }
 
         if (!shouldPromptUnvalidated(nai)) return;
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index e1c7b64..d7c3287 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -595,13 +595,17 @@
         Log.i(TAG, "untag socket cookie " + cookie);
     }
 
+    private boolean isStarted() {
+        return mClatdTracker != null;
+    }
+
     /**
      * Start clatd for a given interface and NAT64 prefix.
      */
     public String clatStart(final String iface, final int netId,
             @NonNull final IpPrefix nat64Prefix)
             throws IOException {
-        if (mClatdTracker != null) {
+        if (isStarted()) {
             throw new IOException("Clatd is already running on " + mClatdTracker.iface
                     + " (pid " + mClatdTracker.pid + ")");
         }
@@ -833,7 +837,7 @@
      * Stop clatd
      */
     public void clatStop() throws IOException {
-        if (mClatdTracker == null) {
+        if (!isStarted()) {
             throw new IOException("Clatd has not started");
         }
         Log.i(TAG, "Stopping clatd pid=" + mClatdTracker.pid + " on " + mClatdTracker.iface);
@@ -902,12 +906,16 @@
     public void dump(@NonNull IndentingPrintWriter pw) {
         // TODO: move map dump to a global place to avoid duplicate dump while there are two or
         // more IPv6 only networks.
-        pw.println("CLAT tracker: " + mClatdTracker.toString());
-        pw.println("Forwarding rules:");
-        pw.increaseIndent();
-        dumpBpfIngress(pw);
-        dumpBpfEgress(pw);
-        pw.decreaseIndent();
+        if (isStarted()) {
+            pw.println("CLAT tracker: " + mClatdTracker.toString());
+            pw.println("Forwarding rules:");
+            pw.increaseIndent();
+            dumpBpfIngress(pw);
+            dumpBpfEgress(pw);
+            pw.decreaseIndent();
+        } else {
+            pw.println("<not started>");
+        }
         pw.println();
     }
 
diff --git a/framework/src/android/net/util/MultinetworkPolicyTracker.java b/service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java
similarity index 99%
rename from framework/src/android/net/util/MultinetworkPolicyTracker.java
rename to service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java
index 4217921..f1cb9f1 100644
--- a/framework/src/android/net/util/MultinetworkPolicyTracker.java
+++ b/service/src/com/android/server/connectivity/MultinetworkPolicyTracker.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.net.util;
+package com.android.server.connectivity;
 
 import static android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI;
 import static android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE;
@@ -100,7 +100,6 @@
      * doesn't provide internet access, unless there is a captive portal on that wifi.
      * This is the behavior in U and above.
      */
-    // TODO : implement the behavior.
     private boolean mActivelyPreferBadWifi;
 
     // Mainline module can't use internal HandlerExecutor, so add an identical executor here.
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index a57e992..4e19781 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -540,6 +540,9 @@
      */
     public void dump(IndentingPrintWriter pw) {
         if (SdkLevel.isAtLeastT()) {
+            // Dump ClatCoordinator information while clatd has been started but not running. The
+            // reason is that it helps to have more information if clatd is started but the
+            // v4-* interface doesn't bring up. See #isStarted, #isRunning.
             if (isStarted()) {
                 pw.println("ClatCoordinator:");
                 pw.increaseIndent();
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
index d56e171..d94c8dc 100644
--- a/service/src/com/android/server/connectivity/NetworkRanker.java
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -16,6 +16,7 @@
 
 package com.android.server.connectivity;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
@@ -27,6 +28,7 @@
 import static com.android.net.module.util.CollectionUtils.filter;
 import static com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_AVOIDED_WHEN_UNVALIDATED;
+import static com.android.server.connectivity.FullScore.POLICY_EVER_EVALUATED;
 import static com.android.server.connectivity.FullScore.POLICY_EVER_USER_SELECTED;
 import static com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED;
 import static com.android.server.connectivity.FullScore.POLICY_IS_DESTROYED;
@@ -38,7 +40,6 @@
 import android.annotation.Nullable;
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
-import android.net.util.MultinetworkPolicyTracker;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.CollectionUtils;
@@ -67,7 +68,6 @@
         /**
          * @see MultinetworkPolicyTracker#getActivelyPreferBadWifi()
          */
-        // TODO : implement the behavior.
         public boolean activelyPreferBadWifi() {
             return mActivelyPreferBadWifi;
         }
@@ -142,11 +142,42 @@
         }
     }
 
-    private <T extends Scoreable> boolean isBadWiFi(@NonNull final T candidate) {
+    /**
+     * Returns whether the wifi passed as an argument is a preferred network to yielding cell.
+     *
+     * When comparing bad wifi to cell with POLICY_YIELD_TO_BAD_WIFI, it may be necessary to
+     * know if a particular bad wifi is preferred to such a cell network. This method computes
+     * and returns this.
+     *
+     * @param candidate a bad wifi to evaluate
+     * @return whether this candidate is preferred to cell with POLICY_YIELD_TO_BAD_WIFI
+     */
+    private <T extends Scoreable> boolean isPreferredBadWiFi(@NonNull final T candidate) {
         final FullScore score = candidate.getScore();
-        return score.hasPolicy(POLICY_EVER_VALIDATED)
-                && !score.hasPolicy(POLICY_AVOIDED_WHEN_UNVALIDATED)
-                && candidate.getCapsNoCopy().hasTransport(TRANSPORT_WIFI);
+        final NetworkCapabilities caps = candidate.getCapsNoCopy();
+
+        // Whatever the policy, only WiFis can be preferred bad WiFis.
+        if (!caps.hasTransport(TRANSPORT_WIFI)) return false;
+        // Validated networks aren't bad networks, so a fortiori can't be preferred bad WiFis.
+        if (score.hasPolicy(POLICY_IS_VALIDATED)) return false;
+        // A WiFi that the user explicitly wanted to avoid in UI is never a preferred bad WiFi.
+        if (score.hasPolicy(POLICY_AVOIDED_WHEN_UNVALIDATED)) return false;
+
+        if (mConf.activelyPreferBadWifi()) {
+            // If a network is still evaluating, don't prefer it.
+            if (!score.hasPolicy(POLICY_EVER_EVALUATED)) return false;
+
+            // If a network is not a captive portal, then prefer it.
+            if (!caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return true;
+
+            // If it's a captive portal, prefer it if it previously validated but is no longer
+            // validated (i.e., the user logged in in the past, but later the portal closed).
+            return score.hasPolicy(POLICY_EVER_VALIDATED);
+        } else {
+            // Under the original "prefer bad WiFi" policy, only networks that have ever validated
+            // are preferred.
+            return score.hasPolicy(POLICY_EVER_VALIDATED);
+        }
     }
 
     /**
@@ -169,7 +200,7 @@
             // No network with the policy : do nothing.
             return;
         }
-        if (!CollectionUtils.any(rejected, n -> isBadWiFi(n))) {
+        if (!CollectionUtils.any(rejected, n -> isPreferredBadWiFi(n))) {
             // No bad WiFi : do nothing.
             return;
         }
@@ -179,7 +210,7 @@
             // wifis by the following policies (e.g. exiting).
             final ArrayList<T> acceptedYielders = new ArrayList<>(accepted);
             final ArrayList<T> rejectedWithBadWiFis = new ArrayList<>(rejected);
-            partitionInto(rejectedWithBadWiFis, n -> isBadWiFi(n), accepted, rejected);
+            partitionInto(rejectedWithBadWiFis, n -> isPreferredBadWiFi(n), accepted, rejected);
             accepted.addAll(acceptedYielders);
             return;
         }
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 73e4c0e..5b3f37d 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -37,7 +37,7 @@
 import android.net.TestNetworkStackClient
 import android.net.Uri
 import android.net.metrics.IpConnectivityLog
-import android.net.util.MultinetworkPolicyTracker
+import com.android.server.connectivity.MultinetworkPolicyTracker
 import android.os.ConditionVariable
 import android.os.IBinder
 import android.os.SystemConfigManager
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 5e681b6..31b0908 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -29,6 +29,7 @@
 import static android.Manifest.permission.NETWORK_SETTINGS;
 import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.content.Intent.ACTION_PACKAGE_ADDED;
 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
@@ -302,7 +303,6 @@
 import android.net.resolv.aidl.Nat64PrefixEventParcel;
 import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
 import android.net.shared.PrivateDnsConfig;
-import android.net.util.MultinetworkPolicyTracker;
 import android.net.wifi.WifiInfo;
 import android.os.BadParcelableException;
 import android.os.BatteryStatsManager;
@@ -357,14 +357,15 @@
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.LocationPermissionChecker;
 import com.android.net.module.util.NetworkMonitorUtils;
+import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.NetworkAgentConfigShimImpl;
-import com.android.networkstack.apishim.api29.ConstantsShim;
 import com.android.server.ConnectivityService.ConnectivityDiagnosticsCallbackInfo;
 import com.android.server.ConnectivityService.NetworkRequestInfo;
 import com.android.server.ConnectivityServiceTest.ConnectivityServiceDependencies.ReportedInterfaces;
 import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
 import com.android.server.connectivity.ClatCoordinator;
 import com.android.server.connectivity.ConnectivityFlags;
+import com.android.server.connectivity.MultinetworkPolicyTracker;
 import com.android.server.connectivity.Nat464Xlat;
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkNotificationManager;
@@ -1169,10 +1170,11 @@
         void setNetworkPartialValid(boolean isStrictMode) {
             setNetworkPartial();
             mNmValidationResult |= NETWORK_VALIDATION_RESULT_VALID;
+            mNmValidationRedirectUrl = null;
             int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
                     | NETWORK_VALIDATION_PROBE_HTTP;
             int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP;
-            // Suppose the partial network cannot pass the private DNS validation as well, so only
+            // Assume the partial network cannot pass the private DNS validation as well, so only
             // add NETWORK_VALIDATION_PROBE_DNS in probesCompleted but not probesSucceeded.
             if (isStrictMode) {
                 probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS;
@@ -3529,12 +3531,16 @@
 
     /** Expects the specified notification and returns the notification ID. */
     private int expectNotification(TestNetworkAgentWrapper agent, NotificationType type) {
-        verify(mNotificationManager).notify(
+        verify(mNotificationManager, timeout(TIMEOUT_MS)).notify(
                 eq(NetworkNotificationManager.tagFor(agent.getNetwork().netId)),
                 eq(type.eventId), any());
         return type.eventId;
     }
 
+    private void expectNoNotification(@NonNull final TestNetworkAgentWrapper agent) {
+        verify(mNotificationManager, never()).notifyAsUser(anyString(), anyInt(), any(), any());
+    }
+
     /**
      * Expects the specified notification happens when the unvalidated prompt message arrives
      *
@@ -3553,9 +3559,9 @@
      * This generally happens when the network disconnects or when the newtwork validates. During
      * normal usage the notification is also cleared by the system when the notification is tapped.
      */
-    private void expectClearNotification(TestNetworkAgentWrapper agent, int expectedId) {
-        verify(mNotificationManager).cancel(
-                eq(NetworkNotificationManager.tagFor(agent.getNetwork().netId)), eq(expectedId));
+    private void expectClearNotification(TestNetworkAgentWrapper agent, NotificationType type) {
+        verify(mNotificationManager, timeout(TIMEOUT_MS)).cancel(
+                eq(NetworkNotificationManager.tagFor(agent.getNetwork().netId)), eq(type.eventId));
     }
 
     /**
@@ -3566,13 +3572,13 @@
     private void expectUnvalidationCheckWillNotNotify(TestNetworkAgentWrapper agent) {
         mService.scheduleEvaluationTimeout(agent.getNetwork(), 0 /*delayMs */);
         waitForIdle();
-        verify(mNotificationManager, never()).notifyAsUser(anyString(), anyInt(), any(), any());
+        expectNoNotification(agent);
     }
 
     private void expectDisconnectAndClearNotifications(TestNetworkCallback callback,
-            TestNetworkAgentWrapper agent, int id) {
+            TestNetworkAgentWrapper agent, NotificationType type) {
         callback.expectCallback(CallbackEntry.LOST, agent);
-        expectClearNotification(agent, id);
+        expectClearNotification(agent, type);
     }
 
     private NativeNetworkConfig nativeNetworkConfigPhysical(int netId, int permission) {
@@ -3716,7 +3722,7 @@
         // Disconnect wifi, and then reconnect, again with explicitlySelected=true.
         mWiFiNetworkAgent.disconnect();
         expectDisconnectAndClearNotifications(callback, mWiFiNetworkAgent,
-                NotificationType.NO_INTERNET.eventId);
+                NotificationType.NO_INTERNET);
 
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
         mWiFiNetworkAgent.explicitlySelected(true, false);
@@ -3730,7 +3736,7 @@
         // network to disconnect.
         mCm.setAcceptUnvalidated(mWiFiNetworkAgent.getNetwork(), false, false);
         expectDisconnectAndClearNotifications(callback, mWiFiNetworkAgent,
-                NotificationType.NO_INTERNET.eventId);
+                NotificationType.NO_INTERNET);
         reset(mNotificationManager);
 
         // Reconnect, again with explicitlySelected=true, but this time validate.
@@ -4230,7 +4236,7 @@
         assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
 
         // Once the network validates, the notification disappears.
-        expectClearNotification(mWiFiNetworkAgent, NotificationType.PARTIAL_CONNECTIVITY.eventId);
+        expectClearNotification(mWiFiNetworkAgent, NotificationType.PARTIAL_CONNECTIVITY);
 
         // Disconnect and reconnect wifi with partial connectivity again.
         mWiFiNetworkAgent.disconnect();
@@ -4253,7 +4259,7 @@
         mCm.setAcceptPartialConnectivity(mWiFiNetworkAgent.getNetwork(), false /* accept */,
                 false /* always */);
         callback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
-        expectClearNotification(mWiFiNetworkAgent, NotificationType.PARTIAL_CONNECTIVITY.eventId);
+        expectClearNotification(mWiFiNetworkAgent, NotificationType.PARTIAL_CONNECTIVITY);
         reset(mNotificationManager);
 
         // If the user accepted partial connectivity before, and the device connects to that network
@@ -4329,10 +4335,11 @@
 
     @Test
     public void testCaptivePortalOnPartialConnectivity() throws Exception {
-        final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
-        final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
-                .addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
-        mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
+        final TestNetworkCallback wifiCallback = new TestNetworkCallback();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build();
+        mCm.registerNetworkCallback(wifiRequest, wifiCallback);
 
         final TestNetworkCallback validatedCallback = new TestNetworkCallback();
         final NetworkRequest validatedRequest = new NetworkRequest.Builder()
@@ -4344,21 +4351,28 @@
         mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
         String redirectUrl = "http://android.com/path";
         mWiFiNetworkAgent.connectWithCaptivePortal(redirectUrl, false /* isStrictMode */);
-        captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
         assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), redirectUrl);
 
+        // This is necessary because of b/245893397, the same bug that happens where we use
+        // expectAvailableDoubleValidatedCallbacks.
+        // TODO : fix b/245893397 and remove this.
+        wifiCallback.expectCapabilitiesWith(NET_CAPABILITY_CAPTIVE_PORTAL, mWiFiNetworkAgent);
+
         // Check that startCaptivePortalApp sends the expected command to NetworkMonitor.
         mCm.startCaptivePortalApp(mWiFiNetworkAgent.getNetwork());
         verify(mWiFiNetworkAgent.mNetworkMonitor, timeout(TIMEOUT_MS).times(1))
                 .launchCaptivePortalApp();
 
         // Report that the captive portal is dismissed with partial connectivity, and check that
-        // callbacks are fired.
+        // callbacks are fired with PARTIAL and without CAPTIVE_PORTAL.
         mWiFiNetworkAgent.setNetworkPartial();
         mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
         waitForIdle();
-        captivePortalCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY,
-                mWiFiNetworkAgent);
+        wifiCallback.expectCapabilitiesThat(
+                mWiFiNetworkAgent, nc ->
+                nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)
+                && !nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL));
 
         // Report partial connectivity is accepted.
         mWiFiNetworkAgent.setNetworkPartialValid(false /* isStrictMode */);
@@ -4366,13 +4380,12 @@
                 false /* always */);
         waitForIdle();
         mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
-        captivePortalCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        wifiCallback.expectCapabilitiesWith(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
         validatedCallback.expectAvailableCallbacksValidated(mWiFiNetworkAgent);
-        NetworkCapabilities nc =
-                validatedCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+        validatedCallback.expectCapabilitiesWith(NET_CAPABILITY_PARTIAL_CONNECTIVITY,
                 mWiFiNetworkAgent);
 
-        mCm.unregisterNetworkCallback(captivePortalCallback);
+        mCm.unregisterNetworkCallback(wifiCallback);
         mCm.unregisterNetworkCallback(validatedCallback);
     }
 
@@ -4455,6 +4468,11 @@
         mCm.reportNetworkConnectivity(wifiNetwork, false);
         captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
         validatedCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        // This is necessary because of b/245893397, the same bug that happens where we use
+        // expectAvailableDoubleValidatedCallbacks.
+        // TODO : fix b/245893397 and remove this.
+        captivePortalCallback.expectCallback(CallbackEntry.NETWORK_CAPS_UPDATED,
+                mWiFiNetworkAgent);
 
         // Check that startCaptivePortalApp sends the expected command to NetworkMonitor.
         mCm.startCaptivePortalApp(wifiNetwork);
@@ -5758,6 +5776,56 @@
         wifiCallback.assertNoCallback();
     }
 
+    public void doTestPreferBadWifi(final boolean preferBadWifi) throws Exception {
+        // Pretend we're on a carrier that restricts switching away from bad wifi, and
+        // depending on the parameter one that may indeed prefer bad wifi.
+        doReturn(0).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
+        doReturn(preferBadWifi ? 1 : 0).when(mResources)
+                .getInteger(R.integer.config_activelyPreferBadWifi);
+        mPolicyTracker.reevaluate();
+
+        registerDefaultNetworkCallbacks();
+        final NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .build();
+        final TestableNetworkCallback wifiCallback = new TestableNetworkCallback();
+        mCm.registerNetworkCallback(wifiRequest, wifiCallback);
+
+        // Bring up validated cell and unvalidated wifi.
+        mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mDefaultNetworkCallback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
+
+        mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        wifiCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+
+        if (preferBadWifi) {
+            expectUnvalidationCheckWillNotify(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
+            mDefaultNetworkCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
+        } else {
+            expectUnvalidationCheckWillNotNotify(mWiFiNetworkAgent);
+            mDefaultNetworkCallback.assertNoCallback();
+        }
+    }
+
+    @Test
+    public void testPreferBadWifi_doNotPrefer() throws Exception {
+        // Starting with U this mode is no longer supported and can't actually be tested
+        assumeFalse(SdkLevel.isAtLeastU());
+        runAsShell(READ_DEVICE_CONFIG, () -> {
+            doTestPreferBadWifi(false /* preferBadWifi */);
+        });
+    }
+
+    @Test
+    public void testPreferBadWifi_doPrefer() throws Exception {
+        runAsShell(READ_DEVICE_CONFIG, () -> {
+            doTestPreferBadWifi(true /* preferBadWifi */);
+        });
+    }
+
     @Test
     public void testAvoidBadWifi() throws Exception {
         final ContentResolver cr = mServiceContext.getContentResolver();
@@ -5781,7 +5849,8 @@
         TestNetworkCallback validatedWifiCallback = new TestNetworkCallback();
         mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback);
 
-        Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 0);
+        // Prompt mode, so notifications can be tested
+        Settings.Global.putString(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null);
         mPolicyTracker.reevaluate();
 
         // Bring up validated cell.
@@ -5803,6 +5872,7 @@
         mCm.reportNetworkConnectivity(wifiNetwork, false);
         defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
         validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        expectNotification(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
 
         // Because avoid bad wifi is off, we don't switch to cellular.
         defaultCallback.assertNoCallback();
@@ -5818,14 +5888,20 @@
         mPolicyTracker.reevaluate();
         defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
         assertEquals(mCm.getActiveNetwork(), cellNetwork);
+        expectClearNotification(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
 
         // Switch back to a restrictive carrier.
         doReturn(0).when(mResources).getInteger(R.integer.config_networkAvoidBadWifi);
         mPolicyTracker.reevaluate();
         defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
         assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+        // A notification was already shown for this very network.
+        expectNoNotification(mWiFiNetworkAgent);
 
         // Simulate the user selecting "switch" on the dialog, and check that we switch to cell.
+        // In principle this is a little bit unrealistic because the switch to a less restrictive
+        // carrier above should have remove the notification but this doesn't matter for the
+        // purposes of this test.
         mCm.setAvoidUnvalidated(wifiNetwork);
         defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
         assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
@@ -5847,6 +5923,7 @@
         mCm.reportNetworkConnectivity(wifiNetwork, false);
         defaultCallback.expectCapabilitiesWithout(NET_CAPABILITY_VALIDATED, mWiFiNetworkAgent);
         validatedWifiCallback.expectCallback(CallbackEntry.LOST, mWiFiNetworkAgent);
+        expectNotification(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
 
         // Simulate the user selecting "switch" and checking the don't ask again checkbox.
         Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1);
@@ -5859,6 +5936,7 @@
         assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
                 NET_CAPABILITY_VALIDATED));
         assertEquals(mCm.getActiveNetwork(), cellNetwork);
+        expectClearNotification(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
 
         // Simulate the user turning the cellular fallback setting off and then on.
         // We switch to wifi and then to cell.
@@ -5866,6 +5944,9 @@
         mPolicyTracker.reevaluate();
         defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
         assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+        // Notification is cleared again because CS doesn't particularly remember that it has
+        // cleared it before, and if it hasn't cleared it before then it should do so now.
+        expectClearNotification(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
         Settings.Global.putInt(cr, ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, 1);
         mPolicyTracker.reevaluate();
         defaultCallback.expectAvailableCallbacksValidated(mCellNetworkAgent);
@@ -5876,6 +5957,8 @@
         defaultCallback.expectCallback(CallbackEntry.LOST, mCellNetworkAgent);
         defaultCallback.expectAvailableCallbacksUnvalidated(mWiFiNetworkAgent);
         validatedWifiCallback.assertNoCallback();
+        // Notification is cleared yet again because the device switched to wifi.
+        expectClearNotification(mWiFiNetworkAgent, NotificationType.LOST_INTERNET);
 
         mCm.unregisterNetworkCallback(cellNetworkCallback);
         mCm.unregisterNetworkCallback(validatedWifiCallback);
@@ -12979,6 +13062,12 @@
         if (null != mTestPackageDefaultNetworkCallback2) {
             mCm.unregisterNetworkCallback(mTestPackageDefaultNetworkCallback2);
         }
+        mSystemDefaultNetworkCallback = null;
+        mDefaultNetworkCallback = null;
+        mProfileDefaultNetworkCallback = null;
+        mTestPackageDefaultNetworkCallback = null;
+        mProfileDefaultNetworkCallbackAsAppUid2 = null;
+        mTestPackageDefaultNetworkCallback2 = null;
     }
 
     private void setupMultipleDefaultNetworksForOemNetworkPreferenceNotCurrentUidTest(
diff --git a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
index 7646c19..85bc4a9 100644
--- a/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/ClatCoordinatorTest.java
@@ -516,28 +516,38 @@
         assertEquals(65508, ClatCoordinator.adjustMtu(CLAT_MAX_MTU + 1 /* over maximum mtu */));
     }
 
-    @Test
-    public void testDump() throws Exception {
-        final ClatCoordinator coordinator = makeClatCoordinator();
+    private void verifyDump(final ClatCoordinator coordinator, boolean clatStarted) {
         final StringWriter stringWriter = new StringWriter();
         final IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ");
-        coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
         coordinator.dump(ipw);
 
         final String[] dumpStrings = stringWriter.toString().split("\n");
-        assertEquals(6, dumpStrings.length);
-        assertEquals("CLAT tracker: iface: test0 (1000), v4iface: v4-test0 (1001), "
-                + "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
-                + "pid: 10483, cookie: 27149", dumpStrings[0].trim());
-        assertEquals("Forwarding rules:", dumpStrings[1].trim());
-        assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
-                dumpStrings[2].trim());
-        assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
-                dumpStrings[3].trim());
-        assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
-                dumpStrings[4].trim());
-        assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
-                dumpStrings[5].trim());
+        if (clatStarted) {
+            assertEquals(6, dumpStrings.length);
+            assertEquals("CLAT tracker: iface: test0 (1000), v4iface: v4-test0 (1001), "
+                    + "v4: /192.0.0.46, v6: /2001:db8:0:b11::464, pfx96: /64:ff9b::, "
+                    + "pid: 10483, cookie: 27149", dumpStrings[0].trim());
+            assertEquals("Forwarding rules:", dumpStrings[1].trim());
+            assertEquals("BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif",
+                    dumpStrings[2].trim());
+            assertEquals("1000 /64:ff9b::/96 /2001:db8:0:b11::464 -> /192.0.0.46 1001",
+                    dumpStrings[3].trim());
+            assertEquals("BPF egress map: iif v4Addr -> v6Addr nat64Prefix oif",
+                    dumpStrings[4].trim());
+            assertEquals("1001 /192.0.0.46 -> /2001:db8:0:b11::464 /64:ff9b::/96 1000 ether",
+                    dumpStrings[5].trim());
+        } else {
+            assertEquals(1, dumpStrings.length);
+            assertEquals("<not started>", dumpStrings[0].trim());
+        }
+    }
+
+    @Test
+    public void testDump() throws Exception {
+        final ClatCoordinator coordinator = makeClatCoordinator();
+        verifyDump(coordinator, false /* clatStarted */);
+        coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX);
+        verifyDump(coordinator, true /* clatStarted */);
     }
 
     @Test
@@ -548,25 +558,18 @@
                 () -> coordinator.clatStart(BASE_IFACE, NETID, invalidPrefix));
     }
 
-    private void assertStartClat(final TestDependencies deps) throws Exception {
-        final ClatCoordinator coordinator = new ClatCoordinator(deps);
-        assertNotNull(coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
-    }
-
-    private void assertNotStartClat(final TestDependencies deps) {
-        // Expect that the injection function of TestDependencies causes clatStart() failed.
-        final ClatCoordinator coordinator = new ClatCoordinator(deps);
-        assertThrows(IOException.class,
-                () -> coordinator.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
-    }
-
     private void checkNotStartClat(final TestDependencies deps, final boolean needToCloseTunFd,
             final boolean needToClosePacketSockFd, final boolean needToCloseRawSockFd)
             throws Exception {
-        // [1] Expect that modified TestDependencies can't start clatd.
-        // Use precise check to make sure that there is no unexpected file descriptor closing.
         clearInvocations(TUN_PFD, RAW_SOCK_PFD, PACKET_SOCK_PFD);
-        assertNotStartClat(deps);
+
+        // [1] Expect that modified TestDependencies can't start clatd.
+        // Expect that the injection function of TestDependencies causes clatStart() failed.
+        final ClatCoordinator coordinatorWithBrokenDeps = new ClatCoordinator(deps);
+        assertThrows(IOException.class,
+                () -> coordinatorWithBrokenDeps.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
+
+        // Use precise check to make sure that there is no unexpected file descriptor closing.
         if (needToCloseTunFd) {
             verify(TUN_PFD).close();
         } else {
@@ -583,10 +586,15 @@
             verify(RAW_SOCK_PFD, never()).close();
         }
 
+        // Check that dump doesn't crash after any clat starting failure.
+        verifyDump(coordinatorWithBrokenDeps, false /* clatStarted */);
+
         // [2] Expect that unmodified TestDependencies can start clatd.
         // Used to make sure that the above modified TestDependencies has really broken the
         // clatd starting.
-        assertStartClat(new TestDependencies());
+        final ClatCoordinator coordinatorWithDefaultDeps = new ClatCoordinator(
+                new TestDependencies());
+        assertNotNull(coordinatorWithDefaultDeps.clatStart(BASE_IFACE, NETID, NAT64_IP_PREFIX));
     }
 
     // The following testNotStartClat* tests verifies bunches of code for unwinding the
diff --git a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt b/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt
similarity index 97%
rename from tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
rename to tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt
index f317863..c4ce041 100644
--- a/tests/unit/java/android/net/util/MultinetworkPolicyTrackerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/MultinetworkPolicyTrackerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.net.util
+package com.android.server.connectivity
 
 import android.content.Context
 import android.content.res.Resources
@@ -24,7 +24,7 @@
 import android.net.ConnectivityResources
 import android.net.ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI
 import android.net.ConnectivitySettingsManager.NETWORK_METERED_MULTIPATH_PREFERENCE
-import android.net.util.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener
+import com.android.server.connectivity.MultinetworkPolicyTracker.ActiveDataSubscriptionIdListener
 import android.os.Build
 import android.os.Handler
 import android.provider.Settings
@@ -60,7 +60,7 @@
  * Tests for [MultinetworkPolicyTracker].
  *
  * Build, install and run with:
- * atest android.net.util.MultinetworkPolicyTrackerTest
+ * atest FrameworksNetTest:MultinetworkPolicyTrackerTest
  */
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
diff --git a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
index 05c648d..ae8b438 100644
--- a/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/NetworkRankerTest.kt
@@ -17,6 +17,7 @@
 package com.android.server.connectivity
 
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkScore.KEEP_CONNECTED_NONE
@@ -25,25 +26,29 @@
 import android.net.NetworkScore.POLICY_YIELD_TO_BAD_WIFI
 import android.os.Build
 import androidx.test.filters.SmallTest
+import com.android.connectivity.resources.R
 import com.android.server.connectivity.FullScore.POLICY_AVOIDED_WHEN_UNVALIDATED
+import com.android.server.connectivity.FullScore.POLICY_EVER_EVALUATED
 import com.android.server.connectivity.FullScore.POLICY_EVER_VALIDATED
 import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED
 import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 import kotlin.test.assertEquals
 
 private fun score(vararg policies: Int) = FullScore(
         policies.fold(0L) { acc, e -> acc or (1L shl e) }, KEEP_CONNECTED_NONE)
-private fun caps(transport: Int) = NetworkCapabilities.Builder().addTransportType(transport).build()
+private fun caps(transport: Int, vararg capabilities: Int) =
+        NetworkCapabilities.Builder().addTransportType(transport).apply {
+            capabilities.forEach { addCapability(it) }
+        }.build()
 
 @SmallTest
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class NetworkRankerTest {
-    private val mRanker = NetworkRanker(NetworkRanker.Configuration(
-            false /* activelyPreferBadWifi */))
+@RunWith(Parameterized::class)
+class NetworkRankerTest(private val activelyPreferBadWifi: Boolean) {
+    private val mRanker = NetworkRanker(NetworkRanker.Configuration(activelyPreferBadWifi))
 
     private class TestScore(private val sc: FullScore, private val nc: NetworkCapabilities)
             : NetworkRanker.Scoreable {
@@ -51,35 +56,91 @@
         override fun getCapsNoCopy(): NetworkCapabilities = nc
     }
 
+    @get:Rule
+    val mIgnoreRule: DevSdkIgnoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.R)
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters
+        fun ranker() = listOf(true, false)
+    }
+
     @Test
     fun testYieldToBadWiFiOneCell() {
         // Only cell, it wins
-        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+        val winner = TestScore(
+                score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED, POLICY_EVER_EVALUATED),
                 caps(TRANSPORT_CELLULAR))
         val scores = listOf(winner)
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
     }
 
     @Test
+    fun testPreferBadWifiOneCellOneEvaluatingWifi() {
+        // TODO : refactor the tests to name each network like this test
+        val wifi = TestScore(score(), caps(TRANSPORT_WIFI))
+        val cell = TestScore(
+                score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED, POLICY_EVER_EVALUATED),
+                caps(TRANSPORT_CELLULAR))
+        assertEquals(cell, mRanker.getBestNetworkByPolicy(listOf(wifi, cell), null))
+    }
+
+    @Test
     fun testYieldToBadWiFiOneCellOneBadWiFi() {
         // Bad wifi wins against yielding validated cell
-        val winner = TestScore(score(POLICY_EVER_VALIDATED), caps(TRANSPORT_WIFI))
+        val winner = TestScore(score(POLICY_EVER_VALIDATED, POLICY_EVER_EVALUATED),
+                caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                TestScore(
+                        score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED, POLICY_EVER_EVALUATED),
                         caps(TRANSPORT_CELLULAR))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
     }
 
     @Test
+    fun testPreferBadWifiOneCellOneBadWifi() {
+        val wifi = TestScore(score(POLICY_EVER_EVALUATED), caps(TRANSPORT_WIFI))
+        val cell = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR))
+        val scores = listOf(wifi, cell)
+        val winner = if (activelyPreferBadWifi) wifi else cell
+        assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
+    }
+
+    @Test
+    fun testPreferBadWifiOneCellOneCaptivePortalWifi() {
+        val wifi = TestScore(score(POLICY_EVER_EVALUATED),
+                caps(TRANSPORT_WIFI, NET_CAPABILITY_CAPTIVE_PORTAL))
+        val cell = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR))
+        assertEquals(cell, mRanker.getBestNetworkByPolicy(listOf(wifi, cell), null))
+    }
+
+    @Test
+    fun testYieldToBadWifiOneCellOneCaptivePortalWifiThatClosed() {
+        val wifi = TestScore(score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED),
+                caps(TRANSPORT_WIFI, NET_CAPABILITY_CAPTIVE_PORTAL))
+        val cell = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR))
+        assertEquals(wifi, mRanker.getBestNetworkByPolicy(listOf(wifi, cell), null))
+    }
+
+    @Test
     fun testYieldToBadWifiAvoidUnvalidated() {
         // Bad wifi avoided when unvalidated loses against yielding validated cell
-        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                 caps(TRANSPORT_CELLULAR))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED, POLICY_AVOIDED_WHEN_UNVALIDATED),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED,
+                                POLICY_AVOIDED_WHEN_UNVALIDATED),
                         caps(TRANSPORT_WIFI))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
@@ -88,12 +149,16 @@
     @Test
     fun testYieldToBadWiFiOneCellTwoBadWiFi() {
         // Bad wifi wins against yielding validated cell. Prefer the one that's primary.
-        val winner = TestScore(score(POLICY_EVER_VALIDATED,
-                POLICY_TRANSPORT_PRIMARY), caps(TRANSPORT_WIFI))
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED), caps(TRANSPORT_WIFI)),
-                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED),
+                        caps(TRANSPORT_WIFI)),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
@@ -103,11 +168,13 @@
     fun testYieldToBadWiFiOneCellTwoBadWiFiOneNotAvoided() {
         // Bad wifi ever validated wins against bad wifi that never was validated (or was
         // avoided when bad).
-        val winner = TestScore(score(POLICY_EVER_VALIDATED), caps(TRANSPORT_WIFI))
+        val winner = TestScore(score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED),
+                caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(), caps(TRANSPORT_WIFI)),
-                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                TestScore(score(POLICY_EVER_EVALUATED), caps(TRANSPORT_WIFI)),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
@@ -116,27 +183,49 @@
     @Test
     fun testYieldToBadWiFiOneCellOneBadWiFiOneGoodWiFi() {
         // Good wifi wins
-        val winner = TestScore(score(POLICY_EVER_VALIDATED, POLICY_IS_VALIDATED),
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED, POLICY_IS_VALIDATED),
                 caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED,
+                                POLICY_TRANSPORT_PRIMARY),
                         caps(TRANSPORT_WIFI)),
-                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
     }
 
     @Test
+    fun testPreferBadWifiOneCellOneBadWifiOneEvaluatingWifi() {
+        val cell = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR))
+        val badWifi = TestScore(score(POLICY_EVER_EVALUATED), caps(TRANSPORT_WIFI))
+        val evaluatingWifi = TestScore(score(), caps(TRANSPORT_WIFI))
+        val winner = if (activelyPreferBadWifi) badWifi else cell
+        assertEquals(winner,
+                mRanker.getBestNetworkByPolicy(listOf(cell, badWifi, evaluatingWifi), null))
+    }
+
+    @Test
     fun testYieldToBadWiFiTwoCellsOneBadWiFi() {
         // Cell that doesn't yield wins over cell that yields and bad wifi
-        val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR))
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_CELLULAR))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED,
+                                POLICY_TRANSPORT_PRIMARY),
                         caps(TRANSPORT_WIFI)),
-                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI,
+                                POLICY_EVER_VALIDATED, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
@@ -145,13 +234,20 @@
     @Test
     fun testYieldToBadWiFiTwoCellsOneBadWiFiOneGoodWiFi() {
         // Good wifi wins over cell that doesn't yield and cell that yields
-        val winner = TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_WIFI))
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_IS_VALIDATED),
+                caps(TRANSPORT_WIFI))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED, POLICY_TRANSPORT_PRIMARY),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED,
+                                POLICY_TRANSPORT_PRIMARY),
                         caps(TRANSPORT_WIFI)),
-                TestScore(score(POLICY_IS_VALIDATED), caps(TRANSPORT_CELLULAR)),
-                TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_IS_VALIDATED),
+                        caps(TRANSPORT_CELLULAR)),
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                         caps(TRANSPORT_CELLULAR))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
@@ -160,11 +256,14 @@
     @Test
     fun testYieldToBadWiFiOneExitingGoodWiFi() {
         // Yielding cell wins over good exiting wifi
-        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                 caps(TRANSPORT_CELLULAR))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_IS_VALIDATED, POLICY_EXITING), caps(TRANSPORT_WIFI))
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_IS_VALIDATED, POLICY_EXITING),
+                        caps(TRANSPORT_WIFI))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
     }
@@ -172,11 +271,14 @@
     @Test
     fun testYieldToBadWiFiOneExitingBadWiFi() {
         // Yielding cell wins over bad exiting wifi
-        val winner = TestScore(score(POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
+        val winner = TestScore(
+                score(POLICY_EVER_EVALUATED, POLICY_YIELD_TO_BAD_WIFI, POLICY_IS_VALIDATED),
                 caps(TRANSPORT_CELLULAR))
         val scores = listOf(
                 winner,
-                TestScore(score(POLICY_EVER_VALIDATED, POLICY_EXITING), caps(TRANSPORT_WIFI))
+                TestScore(
+                        score(POLICY_EVER_EVALUATED, POLICY_EVER_VALIDATED, POLICY_EXITING),
+                        caps(TRANSPORT_WIFI))
         )
         assertEquals(winner, mRanker.getBestNetworkByPolicy(scores, null))
     }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 06dabb0..1c54651 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -29,7 +29,6 @@
 import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.UserHandle.PER_USER_RANGE;
 
-import static com.android.modules.utils.build.SdkLevel.isAtLeastT;
 import static com.android.testutils.Cleanup.testAndCleanup;
 import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 import static com.android.testutils.MiscAsserts.assertThrows;
@@ -41,7 +40,6 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -136,7 +134,6 @@
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
 import com.android.internal.util.HexDump;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.server.DeviceIdleInternal;
 import com.android.server.IpSecService;
 import com.android.server.VpnTestBase;
@@ -341,7 +338,7 @@
 
     @After
     public void tearDown() throws Exception {
-        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
+        doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
     }
 
     private <T> void mockService(Class<T> clazz, String name, T service) {
@@ -695,7 +692,6 @@
     @Test
     public void testPrepare_throwSecurityExceptionWhenGivenPackageDoesNotBelongToTheCaller()
             throws Exception {
-        assumeTrue(isAtLeastT());
         final Vpn vpn = createVpnAndSetupUidChecks();
         assertThrows(SecurityException.class,
                 () -> vpn.prepare("com.not.vpn.owner", null, VpnManager.TYPE_VPN_SERVICE));
@@ -1168,7 +1164,6 @@
 
     @Test
     public void testStartOpAndFinishOpWillBeCalledWhenPlatformVpnIsOnAndOff() throws Exception {
-        assumeTrue(SdkLevel.isAtLeastT());
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
         when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
                 .thenReturn(mVpnProfile.encode());
@@ -1194,7 +1189,6 @@
 
     @Test
     public void testStartOpWithSeamlessHandover() throws Exception {
-        assumeTrue(SdkLevel.isAtLeastT());
         final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_VPN);
         assertTrue(vpn.prepare(TEST_VPN_PKG, null, VpnManager.TYPE_VPN_SERVICE));
         final VpnConfig config = new VpnConfig();
@@ -1226,7 +1220,7 @@
     }
 
     private void verifyVpnManagerEvent(String sessionKey, String category, int errorClass,
-            int errorCode, VpnProfileState... profileState) {
+            int errorCode, String[] packageName, VpnProfileState... profileState) {
         final Context userContext =
                 mContext.createContextAsUser(UserHandle.of(PRIMARY_USER.id), 0 /* flags */);
         final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -1236,9 +1230,11 @@
 
         for (int i = 0; i < verifyTimes; i++) {
             final Intent intent = intentArgumentCaptor.getAllValues().get(i);
+            assertEquals(packageName[i], intent.getPackage());
             assertEquals(sessionKey, intent.getStringExtra(VpnManager.EXTRA_SESSION_KEY));
             final Set<String> categories = intent.getCategories();
             assertTrue(categories.contains(category));
+            assertEquals(1, categories.size());
             assertEquals(errorClass,
                     intent.getIntExtra(VpnManager.EXTRA_ERROR_CLASS, -1 /* defaultValue */));
             assertEquals(errorCode,
@@ -1251,9 +1247,21 @@
         reset(userContext);
     }
 
+    private void verifyDeactivatedByUser(String sessionKey, String[] packageName) {
+        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
+        // errorCode won't be set.
+        verifyVpnManagerEvent(sessionKey, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                -1 /* errorClass */, -1 /* errorCode */, packageName, null /* profileState */);
+    }
+
+    private void verifyAlwaysOnStateChanged(String[] packageName, VpnProfileState... profileState) {
+        verifyVpnManagerEvent(null /* sessionKey */,
+                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
+                -1 /* errorCode */, packageName, profileState);
+    }
+
     @Test
     public void testVpnManagerEventForUserDeactivated() throws Exception {
-        assumeTrue(SdkLevel.isAtLeastT());
         // For security reasons, Vpn#prepare() will check that oldPackage and newPackage are either
         // null or the package of the caller. This test will call Vpn#prepare() to pretend the old
         // VPN is replaced by a new one. But only Settings can change to some other packages, and
@@ -1271,10 +1279,7 @@
         verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
         verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
         reset(mDeviceIdleInternal);
-        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
-        // errorCode won't be set.
-        verifyVpnManagerEvent(sessionKey1, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
-                -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+        verifyDeactivatedByUser(sessionKey1, new String[] {TEST_VPN_PKG});
         reset(mAppOps);
 
         // Test the case that the user chooses another vpn and the original one is replaced.
@@ -1284,15 +1289,11 @@
         verifyPlatformVpnIsDeactivated(TEST_VPN_PKG);
         verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
         reset(mDeviceIdleInternal);
-        // CATEGORY_EVENT_DEACTIVATED_BY_USER is not an error event, so both of errorClass and
-        // errorCode won't be set.
-        verifyVpnManagerEvent(sessionKey2, VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
-                -1 /* errorClass */, -1 /* errorCode */, null /* profileState */);
+        verifyDeactivatedByUser(sessionKey2, new String[] {TEST_VPN_PKG});
     }
 
     @Test
     public void testVpnManagerEventForAlwaysOnChanged() throws Exception {
-        assumeTrue(SdkLevel.isAtLeastT());
         // Calling setAlwaysOnPackage() needs to hold CONTROL_VPN.
         doReturn(PERMISSION_GRANTED).when(mContext).checkCallingOrSelfPermission(CONTROL_VPN);
         final Vpn vpn = createVpn(PRIMARY_USER.id);
@@ -1301,9 +1302,8 @@
                 null /* lockdownAllowlist */));
         verifyPowerSaveTempWhitelistApp(PKGS[1]);
         reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
 
         // Enable VPN lockdown for PKGS[1].
@@ -1311,9 +1311,8 @@
                 null /* lockdownAllowlist */));
         verifyPowerSaveTempWhitelistApp(PKGS[1]);
         reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, true /* alwaysOn */, true /* lockdown */));
 
         // Disable VPN lockdown for PKGS[1].
@@ -1321,9 +1320,8 @@
                 null /* lockdownAllowlist */));
         verifyPowerSaveTempWhitelistApp(PKGS[1]);
         reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
 
         // Disable VPN always-on.
@@ -1331,9 +1329,8 @@
                 null /* lockdownAllowlist */));
         verifyPowerSaveTempWhitelistApp(PKGS[1]);
         reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, false /* alwaysOn */, false /* lockdown */));
 
         // Enable VPN always-on for PKGS[1] again.
@@ -1341,9 +1338,8 @@
                 null /* lockdownAllowlist */));
         verifyPowerSaveTempWhitelistApp(PKGS[1]);
         reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+        verifyAlwaysOnStateChanged(new String[] {PKGS[1]},
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
 
         // Enable VPN always-on for PKGS[2].
@@ -1355,9 +1351,8 @@
         // Pass 2 VpnProfileState objects to verifyVpnManagerEvent(), the first one is sent to
         // PKGS[1] to notify PKGS[1] that the VPN always-on is disabled, the second one is sent to
         // PKGS[2] to notify PKGS[2] that the VPN always-on is enabled.
-        verifyVpnManagerEvent(null /* sessionKey */,
-                VpnManager.CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED, -1 /* errorClass */,
-                -1 /* errorCode */, new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
+        verifyAlwaysOnStateChanged(new String[] {PKGS[1], PKGS[2]},
+                new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, false /* alwaysOn */, false /* lockdown */),
                 new VpnProfileState(VpnProfileState.STATE_DISCONNECTED,
                         null /* sessionKey */, true /* alwaysOn */, false /* lockdown */));
@@ -1479,7 +1474,8 @@
 
         verifyPowerSaveTempWhitelistApp(TEST_VPN_PKG);
         reset(mDeviceIdleInternal);
-        verifyVpnManagerEvent(sessionKey, category, errorType, errorCode, null /* profileState */);
+        verifyVpnManagerEvent(sessionKey, category, errorType, errorCode,
+                new String[] {TEST_VPN_PKG}, null /* profileState */);
         if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
             verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
                     .unregisterNetworkCallback(eq(cb));
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
index dde1d94..5e7f0ff 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -23,24 +23,15 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.net.EthernetManager;
-import android.net.IEthernetServiceListener;
 import android.net.INetd;
 import android.net.InetAddresses;
-import android.net.InterfaceConfigurationParcel;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
@@ -66,7 +57,6 @@
 
 import java.net.InetAddress;
 import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 @SmallTest
 @RunWith(DevSdkIgnoreRunner.class)
@@ -384,74 +374,4 @@
 
         assertTrue(isValidTestInterface);
     }
-
-    public static class EthernetStateListener extends IEthernetServiceListener.Stub {
-        @Override
-        public void onEthernetStateChanged(int state) { }
-
-        @Override
-        public void onInterfaceStateChanged(String iface, int state, int role,
-                IpConfiguration configuration) { }
-    }
-
-    private InterfaceConfigurationParcel createMockedIfaceParcel(final String ifname,
-            final String hwAddr) {
-        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
-        ifaceParcel.ifName = ifname;
-        ifaceParcel.hwAddr = hwAddr;
-        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
-        return ifaceParcel;
-    }
-
-    @Test
-    public void testListenEthernetStateChange() throws Exception {
-        tracker.setIncludeTestInterfaces(true);
-        waitForIdle();
-
-        final String testIface = "testtap123";
-        final String testHwAddr = "11:22:33:44:55:66";
-        final InterfaceConfigurationParcel ifaceParcel = createMockedIfaceParcel(testIface,
-                testHwAddr);
-        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
-        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
-        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
-
-        final AtomicBoolean ifaceUp = new AtomicBoolean(true);
-        doAnswer(inv -> ifaceUp.get()).when(mFactory).hasInterface(testIface);
-        doAnswer(inv ->
-                ifaceUp.get() ? EthernetManager.STATE_LINK_UP : EthernetManager.STATE_ABSENT)
-                .when(mFactory).getInterfaceState(testIface);
-        doAnswer(inv -> {
-            ifaceUp.set(true);
-            return null;
-        }).when(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
-        doAnswer(inv -> {
-            ifaceUp.set(false);
-            return null;
-        }).when(mFactory).removeInterface(testIface);
-
-        final EthernetStateListener listener = spy(new EthernetStateListener());
-        tracker.addListener(listener, true /* canUseRestrictedNetworks */);
-        // Check default state.
-        waitForIdle();
-        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
-                anyInt(), any());
-        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
-        reset(listener);
-
-        tracker.setEthernetEnabled(false);
-        waitForIdle();
-        verify(mFactory).removeInterface(eq(testIface));
-        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED));
-        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT),
-                anyInt(), any());
-        reset(listener);
-
-        tracker.setEthernetEnabled(true);
-        waitForIdle();
-        verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
-        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
-        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
-                anyInt(), any());
-    }
 }