Merge "[FUI26] Address comments on aosp/1560408"
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 9d1c1ff..390c3b9 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -74,6 +74,36 @@
 public class VcnManager {
     @NonNull private static final String TAG = VcnManager.class.getSimpleName();
 
+    /**
+     * Key for WiFi entry RSSI thresholds
+     *
+     * <p>The VCN will only migrate to a Carrier WiFi network that has a signal strength greater
+     * than, or equal to this threshold.
+     *
+     * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY =
+            "vcn_network_selection_wifi_entry_rssi_threshold";
+
+    /**
+     * Key for WiFi entry RSSI thresholds
+     *
+     * <p>If the VCN's selected Carrier WiFi network has a signal strength less than this threshold,
+     * the VCN will attempt to migrate away from the Carrier WiFi network.
+     *
+     * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
+     *
+     * @hide
+     */
+    @NonNull
+    public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY =
+            "vcn_network_selection_wifi_exit_rssi_threshold";
+
+    // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
+
     private static final Map<
                     VcnNetworkPolicyChangeListener, VcnUnderlyingNetworkPolicyListenerBinder>
             REGISTERED_POLICY_LISTENERS = new ConcurrentHashMap<>();
diff --git a/packages/Connectivity/framework/api/module-lib-current.txt b/packages/Connectivity/framework/api/module-lib-current.txt
index b219375..6c454bc 100644
--- a/packages/Connectivity/framework/api/module-lib-current.txt
+++ b/packages/Connectivity/framework/api/module-lib-current.txt
@@ -48,6 +48,7 @@
 
   public class ConnectivitySettingsManager {
     method public static void clearGlobalProxy(@NonNull android.content.Context);
+    method @NonNull public static java.util.Set<java.lang.String> getAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
     method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
     method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
     method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
@@ -65,9 +66,9 @@
     method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context);
     method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context);
     method public static int getPrivateDnsMode(@NonNull android.content.Context);
-    method @NonNull public static java.util.Set<java.lang.String> getRestrictedAllowedApps(@NonNull android.content.Context);
     method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
     method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+    method public static void setAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.String>);
     method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
     method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
     method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
@@ -85,7 +86,6 @@
     method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int);
     method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String);
     method public static void setPrivateDnsMode(@NonNull android.content.Context, int);
-    method public static void setRestrictedAllowedApps(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.String>);
     method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
     method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
     field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
diff --git a/packages/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java b/packages/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java
index 07754e4..762f24f 100644
--- a/packages/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/packages/Connectivity/framework/src/android/net/ConnectivitySettingsManager.java
@@ -43,7 +43,6 @@
 import java.util.List;
 import java.util.Set;
 import java.util.StringJoiner;
-import java.util.regex.Pattern;
 
 /**
  * A manager class for connectivity module settings.
@@ -375,11 +374,12 @@
     private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING = "hostname";
 
     /**
-     * A list of apps that should be granted netd system permission for using restricted networks.
+     * A list of apps that is allowed on restricted networks.
      *
      * @hide
      */
-    public static final String RESTRICTED_ALLOWED_APPS = "restricted_allowed_apps";
+    public static final String APPS_ALLOWED_ON_RESTRICTED_NETWORKS =
+            "apps_allowed_on_restricted_networks";
 
     /**
      * Get mobile data activity timeout from {@link Settings}.
@@ -1047,17 +1047,16 @@
     }
 
     /**
-     * Get the list of apps(from {@link Settings}) that should be granted netd system permission for
-     * using restricted networks.
+     * Get the list of apps(from {@link Settings}) that is allowed on restricted networks.
      *
      * @param context The {@link Context} to query the setting.
-     * @return A list of apps that should be granted netd system permission for using restricted
-     *         networks or null if no setting value.
+     * @return A list of apps that is allowed on restricted networks or null if no setting
+     *         value.
      */
     @NonNull
-    public static Set<String> getRestrictedAllowedApps(@NonNull Context context) {
+    public static Set<String> getAppsAllowedOnRestrictedNetworks(@NonNull Context context) {
         final String appList = Settings.Secure.getString(
-                context.getContentResolver(), RESTRICTED_ALLOWED_APPS);
+                context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS);
         if (TextUtils.isEmpty(appList)) {
             return new ArraySet<>();
         }
@@ -1065,27 +1064,24 @@
     }
 
     /**
-     * Set the list of apps(from {@link Settings}) that should be granted netd system permission for
-     * using restricted networks.
+     * Set the list of apps(from {@link Settings}) that is allowed on restricted networks.
      *
      * Note: Please refer to android developer guidelines for valid app(package name).
      * https://developer.android.com/guide/topics/manifest/manifest-element.html#package
      *
      * @param context The {@link Context} to set the setting.
-     * @param list A list of apps that should be granted netd system permission for using
-     *             restricted networks.
+     * @param list A list of apps that is allowed on restricted networks.
      */
-    public static void setRestrictedAllowedApps(@NonNull Context context,
+    public static void setAppsAllowedOnRestrictedNetworks(@NonNull Context context,
             @NonNull Set<String> list) {
-        final Pattern appPattern = Pattern.compile("[a-zA-Z_0-9]+([.][a-zA-Z_0-9]+)*");
         final StringJoiner joiner = new StringJoiner(";");
         for (String app : list) {
-            if (!appPattern.matcher(app).matches()) {
+            if (app == null || app.contains(";")) {
                 throw new IllegalArgumentException("Invalid app(package name)");
             }
             joiner.add(app);
         }
-        Settings.Secure.putString(
-                context.getContentResolver(), RESTRICTED_ALLOWED_APPS, joiner.toString());
+        Settings.Secure.putString(context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS,
+                joiner.toString());
     }
 }
diff --git a/packages/Connectivity/service/Android.bp b/packages/Connectivity/service/Android.bp
index 1330e71..513de19 100644
--- a/packages/Connectivity/service/Android.bp
+++ b/packages/Connectivity/service/Android.bp
@@ -52,8 +52,8 @@
 java_library {
     name: "service-connectivity-pre-jarjar",
     srcs: [
+        "src/**/*.java",
         ":framework-connectivity-shared-srcs",
-        ":connectivity-service-srcs",
     ],
     libs: [
         "android.net.ipsec.ike",
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/packages/Connectivity/service/src/com/android/server/ConnectivityService.java
similarity index 99%
rename from services/core/java/com/android/server/ConnectivityService.java
rename to packages/Connectivity/service/src/com/android/server/ConnectivityService.java
index 7ffca45..051a00b 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/packages/Connectivity/service/src/com/android/server/ConnectivityService.java
@@ -1394,7 +1394,7 @@
         // arguments like the handler or the DnsResolver.
         // TODO : remove this ; it is probably better handled with a sentinel request.
         mNoServiceNetwork = new NetworkAgentInfo(null,
-                new Network(NO_SERVICE_NET_ID),
+                new Network(INetd.UNREACHABLE_NET_ID),
                 new NetworkInfo(TYPE_NONE, 0, "", ""),
                 new LinkProperties(), new NetworkCapabilities(),
                 new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
@@ -6488,8 +6488,6 @@
     // Request used to optionally keep vehicle internal network always active
     private final NetworkRequest mDefaultVehicleRequest;
 
-    // TODO replace with INetd.UNREACHABLE_NET_ID when available.
-    private static final int NO_SERVICE_NET_ID = 52;
     // Sentinel NAI used to direct apps with default networks that should have no connectivity to a
     // network with no service. This NAI should never be matched against, nor should any public API
     // ever return the associated network. For this reason, this NAI is not in the list of available
diff --git a/services/core/java/com/android/server/ConnectivityServiceInitializer.java b/packages/Connectivity/service/src/com/android/server/ConnectivityServiceInitializer.java
similarity index 100%
rename from services/core/java/com/android/server/ConnectivityServiceInitializer.java
rename to packages/Connectivity/service/src/com/android/server/ConnectivityServiceInitializer.java
diff --git a/services/core/java/com/android/server/NetIdManager.java b/packages/Connectivity/service/src/com/android/server/NetIdManager.java
similarity index 100%
rename from services/core/java/com/android/server/NetIdManager.java
rename to packages/Connectivity/service/src/com/android/server/NetIdManager.java
diff --git a/services/core/java/com/android/server/TestNetworkService.java b/packages/Connectivity/service/src/com/android/server/TestNetworkService.java
similarity index 100%
rename from services/core/java/com/android/server/TestNetworkService.java
rename to packages/Connectivity/service/src/com/android/server/TestNetworkService.java
diff --git a/services/core/java/com/android/server/connectivity/AutodestructReference.java b/packages/Connectivity/service/src/com/android/server/connectivity/AutodestructReference.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/AutodestructReference.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/AutodestructReference.java
diff --git a/services/core/java/com/android/server/connectivity/ConnectivityConstants.java b/packages/Connectivity/service/src/com/android/server/connectivity/ConnectivityConstants.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/ConnectivityConstants.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/ConnectivityConstants.java
diff --git a/services/core/java/com/android/server/connectivity/DnsManager.java b/packages/Connectivity/service/src/com/android/server/connectivity/DnsManager.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/DnsManager.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/DnsManager.java
diff --git a/services/core/java/com/android/server/connectivity/FullScore.java b/packages/Connectivity/service/src/com/android/server/connectivity/FullScore.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/FullScore.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/FullScore.java
diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/packages/Connectivity/service/src/com/android/server/connectivity/KeepaliveTracker.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/KeepaliveTracker.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/KeepaliveTracker.java
diff --git a/services/core/java/com/android/server/connectivity/LingerMonitor.java b/packages/Connectivity/service/src/com/android/server/connectivity/LingerMonitor.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/LingerMonitor.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/LingerMonitor.java
diff --git a/services/core/java/com/android/server/connectivity/MockableSystemProperties.java b/packages/Connectivity/service/src/com/android/server/connectivity/MockableSystemProperties.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/MockableSystemProperties.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/MockableSystemProperties.java
diff --git a/services/core/java/com/android/server/connectivity/Nat464Xlat.java b/packages/Connectivity/service/src/com/android/server/connectivity/Nat464Xlat.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/Nat464Xlat.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/Nat464Xlat.java
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/packages/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/NetworkAgentInfo.java
diff --git a/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java b/packages/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/NetworkDiagnostics.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/NetworkDiagnostics.java
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/packages/Connectivity/service/src/com/android/server/connectivity/NetworkNotificationManager.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/NetworkNotificationManager.java
diff --git a/services/core/java/com/android/server/connectivity/NetworkOffer.java b/packages/Connectivity/service/src/com/android/server/connectivity/NetworkOffer.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/NetworkOffer.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/NetworkOffer.java
diff --git a/services/core/java/com/android/server/connectivity/NetworkRanker.java b/packages/Connectivity/service/src/com/android/server/connectivity/NetworkRanker.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/NetworkRanker.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/NetworkRanker.java
diff --git a/services/core/java/com/android/server/connectivity/PermissionMonitor.java b/packages/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/PermissionMonitor.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/PermissionMonitor.java
diff --git a/services/core/java/com/android/server/connectivity/ProfileNetworkPreferences.java b/packages/Connectivity/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/ProfileNetworkPreferences.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
diff --git a/services/core/java/com/android/server/connectivity/ProxyTracker.java b/packages/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/ProxyTracker.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/ProxyTracker.java
diff --git a/services/core/java/com/android/server/connectivity/QosCallbackAgentConnection.java b/packages/Connectivity/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/QosCallbackAgentConnection.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
diff --git a/services/core/java/com/android/server/connectivity/QosCallbackTracker.java b/packages/Connectivity/service/src/com/android/server/connectivity/QosCallbackTracker.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/QosCallbackTracker.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/QosCallbackTracker.java
diff --git a/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java b/packages/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java
similarity index 100%
rename from services/core/java/com/android/server/connectivity/TcpKeepaliveController.java
rename to packages/Connectivity/service/src/com/android/server/connectivity/TcpKeepaliveController.java
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 9d50564..706f738 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -10,7 +10,6 @@
 filegroup {
     name: "services.core-sources",
     srcs: ["java/**/*.java"],
-    exclude_srcs: [":connectivity-service-srcs"],
     path: "java",
     visibility: ["//frameworks/base/services"],
 }
@@ -200,30 +199,3 @@
     src: ":services.core.json.gz",
 }
 
-// TODO: Move connectivity service sources to independent directory.
-filegroup {
-    name: "connectivity-service-srcs",
-    srcs: [
-        "java/com/android/server/ConnectivityService.java",
-        "java/com/android/server/ConnectivityServiceInitializer.java",
-        "java/com/android/server/TestNetworkService.java",
-        "java/com/android/server/connectivity/AutodestructReference.java",
-        "java/com/android/server/connectivity/ConnectivityConstants.java",
-        "java/com/android/server/connectivity/DnsManager.java",
-        "java/com/android/server/connectivity/FullScore.java",
-        "java/com/android/server/connectivity/KeepaliveTracker.java",
-        "java/com/android/server/connectivity/LingerMonitor.java",
-        "java/com/android/server/connectivity/MockableSystemProperties.java",
-        "java/com/android/server/connectivity/Nat464Xlat.java",
-        "java/com/android/server/connectivity/NetworkAgentInfo.java",
-        "java/com/android/server/connectivity/NetworkDiagnostics.java",
-        "java/com/android/server/connectivity/NetworkNotificationManager.java",
-        "java/com/android/server/connectivity/NetworkOffer.java",
-        "java/com/android/server/connectivity/NetworkRanker.java",
-        "java/com/android/server/connectivity/PermissionMonitor.java",
-        "java/com/android/server/connectivity/ProxyTracker.java",
-        "java/com/android/server/connectivity/QosCallbackAgentConnection.java",
-        "java/com/android/server/connectivity/QosCallbackTracker.java",
-        "java/com/android/server/connectivity/TcpKeepaliveController.java",
-    ],
-}
diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
index 19fbdbd..5565ccb 100644
--- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
+++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -145,7 +145,7 @@
      */
     public void handleSubscriptionsChanged() {
         final Map<ParcelUuid, Set<String>> privilegedPackages = new HashMap<>();
-        final Map<Integer, ParcelUuid> newSubIdToGroupMap = new HashMap<>();
+        final Map<Integer, SubscriptionInfo> newSubIdToInfoMap = new HashMap<>();
 
         final List<SubscriptionInfo> allSubs = mSubscriptionManager.getAllSubscriptionInfoList();
         if (allSubs == null) {
@@ -160,7 +160,7 @@
             }
 
             // Build subId -> subGrp cache
-            newSubIdToGroupMap.put(subInfo.getSubscriptionId(), subInfo.getGroupUuid());
+            newSubIdToInfoMap.put(subInfo.getSubscriptionId(), subInfo);
 
             // Update subscription groups that are both ready, and active. For a group to be
             // considered active, both of the following must be true:
@@ -186,7 +186,7 @@
         }
 
         final TelephonySubscriptionSnapshot newSnapshot =
-                new TelephonySubscriptionSnapshot(newSubIdToGroupMap, privilegedPackages);
+                new TelephonySubscriptionSnapshot(newSubIdToInfoMap, privilegedPackages);
 
         // If snapshot was meaningfully updated, fire the callback
         if (!newSnapshot.equals(mCurrentSnapshot)) {
@@ -245,7 +245,7 @@
 
     /** TelephonySubscriptionSnapshot is a class containing info about active subscriptions */
     public static class TelephonySubscriptionSnapshot {
-        private final Map<Integer, ParcelUuid> mSubIdToGroupMap;
+        private final Map<Integer, SubscriptionInfo> mSubIdToInfoMap;
         private final Map<ParcelUuid, Set<String>> mPrivilegedPackages;
 
         public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT =
@@ -253,12 +253,12 @@
 
         @VisibleForTesting(visibility = Visibility.PRIVATE)
         TelephonySubscriptionSnapshot(
-                @NonNull Map<Integer, ParcelUuid> subIdToGroupMap,
+                @NonNull Map<Integer, SubscriptionInfo> subIdToInfoMap,
                 @NonNull Map<ParcelUuid, Set<String>> privilegedPackages) {
-            Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null");
+            Objects.requireNonNull(subIdToInfoMap, "subIdToInfoMap was null");
             Objects.requireNonNull(privilegedPackages, "privilegedPackages was null");
 
-            mSubIdToGroupMap = Collections.unmodifiableMap(subIdToGroupMap);
+            mSubIdToInfoMap = Collections.unmodifiableMap(subIdToInfoMap);
 
             final Map<ParcelUuid, Set<String>> unmodifiableInnerSets = new ArrayMap<>();
             for (Entry<ParcelUuid, Set<String>> entry : privilegedPackages.entrySet()) {
@@ -285,7 +285,9 @@
         /** Returns the Subscription Group for a given subId. */
         @Nullable
         public ParcelUuid getGroupForSubId(int subId) {
-            return mSubIdToGroupMap.get(subId);
+            return mSubIdToInfoMap.containsKey(subId)
+                    ? mSubIdToInfoMap.get(subId).getGroupUuid()
+                    : null;
         }
 
         /**
@@ -295,8 +297,8 @@
         public Set<Integer> getAllSubIdsInGroup(ParcelUuid subGrp) {
             final Set<Integer> subIds = new ArraySet<>();
 
-            for (Entry<Integer, ParcelUuid> entry : mSubIdToGroupMap.entrySet()) {
-                if (subGrp.equals(entry.getValue())) {
+            for (Entry<Integer, SubscriptionInfo> entry : mSubIdToInfoMap.entrySet()) {
+                if (subGrp.equals(entry.getValue().getGroupUuid())) {
                     subIds.add(entry.getKey());
                 }
             }
@@ -304,9 +306,17 @@
             return subIds;
         }
 
+        /** Checks if the requested subscription is opportunistic */
+        @NonNull
+        public boolean isOpportunistic(int subId) {
+            return mSubIdToInfoMap.containsKey(subId)
+                    ? mSubIdToInfoMap.get(subId).isOpportunistic()
+                    : false;
+        }
+
         @Override
         public int hashCode() {
-            return Objects.hash(mSubIdToGroupMap, mPrivilegedPackages);
+            return Objects.hash(mSubIdToInfoMap, mPrivilegedPackages);
         }
 
         @Override
@@ -317,7 +327,7 @@
 
             final TelephonySubscriptionSnapshot other = (TelephonySubscriptionSnapshot) obj;
 
-            return mSubIdToGroupMap.equals(other.mSubIdToGroupMap)
+            return mSubIdToInfoMap.equals(other.mSubIdToInfoMap)
                     && mPrivilegedPackages.equals(other.mPrivilegedPackages);
         }
 
@@ -326,7 +336,7 @@
             pw.println("TelephonySubscriptionSnapshot:");
             pw.increaseIndent();
 
-            pw.println("mSubIdToGroupMap: " + mSubIdToGroupMap);
+            pw.println("mSubIdToInfoMap: " + mSubIdToInfoMap);
             pw.println("mPrivilegedPackages: " + mPrivilegedPackages);
 
             pw.decreaseIndent();
@@ -335,7 +345,7 @@
         @Override
         public String toString() {
             return "TelephonySubscriptionSnapshot{ "
-                    + "mSubIdToGroupMap=" + mSubIdToGroupMap
+                    + "mSubIdToInfoMap=" + mSubIdToInfoMap
                     + ", mPrivilegedPackages=" + mPrivilegedPackages
                     + " }";
         }
diff --git a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java b/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
index 3bdeec0..fb4c623 100644
--- a/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
+++ b/services/core/java/com/android/server/vcn/UnderlyingNetworkTracker.java
@@ -16,6 +16,10 @@
 
 package com.android.server.vcn;
 
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.ConnectivityManager;
@@ -25,8 +29,16 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnManager;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyCallback;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -35,9 +47,13 @@
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.TreeSet;
 
 /**
  * Tracks a set of Networks underpinning a VcnGatewayConnection.
@@ -51,6 +67,45 @@
 public class UnderlyingNetworkTracker {
     @NonNull private static final String TAG = UnderlyingNetworkTracker.class.getSimpleName();
 
+    /**
+     * Minimum signal strength for a WiFi network to be eligible for switching to
+     *
+     * <p>A network that satisfies this is eligible to become the selected underlying network with
+     * no additional conditions
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT = -70;
+
+    /**
+     * Minimum signal strength to continue using a WiFi network
+     *
+     * <p>A network that satisfies the conditions may ONLY continue to be used if it is already
+     * selected as the underlying network. A WiFi network satisfying this condition, but NOT the
+     * prospective-network RSSI threshold CANNOT be switched to.
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int WIFI_EXIT_RSSI_THRESHOLD_DEFAULT = -74;
+
+    /** Priority for any cellular network for which the subscription is listed as opportunistic */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PRIORITY_OPPORTUNISTIC_CELLULAR = 0;
+
+    /** Priority for any WiFi network which is in use, and satisfies the in-use RSSI threshold */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PRIORITY_WIFI_IN_USE = 1;
+
+    /** Priority for any WiFi network which satisfies the prospective-network RSSI threshold */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PRIORITY_WIFI_PROSPECTIVE = 2;
+
+    /** Priority for any standard macro cellular network */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PRIORITY_MACRO_CELLULAR = 3;
+
+    /** Priority for any other networks (including unvalidated, etc) */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int PRIORITY_ANY = Integer.MAX_VALUE;
+
     @NonNull private final VcnContext mVcnContext;
     @NonNull private final ParcelUuid mSubscriptionGroup;
     @NonNull private final Set<Integer> mRequiredUnderlyingNetworkCapabilities;
@@ -58,12 +113,17 @@
     @NonNull private final Dependencies mDeps;
     @NonNull private final Handler mHandler;
     @NonNull private final ConnectivityManager mConnectivityManager;
+    @NonNull private final TelephonyCallback mActiveDataSubIdListener =
+            new VcnActiveDataSubscriptionIdListener();
 
     @NonNull private final List<NetworkCallback> mCellBringupCallbacks = new ArrayList<>();
     @Nullable private NetworkCallback mWifiBringupCallback;
-    @Nullable private NetworkCallback mRouteSelectionCallback;
+    @Nullable private NetworkCallback mWifiEntryRssiThresholdCallback;
+    @Nullable private NetworkCallback mWifiExitRssiThresholdCallback;
+    @Nullable private UnderlyingNetworkListener mRouteSelectionCallback;
 
     @NonNull private TelephonySubscriptionSnapshot mLastSnapshot;
+    @Nullable private PersistableBundle mCarrierConfig;
     private boolean mIsQuitting = false;
 
     @Nullable private UnderlyingNetworkRecord mCurrentRecord;
@@ -104,6 +164,30 @@
         mHandler = new Handler(mVcnContext.getLooper());
 
         mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class);
+        mVcnContext
+                .getContext()
+                .getSystemService(TelephonyManager.class)
+                .registerTelephonyCallback(new HandlerExecutor(mHandler), mActiveDataSubIdListener);
+
+        // TODO: Listen for changes in carrier config that affect this.
+        for (int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) {
+            PersistableBundle config =
+                    mVcnContext
+                            .getContext()
+                            .getSystemService(CarrierConfigManager.class)
+                            .getConfigForSubId(subId);
+
+            if (config != null) {
+                mCarrierConfig = config;
+
+                // Attempt to use (any) non-opportunistic subscription. If this subscription is
+                // opportunistic, continue and try to find a non-opportunistic subscription, using
+                // the opportunistic ones as a last resort.
+                if (!isOpportunistic(mLastSnapshot, Collections.singleton(subId))) {
+                    break;
+                }
+            }
+        }
 
         registerOrUpdateNetworkRequests();
     }
@@ -111,16 +195,30 @@
     private void registerOrUpdateNetworkRequests() {
         NetworkCallback oldRouteSelectionCallback = mRouteSelectionCallback;
         NetworkCallback oldWifiCallback = mWifiBringupCallback;
+        NetworkCallback oldWifiEntryRssiThresholdCallback = mWifiEntryRssiThresholdCallback;
+        NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback;
         List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
         mCellBringupCallbacks.clear();
 
         // Register new callbacks. Make-before-break; always register new callbacks before removal
         // of old callbacks
         if (!mIsQuitting) {
-            mRouteSelectionCallback = new RouteSelectionCallback();
-            mConnectivityManager.requestBackgroundNetwork(
+            mRouteSelectionCallback = new UnderlyingNetworkListener();
+            mConnectivityManager.registerNetworkCallback(
                     getRouteSelectionRequest(), mRouteSelectionCallback, mHandler);
 
+            mWifiEntryRssiThresholdCallback = new NetworkBringupCallback();
+            mConnectivityManager.registerNetworkCallback(
+                    getWifiEntryRssiThresholdNetworkRequest(),
+                    mWifiEntryRssiThresholdCallback,
+                    mHandler);
+
+            mWifiExitRssiThresholdCallback = new NetworkBringupCallback();
+            mConnectivityManager.registerNetworkCallback(
+                    getWifiExitRssiThresholdNetworkRequest(),
+                    mWifiExitRssiThresholdCallback,
+                    mHandler);
+
             mWifiBringupCallback = new NetworkBringupCallback();
             mConnectivityManager.requestBackgroundNetwork(
                     getWifiNetworkRequest(), mWifiBringupCallback, mHandler);
@@ -135,6 +233,8 @@
         } else {
             mRouteSelectionCallback = null;
             mWifiBringupCallback = null;
+            mWifiEntryRssiThresholdCallback = null;
+            mWifiExitRssiThresholdCallback = null;
             // mCellBringupCallbacks already cleared above.
         }
 
@@ -145,6 +245,12 @@
         if (oldWifiCallback != null) {
             mConnectivityManager.unregisterNetworkCallback(oldWifiCallback);
         }
+        if (oldWifiEntryRssiThresholdCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(oldWifiEntryRssiThresholdCallback);
+        }
+        if (oldWifiExitRssiThresholdCallback != null) {
+            mConnectivityManager.unregisterNetworkCallback(oldWifiExitRssiThresholdCallback);
+        }
         for (NetworkCallback cellBringupCallback : oldCellCallbacks) {
             mConnectivityManager.unregisterNetworkCallback(cellBringupCallback);
         }
@@ -168,6 +274,8 @@
         }
 
         return getBaseNetworkRequestBuilder()
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
                 .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
                 .build();
     }
@@ -189,6 +297,38 @@
     }
 
     /**
+     * Builds the WiFi entry threshold signal strength request
+     *
+     * <p>This request ensures that WiFi reports the crossing of the wifi entry RSSI threshold.
+     * Without this request, WiFi rate-limits, and reports signal strength changes at too slow a
+     * pace to effectively select a short-lived WiFi offload network.
+     */
+    private NetworkRequest getWifiEntryRssiThresholdNetworkRequest() {
+        return getBaseNetworkRequestBuilder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
+                // Ensure wifi updates signal strengths when crossing this threshold.
+                .setSignalStrength(getWifiEntryRssiThreshold(mCarrierConfig))
+                .build();
+    }
+
+    /**
+     * Builds the WiFi exit threshold signal strength request
+     *
+     * <p>This request ensures that WiFi reports the crossing of the wifi exit RSSI threshold.
+     * Without this request, WiFi rate-limits, and reports signal strength changes at too slow a
+     * pace to effectively select away from a failing WiFi network.
+     */
+    private NetworkRequest getWifiExitRssiThresholdNetworkRequest() {
+        return getBaseNetworkRequestBuilder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
+                // Ensure wifi updates signal strengths when crossing this threshold.
+                .setSignalStrength(getWifiExitRssiThreshold(mCarrierConfig))
+                .build();
+    }
+
+    /**
      * Builds a Cellular bringup request for a given subId
      *
      * <p>This request is filed in order to ensure that the Telephony stack always has a
@@ -233,10 +373,18 @@
      * reevaluate its NetworkBringupCallbacks. This may result in NetworkRequests being registered
      * or unregistered if the subIds mapped to the this Tracker's SubscriptionGroup change.
      */
-    public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) {
-        Objects.requireNonNull(snapshot, "Missing snapshot");
+    public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot newSnapshot) {
+        Objects.requireNonNull(newSnapshot, "Missing newSnapshot");
 
-        mLastSnapshot = snapshot;
+        final TelephonySubscriptionSnapshot oldSnapshot = mLastSnapshot;
+        mLastSnapshot = newSnapshot;
+
+        // Only trigger re-registration if subIds in this group have changed
+        if (oldSnapshot
+                .getAllSubIdsInGroup(mSubscriptionGroup)
+                .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) {
+            return;
+        }
         registerOrUpdateNetworkRequests();
     }
 
@@ -247,88 +395,43 @@
 
         // Will unregister all existing callbacks, but not register new ones due to quitting flag.
         registerOrUpdateNetworkRequests();
+
+        mVcnContext
+                .getContext()
+                .getSystemService(TelephonyManager.class)
+                .unregisterTelephonyCallback(mActiveDataSubIdListener);
     }
 
-    /** Returns whether the currently selected Network matches the given network. */
-    private static boolean isSameNetwork(
-            @Nullable UnderlyingNetworkRecord.Builder recordInProgress, @NonNull Network network) {
-        return recordInProgress != null && recordInProgress.getNetwork().equals(network);
-    }
+    private void reevaluateNetworks() {
+        TreeSet<UnderlyingNetworkRecord> sorted =
+                new TreeSet<>(
+                        UnderlyingNetworkRecord.getComparator(
+                                mSubscriptionGroup, mLastSnapshot, mCurrentRecord, mCarrierConfig));
+        sorted.addAll(mRouteSelectionCallback.getUnderlyingNetworks());
 
-    /** Notify the Callback if a full UnderlyingNetworkRecord exists. */
-    private void maybeNotifyCallback() {
-        // Only forward this update if a complete record has been received
-        if (!mRecordInProgress.isValid()) {
+        UnderlyingNetworkRecord candidate = sorted.isEmpty() ? null : sorted.first();
+        if (Objects.equals(mCurrentRecord, candidate)) {
             return;
         }
 
-        // Only forward this update if the updated record differs form the current record
-        UnderlyingNetworkRecord updatedRecord = mRecordInProgress.build();
-        if (!updatedRecord.equals(mCurrentRecord)) {
-            mCurrentRecord = updatedRecord;
-
-            mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
-        }
+        mCurrentRecord = candidate;
+        mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
     }
 
-    private void handleNetworkAvailable(@NonNull Network network) {
-        mVcnContext.ensureRunningOnLooperThread();
-
-        mRecordInProgress = new UnderlyingNetworkRecord.Builder(network);
-    }
-
-    private void handleNetworkLost(@NonNull Network network) {
-        mVcnContext.ensureRunningOnLooperThread();
-
-        if (!isSameNetwork(mRecordInProgress, network)) {
-            Slog.wtf(TAG, "Non-underlying Network lost");
-            return;
+    private static boolean isOpportunistic(
+            @NonNull TelephonySubscriptionSnapshot snapshot, Set<Integer> subIds) {
+        if (snapshot == null) {
+            Slog.wtf(TAG, "Got null snapshot");
+            return false;
         }
 
-        mRecordInProgress = null;
-        mCurrentRecord = null;
-        mCb.onSelectedUnderlyingNetworkChanged(null /* underlyingNetworkRecord */);
-    }
-
-    private void handleCapabilitiesChanged(
-            @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
-        mVcnContext.ensureRunningOnLooperThread();
-
-        if (!isSameNetwork(mRecordInProgress, network)) {
-            Slog.wtf(TAG, "Invalid update to NetworkCapabilities");
-            return;
+        for (int subId : subIds) {
+            if (snapshot.isOpportunistic(subId)) {
+                return true;
+            }
         }
 
-        mRecordInProgress.setNetworkCapabilities(networkCapabilities);
-
-        maybeNotifyCallback();
-    }
-
-    private void handlePropertiesChanged(
-            @NonNull Network network, @NonNull LinkProperties linkProperties) {
-        mVcnContext.ensureRunningOnLooperThread();
-
-        if (!isSameNetwork(mRecordInProgress, network)) {
-            Slog.wtf(TAG, "Invalid update to LinkProperties");
-            return;
-        }
-
-        mRecordInProgress.setLinkProperties(linkProperties);
-
-        maybeNotifyCallback();
-    }
-
-    private void handleNetworkBlocked(@NonNull Network network, boolean isBlocked) {
-        mVcnContext.ensureRunningOnLooperThread();
-
-        if (!isSameNetwork(mRecordInProgress, network)) {
-            Slog.wtf(TAG, "Invalid update to isBlocked");
-            return;
-        }
-
-        mRecordInProgress.setIsBlocked(isBlocked);
-
-        maybeNotifyCallback();
+        return false;
     }
 
     /**
@@ -347,36 +450,104 @@
      * truth.
      */
     @VisibleForTesting
-    class RouteSelectionCallback extends NetworkCallback {
+    class UnderlyingNetworkListener extends NetworkCallback {
+        private final Map<Network, UnderlyingNetworkRecord.Builder>
+                mUnderlyingNetworkRecordBuilders = new ArrayMap<>();
+
+        private List<UnderlyingNetworkRecord> getUnderlyingNetworks() {
+            final List<UnderlyingNetworkRecord> records = new ArrayList<>();
+
+            for (UnderlyingNetworkRecord.Builder builder :
+                    mUnderlyingNetworkRecordBuilders.values()) {
+                if (builder.isValid()) {
+                    records.add(builder.build());
+                }
+            }
+
+            return records;
+        }
+
         @Override
         public void onAvailable(@NonNull Network network) {
-            handleNetworkAvailable(network);
+            mUnderlyingNetworkRecordBuilders.put(
+                    network, new UnderlyingNetworkRecord.Builder(network));
         }
 
         @Override
         public void onLost(@NonNull Network network) {
-            handleNetworkLost(network);
+            mUnderlyingNetworkRecordBuilders.remove(network);
+
+            reevaluateNetworks();
         }
 
         @Override
         public void onCapabilitiesChanged(
                 @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
-            if (networkCapabilities.equals(mRecordInProgress.getNetworkCapabilities())) return;
-            handleCapabilitiesChanged(network, networkCapabilities);
+            final UnderlyingNetworkRecord.Builder builder =
+                    mUnderlyingNetworkRecordBuilders.get(network);
+            if (builder == null) {
+                Slog.wtf(TAG, "Got capabilities change for unknown key: " + network);
+                return;
+            }
+
+            builder.setNetworkCapabilities(networkCapabilities);
+            if (builder.isValid()) {
+                reevaluateNetworks();
+            }
         }
 
         @Override
         public void onLinkPropertiesChanged(
                 @NonNull Network network, @NonNull LinkProperties linkProperties) {
-            handlePropertiesChanged(network, linkProperties);
+            final UnderlyingNetworkRecord.Builder builder =
+                    mUnderlyingNetworkRecordBuilders.get(network);
+            if (builder == null) {
+                Slog.wtf(TAG, "Got link properties change for unknown key: " + network);
+                return;
+            }
+
+            builder.setLinkProperties(linkProperties);
+            if (builder.isValid()) {
+                reevaluateNetworks();
+            }
         }
 
         @Override
         public void onBlockedStatusChanged(@NonNull Network network, boolean isBlocked) {
-            handleNetworkBlocked(network, isBlocked);
+            final UnderlyingNetworkRecord.Builder builder =
+                    mUnderlyingNetworkRecordBuilders.get(network);
+            if (builder == null) {
+                Slog.wtf(TAG, "Got blocked status change for unknown key: " + network);
+                return;
+            }
+
+            builder.setIsBlocked(isBlocked);
+            if (builder.isValid()) {
+                reevaluateNetworks();
+            }
         }
     }
 
+    private static int getWifiEntryRssiThreshold(@Nullable PersistableBundle carrierConfig) {
+        if (carrierConfig != null) {
+            return carrierConfig.getInt(
+                    VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
+                    WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT);
+        }
+
+        return WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
+    }
+
+    private static int getWifiExitRssiThreshold(@Nullable PersistableBundle carrierConfig) {
+        if (carrierConfig != null) {
+            return carrierConfig.getInt(
+                    VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
+                    WIFI_EXIT_RSSI_THRESHOLD_DEFAULT);
+        }
+
+        return WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
+    }
+
     /** A record of a single underlying network, caching relevant fields. */
     public static class UnderlyingNetworkRecord {
         @NonNull public final Network network;
@@ -413,6 +584,89 @@
             return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
         }
 
+        /**
+         * Gives networks a priority class, based on the following priorities:
+         *
+         * <ol>
+         *   <li>Opportunistic cellular
+         *   <li>Carrier WiFi, signal strength >= WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT
+         *   <li>Carrier WiFi, active network + signal strength >= WIFI_EXIT_RSSI_THRESHOLD_DEFAULT
+         *   <li>Macro cellular
+         *   <li>Any others
+         * </ol>
+         */
+        private int calculatePriorityClass(
+                ParcelUuid subscriptionGroup,
+                TelephonySubscriptionSnapshot snapshot,
+                UnderlyingNetworkRecord currentlySelected,
+                PersistableBundle carrierConfig) {
+            final NetworkCapabilities caps = networkCapabilities;
+
+            // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
+
+            if (isBlocked) {
+                Slog.wtf(TAG, "Network blocked for System Server: " + network);
+                return PRIORITY_ANY;
+            }
+
+            if (caps.hasTransport(TRANSPORT_CELLULAR)
+                    && isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+                // If this carrier is the active data provider, ensure that opportunistic is only
+                // ever prioritized if it is also the active data subscription. This ensures that
+                // if an opportunistic subscription is still in the process of being switched to,
+                // or switched away from, the VCN does not attempt to continue using it against the
+                // decision made at the telephony layer. Failure to do so may result in the modem
+                // switching back and forth.
+                //
+                // Allow the following two cases:
+                // 1. Active subId is NOT in the group that this VCN is supporting
+                // 2. This opportunistic subscription is for the active subId
+                if (!snapshot.getAllSubIdsInGroup(subscriptionGroup)
+                                .contains(SubscriptionManager.getActiveDataSubscriptionId())
+                        || caps.getSubscriptionIds()
+                                .contains(SubscriptionManager.getActiveDataSubscriptionId())) {
+                    return PRIORITY_OPPORTUNISTIC_CELLULAR;
+                }
+            }
+
+            if (caps.hasTransport(TRANSPORT_WIFI)) {
+                if (caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)
+                        && currentlySelected != null
+                        && network.equals(currentlySelected.network)) {
+                    return PRIORITY_WIFI_IN_USE;
+                }
+
+                if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) {
+                    return PRIORITY_WIFI_PROSPECTIVE;
+                }
+            }
+
+            // Disallow opportunistic subscriptions from matching PRIORITY_MACRO_CELLULAR, as might
+            // be the case when Default Data SubId (CBRS) != Active Data SubId (MACRO), as might be
+            // the case if the Default Data SubId does not support certain services (eg voice
+            // calling)
+            if (caps.hasTransport(TRANSPORT_CELLULAR)
+                    && !isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+                return PRIORITY_MACRO_CELLULAR;
+            }
+
+            return PRIORITY_ANY;
+        }
+
+        private static Comparator<UnderlyingNetworkRecord> getComparator(
+                ParcelUuid subscriptionGroup,
+                TelephonySubscriptionSnapshot snapshot,
+                UnderlyingNetworkRecord currentlySelected,
+                PersistableBundle carrierConfig) {
+            return (left, right) -> {
+                return Integer.compare(
+                        left.calculatePriorityClass(
+                                subscriptionGroup, snapshot, currentlySelected, carrierConfig),
+                        right.calculatePriorityClass(
+                                subscriptionGroup, snapshot, currentlySelected, carrierConfig));
+            };
+        }
+
         /** Dumps the state of this record for logging and debugging purposes. */
         public void dump(IndentingPrintWriter pw) {
             pw.println("UnderlyingNetworkRecord:");
@@ -434,6 +688,8 @@
             boolean mIsBlocked;
             boolean mWasIsBlockedSet;
 
+            @Nullable private UnderlyingNetworkRecord mCached;
+
             private Builder(@NonNull Network network) {
                 mNetwork = network;
             }
@@ -445,6 +701,7 @@
 
             private void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
                 mNetworkCapabilities = networkCapabilities;
+                mCached = null;
             }
 
             @Nullable
@@ -454,11 +711,13 @@
 
             private void setLinkProperties(@NonNull LinkProperties linkProperties) {
                 mLinkProperties = linkProperties;
+                mCached = null;
             }
 
             private void setIsBlocked(boolean isBlocked) {
                 mIsBlocked = isBlocked;
                 mWasIsBlockedSet = true;
+                mCached = null;
             }
 
             private boolean isValid() {
@@ -466,12 +725,30 @@
             }
 
             private UnderlyingNetworkRecord build() {
-                return new UnderlyingNetworkRecord(
-                        mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
+                if (!isValid()) {
+                    throw new IllegalArgumentException(
+                            "Called build before UnderlyingNetworkRecord was valid");
+                }
+
+                if (mCached == null) {
+                    mCached =
+                            new UnderlyingNetworkRecord(
+                                    mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
+                }
+
+                return mCached;
             }
         }
     }
 
+    private class VcnActiveDataSubscriptionIdListener extends TelephonyCallback
+            implements ActiveDataSubscriptionIdListener {
+        @Override
+        public void onActiveDataSubscriptionIdChanged(int subId) {
+            reevaluateNetworks();
+        }
+    }
+
     /** Callbacks for being notified of the changes in, or to the selected underlying network. */
     public interface UnderlyingNetworkTrackerCallback {
         /**
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index 528f240..ca74638 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -88,13 +88,13 @@
     private static final SubscriptionInfo TEST_SUBINFO_2 = mock(SubscriptionInfo.class);
     private static final Map<ParcelUuid, Set<String>> TEST_PRIVILEGED_PACKAGES =
             Collections.singletonMap(TEST_PARCEL_UUID, Collections.singleton(PACKAGE_NAME));
-    private static final Map<Integer, ParcelUuid> TEST_SUBID_TO_GROUP_MAP;
+    private static final Map<Integer, SubscriptionInfo> TEST_SUBID_TO_INFO_MAP;
 
     static {
-        final Map<Integer, ParcelUuid> subIdToGroupMap = new HashMap<>();
-        subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_1, TEST_PARCEL_UUID);
-        subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_2, TEST_PARCEL_UUID);
-        TEST_SUBID_TO_GROUP_MAP = Collections.unmodifiableMap(subIdToGroupMap);
+        final Map<Integer, SubscriptionInfo> subIdToGroupMap = new HashMap<>();
+        subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_1, TEST_SUBINFO_1);
+        subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_2, TEST_SUBINFO_2);
+        TEST_SUBID_TO_INFO_MAP = Collections.unmodifiableMap(subIdToGroupMap);
     }
 
     @NonNull private final Context mContext;
@@ -190,13 +190,13 @@
 
     private TelephonySubscriptionSnapshot buildExpectedSnapshot(
             Map<ParcelUuid, Set<String>> privilegedPackages) {
-        return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, privilegedPackages);
+        return buildExpectedSnapshot(TEST_SUBID_TO_INFO_MAP, privilegedPackages);
     }
 
     private TelephonySubscriptionSnapshot buildExpectedSnapshot(
-            Map<Integer, ParcelUuid> subIdToGroupMap,
+            Map<Integer, SubscriptionInfo> subIdToInfoMap,
             Map<ParcelUuid, Set<String>> privilegedPackages) {
-        return new TelephonySubscriptionSnapshot(subIdToGroupMap, privilegedPackages);
+        return new TelephonySubscriptionSnapshot(subIdToInfoMap, privilegedPackages);
     }
 
     private void verifyNoActiveSubscriptions() {
@@ -371,7 +371,7 @@
     @Test
     public void testTelephonySubscriptionSnapshotGetGroupForSubId() throws Exception {
         final TelephonySubscriptionSnapshot snapshot =
-                new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap());
+                new TelephonySubscriptionSnapshot(TEST_SUBID_TO_INFO_MAP, emptyMap());
 
         assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_1));
         assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_2));
@@ -380,7 +380,7 @@
     @Test
     public void testTelephonySubscriptionSnapshotGetAllSubIdsInGroup() throws Exception {
         final TelephonySubscriptionSnapshot snapshot =
-                new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap());
+                new TelephonySubscriptionSnapshot(TEST_SUBID_TO_INFO_MAP, emptyMap());
 
         assertEquals(
                 new ArraySet<>(Arrays.asList(TEST_SUBSCRIPTION_ID_1, TEST_SUBSCRIPTION_ID_2)),
diff --git a/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java b/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
index 0b72cd9..a36fd79 100644
--- a/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/UnderlyingNetworkTrackerTest.java
@@ -42,12 +42,14 @@
 import android.net.TelephonyNetworkSpecifier;
 import android.os.ParcelUuid;
 import android.os.test.TestLooper;
+import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionInfo;
+import android.telephony.TelephonyManager;
 import android.util.ArraySet;
 
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import com.android.server.vcn.UnderlyingNetworkTracker.NetworkBringupCallback;
-import com.android.server.vcn.UnderlyingNetworkTracker.RouteSelectionCallback;
+import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkListener;
 import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
 import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback;
 
@@ -98,11 +100,13 @@
     @Mock private Context mContext;
     @Mock private VcnNetworkProvider mVcnNetworkProvider;
     @Mock private ConnectivityManager mConnectivityManager;
+    @Mock private TelephonyManager mTelephonyManager;
+    @Mock private CarrierConfigManager mCarrierConfigManager;
     @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
     @Mock private UnderlyingNetworkTrackerCallback mNetworkTrackerCb;
     @Mock private Network mNetwork;
 
-    @Captor private ArgumentCaptor<RouteSelectionCallback> mRouteSelectionCallbackCaptor;
+    @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
 
     private TestLooper mTestLooper;
     private VcnContext mVcnContext;
@@ -127,6 +131,13 @@
                 mConnectivityManager,
                 Context.CONNECTIVITY_SERVICE,
                 ConnectivityManager.class);
+        setupSystemService(
+                mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+        setupSystemService(
+                mContext,
+                mCarrierConfigManager,
+                Context.CARRIER_CONFIG_SERVICE,
+                CarrierConfigManager.class);
 
         when(mSubscriptionSnapshot.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(INITIAL_SUB_IDS);
 
@@ -163,26 +174,26 @@
 
     @Test
     public void testNetworkCallbacksRegisteredOnStartupForTestMode() {
+        final ConnectivityManager cm = mock(ConnectivityManager.class);
+        setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
         final VcnContext vcnContext =
-                spy(
-                        new VcnContext(
-                                mContext,
-                                mTestLooper.getLooper(),
-                                mVcnNetworkProvider,
-                                true /* isInTestMode */));
+                new VcnContext(
+                        mContext,
+                        mTestLooper.getLooper(),
+                        mVcnNetworkProvider,
+                        true /* isInTestMode */);
 
-        mUnderlyingNetworkTracker =
-                new UnderlyingNetworkTracker(
-                        vcnContext,
-                        SUB_GROUP,
-                        mSubscriptionSnapshot,
-                        Collections.singleton(NetworkCapabilities.NET_CAPABILITY_INTERNET),
-                        mNetworkTrackerCb);
+        new UnderlyingNetworkTracker(
+                vcnContext,
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                Collections.singleton(NetworkCapabilities.NET_CAPABILITY_INTERNET),
+                mNetworkTrackerCb);
 
-        verify(mConnectivityManager)
-                .requestBackgroundNetwork(
+        verify(cm)
+                .registerNetworkCallback(
                         eq(getTestNetworkRequest(INITIAL_SUB_IDS)),
-                        any(RouteSelectionCallback.class),
+                        any(UnderlyingNetworkListener.class),
                         any());
     }
 
@@ -200,9 +211,19 @@
         }
 
         verify(mConnectivityManager)
-                .requestBackgroundNetwork(
+                .registerNetworkCallback(
                         eq(getRouteSelectionRequest(expectedSubIds)),
-                        any(RouteSelectionCallback.class),
+                        any(UnderlyingNetworkListener.class),
+                        any());
+        verify(mConnectivityManager)
+                .registerNetworkCallback(
+                        eq(getWifiEntryRssiThresholdRequest(expectedSubIds)),
+                        any(NetworkBringupCallback.class),
+                        any());
+        verify(mConnectivityManager)
+                .registerNetworkCallback(
+                        eq(getWifiExitRssiThresholdRequest(expectedSubIds)),
+                        any(NetworkBringupCallback.class),
                         any());
     }
 
@@ -218,9 +239,10 @@
         mUnderlyingNetworkTracker.updateSubscriptionSnapshot(subscriptionUpdate);
 
         // verify that initially-filed bringup requests are unregistered (cell + wifi)
-        verify(mConnectivityManager, times(INITIAL_SUB_IDS.size() + 1))
+        verify(mConnectivityManager, times(INITIAL_SUB_IDS.size() + 3))
                 .unregisterNetworkCallback(any(NetworkBringupCallback.class));
-        verify(mConnectivityManager).unregisterNetworkCallback(any(RouteSelectionCallback.class));
+        verify(mConnectivityManager)
+                .unregisterNetworkCallback(any(UnderlyingNetworkListener.class));
         verifyNetworkRequestsRegistered(UPDATED_SUB_IDS);
     }
 
@@ -231,6 +253,24 @@
                 .build();
     }
 
+    private NetworkRequest getWifiEntryRssiThresholdRequest(Set<Integer> netCapsSubIds) {
+        // TODO (b/187991063): Add tests for carrier-config based thresholds
+        return getExpectedRequestBase()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setSubscriptionIds(netCapsSubIds)
+                .setSignalStrength(UnderlyingNetworkTracker.WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT)
+                .build();
+    }
+
+    private NetworkRequest getWifiExitRssiThresholdRequest(Set<Integer> netCapsSubIds) {
+        // TODO (b/187991063): Add tests for carrier-config based thresholds
+        return getExpectedRequestBase()
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                .setSubscriptionIds(netCapsSubIds)
+                .setSignalStrength(UnderlyingNetworkTracker.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT)
+                .build();
+    }
+
     private NetworkRequest getCellRequestForSubId(int subId) {
         return getExpectedRequestBase()
                 .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
@@ -239,7 +279,11 @@
     }
 
     private NetworkRequest getRouteSelectionRequest(Set<Integer> netCapsSubIds) {
-        return getExpectedRequestBase().setSubscriptionIds(netCapsSubIds).build();
+        return getExpectedRequestBase()
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
+                .setSubscriptionIds(netCapsSubIds)
+                .build();
     }
 
     private NetworkRequest getTestNetworkRequest(Set<Integer> netCapsSubIds) {
@@ -265,11 +309,12 @@
     public void testTeardown() {
         mUnderlyingNetworkTracker.teardown();
 
-        // Expect 3 NetworkBringupCallbacks to be unregistered: 1 for WiFi and 2 for Cellular (1x
-        // for each subId)
-        verify(mConnectivityManager, times(3))
+        // Expect 5 NetworkBringupCallbacks to be unregistered: 1 for WiFi, 2 for Cellular (1x for
+        // each subId), and 1 for each of the Wifi signal strength thresholds
+        verify(mConnectivityManager, times(5))
                 .unregisterNetworkCallback(any(NetworkBringupCallback.class));
-        verify(mConnectivityManager).unregisterNetworkCallback(any(RouteSelectionCallback.class));
+        verify(mConnectivityManager)
+                .unregisterNetworkCallback(any(UnderlyingNetworkListener.class));
     }
 
     @Test
@@ -302,19 +347,19 @@
         verifyRegistrationOnAvailableAndGetCallback();
     }
 
-    private RouteSelectionCallback verifyRegistrationOnAvailableAndGetCallback() {
+    private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() {
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
 
-    private RouteSelectionCallback verifyRegistrationOnAvailableAndGetCallback(
+    private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback(
             NetworkCapabilities networkCapabilities) {
         verify(mConnectivityManager)
-                .requestBackgroundNetwork(
+                .registerNetworkCallback(
                         eq(getRouteSelectionRequest(INITIAL_SUB_IDS)),
-                        mRouteSelectionCallbackCaptor.capture(),
+                        mUnderlyingNetworkListenerCaptor.capture(),
                         any());
 
-        RouteSelectionCallback cb = mRouteSelectionCallbackCaptor.getValue();
+        UnderlyingNetworkListener cb = mUnderlyingNetworkListenerCaptor.getValue();
         cb.onAvailable(mNetwork);
         cb.onCapabilitiesChanged(mNetwork, networkCapabilities);
         cb.onLinkPropertiesChanged(mNetwork, INITIAL_LINK_PROPERTIES);
@@ -332,7 +377,7 @@
 
     @Test
     public void testRecordTrackerCallbackNotifiedForNetworkCapabilitiesChange() {
-        RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
+        UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
 
@@ -347,7 +392,7 @@
 
     @Test
     public void testRecordTrackerCallbackNotifiedForLinkPropertiesChange() {
-        RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
+        UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onLinkPropertiesChanged(mNetwork, UPDATED_LINK_PROPERTIES);
 
@@ -362,7 +407,7 @@
 
     @Test
     public void testRecordTrackerCallbackNotifiedForNetworkSuspended() {
-        RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
+        UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onCapabilitiesChanged(mNetwork, SUSPENDED_NETWORK_CAPABILITIES);
 
@@ -381,7 +426,7 @@
 
     @Test
     public void testRecordTrackerCallbackNotifiedForNetworkResumed() {
-        RouteSelectionCallback cb =
+        UnderlyingNetworkListener cb =
                 verifyRegistrationOnAvailableAndGetCallback(SUSPENDED_NETWORK_CAPABILITIES);
 
         cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
@@ -401,7 +446,7 @@
 
     @Test
     public void testRecordTrackerCallbackNotifiedForBlocked() {
-        RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
+        UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */);
 
@@ -416,7 +461,7 @@
 
     @Test
     public void testRecordTrackerCallbackNotifiedForNetworkLoss() {
-        RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
+        UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onLost(mNetwork);
 
@@ -425,7 +470,7 @@
 
     @Test
     public void testRecordTrackerCallbackIgnoresDuplicateRecord() {
-        RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
+        UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
         cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
 
@@ -433,4 +478,6 @@
         // UnderlyingNetworkRecord does not actually change
         verifyNoMoreInteractions(mNetworkTrackerCb);
     }
+
+    // TODO (b/187991063): Add tests for network prioritization
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 1ecb4c9..c747bc0 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -54,6 +54,7 @@
 import android.os.ParcelUuid;
 import android.os.PowerManager;
 import android.os.test.TestLooper;
+import android.telephony.SubscriptionInfo;
 
 import com.android.internal.util.State;
 import com.android.internal.util.WakeupMessage;
@@ -73,6 +74,12 @@
 
 public class VcnGatewayConnectionTestBase {
     protected static final ParcelUuid TEST_SUB_GRP = new ParcelUuid(UUID.randomUUID());
+    protected static final SubscriptionInfo TEST_SUB_INFO = mock(SubscriptionInfo.class);
+
+    static {
+        doReturn(TEST_SUB_GRP).when(TEST_SUB_INFO).getGroupUuid();
+    }
+
     protected static final InetAddress TEST_DNS_ADDR =
             InetAddresses.parseNumericAddress("2001:DB8:0:1::");
     protected static final InetAddress TEST_DNS_ADDR_2 =
@@ -116,7 +123,7 @@
 
     protected static final TelephonySubscriptionSnapshot TEST_SUBSCRIPTION_SNAPSHOT =
             new TelephonySubscriptionSnapshot(
-                    Collections.singletonMap(TEST_SUB_ID, TEST_SUB_GRP), Collections.EMPTY_MAP);
+                    Collections.singletonMap(TEST_SUB_ID, TEST_SUB_INFO), Collections.EMPTY_MAP);
 
     @NonNull protected final Context mContext;
     @NonNull protected final TestLooper mTestLooper;