Merge "Calculate priority based on caller configurations" am: 36a25527b4

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1906130

Change-Id: I3817a344247104eb4056e949d09a123662fb6b65
diff --git a/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java b/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java
index 6b33e4f..50a6bfc 100644
--- a/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java
+++ b/core/java/android/net/vcn/VcnCellUnderlyingNetworkPriority.java
@@ -29,6 +29,7 @@
 import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.util.PersistableBundleUtils;
 
 import java.util.ArrayList;
@@ -200,6 +201,15 @@
                 && mRequireOpportunistic == rhs.mRequireOpportunistic;
     }
 
+    /** @hide */
+    @Override
+    void dumpTransportSpecificFields(IndentingPrintWriter pw) {
+        pw.println("mAllowedNetworkPlmnIds: " + mAllowedNetworkPlmnIds.toString());
+        pw.println("mAllowedSpecificCarrierIds: " + mAllowedSpecificCarrierIds.toString());
+        pw.println("mAllowRoaming: " + mAllowRoaming);
+        pw.println("mRequireOpportunistic: " + mRequireOpportunistic);
+    }
+
     /** This class is used to incrementally build WifiNetworkPriority objects. */
     public static class Builder extends VcnUnderlyingNetworkPriority.Builder<Builder> {
         @NonNull private final Set<String> mAllowedNetworkPlmnIds = new ArraySet<>();
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index de4ada2..31e38c0 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -160,7 +160,9 @@
                 TimeUnit.MINUTES.toMillis(15)
             };
 
-    private static final LinkedHashSet<VcnUnderlyingNetworkPriority>
+    /** @hide */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static final LinkedHashSet<VcnUnderlyingNetworkPriority>
             DEFAULT_UNDERLYING_NETWORK_PRIORITIES = new LinkedHashSet<>();
 
     static {
diff --git a/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java b/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java
index 82f6ae7..551f757 100644
--- a/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java
+++ b/core/java/android/net/vcn/VcnUnderlyingNetworkPriority.java
@@ -21,8 +21,10 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.PersistableBundle;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -37,10 +39,17 @@
     /** @hide */
     protected static final int NETWORK_PRIORITY_TYPE_CELL = 2;
 
-    /** Denotes that network quality needs to be OK */
-    public static final int NETWORK_QUALITY_OK = 10000;
     /** Denotes that any network quality is acceptable */
-    public static final int NETWORK_QUALITY_ANY = Integer.MAX_VALUE;
+    public static final int NETWORK_QUALITY_ANY = 0;
+    /** Denotes that network quality needs to be OK */
+    public static final int NETWORK_QUALITY_OK = 100000;
+
+    private static final SparseArray<String> NETWORK_QUALITY_TO_STRING_MAP = new SparseArray<>();
+
+    static {
+        NETWORK_QUALITY_TO_STRING_MAP.put(NETWORK_QUALITY_ANY, "NETWORK_QUALITY_ANY");
+        NETWORK_QUALITY_TO_STRING_MAP.put(NETWORK_QUALITY_OK, "NETWORK_QUALITY_OK");
+    }
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -125,6 +134,28 @@
                 && mAllowMetered == rhs.mAllowMetered;
     }
 
+    /** @hide */
+    abstract void dumpTransportSpecificFields(IndentingPrintWriter pw);
+
+    /**
+     * Dumps the state of this record for logging and debugging purposes.
+     *
+     * @hide
+     */
+    public void dump(IndentingPrintWriter pw) {
+        pw.println(this.getClass().getSimpleName() + ":");
+        pw.increaseIndent();
+
+        pw.println(
+                "mNetworkQuality: "
+                        + NETWORK_QUALITY_TO_STRING_MAP.get(
+                                mNetworkQuality, "Invalid value " + mNetworkQuality));
+        pw.println("mAllowMetered: " + mAllowMetered);
+        dumpTransportSpecificFields(pw);
+
+        pw.decreaseIndent();
+    }
+
     /** Retrieve the required network quality. */
     @NetworkQuality
     public int getNetworkQuality() {
diff --git a/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java b/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java
index fc7e7e2..2ba9169 100644
--- a/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java
+++ b/core/java/android/net/vcn/VcnWifiUnderlyingNetworkPriority.java
@@ -22,6 +22,7 @@
 import android.os.PersistableBundle;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.Objects;
 
@@ -81,6 +82,12 @@
         return mSsid == rhs.mSsid;
     }
 
+    /** @hide */
+    @Override
+    void dumpTransportSpecificFields(IndentingPrintWriter pw) {
+        pw.println("mSsid: " + mSsid);
+    }
+
     /** Retrieve the required SSID, or {@code null} if there is no requirement on SSID. */
     @Nullable
     public String getSsid() {
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 584530c..8b80b4a 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -687,6 +687,7 @@
         mUnderlyingNetworkController =
                 mDeps.newUnderlyingNetworkController(
                         mVcnContext,
+                        mConnectionConfig,
                         subscriptionGroup,
                         mLastSnapshot,
                         mUnderlyingNetworkControllerCallback);
@@ -2376,11 +2377,12 @@
         /** Builds a new UnderlyingNetworkController. */
         public UnderlyingNetworkController newUnderlyingNetworkController(
                 VcnContext vcnContext,
+                VcnGatewayConnectionConfig connectionConfig,
                 ParcelUuid subscriptionGroup,
                 TelephonySubscriptionSnapshot snapshot,
                 UnderlyingNetworkControllerCallback callback) {
             return new UnderlyingNetworkController(
-                    vcnContext, subscriptionGroup, snapshot, callback);
+                    vcnContext, connectionConfig, subscriptionGroup, snapshot, callback);
         }
 
         /** Builds a new IkeSession. */
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
index bea8ae9..7b26fe0 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -15,25 +15,36 @@
  */
 package com.android.server.vcn.routeselection;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_ANY;
+import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_OK;
 
 import static com.android.server.VcnManagementService.LOCAL_LOG;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnCellUnderlyingNetworkPriority;
 import android.net.vcn.VcnManager;
+import android.net.vcn.VcnUnderlyingNetworkPriority;
+import android.net.vcn.VcnWifiUnderlyingNetworkPriority;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
 import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
 
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 /** @hide */
@@ -56,52 +67,20 @@
      */
     @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;
 
-    private static final SparseArray<String> PRIORITY_TO_STRING_MAP = new SparseArray<>();
-
-    static {
-        PRIORITY_TO_STRING_MAP.put(
-                PRIORITY_OPPORTUNISTIC_CELLULAR, "PRIORITY_OPPORTUNISTIC_CELLULAR");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_WIFI_IN_USE, "PRIORITY_WIFI_IN_USE");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_WIFI_PROSPECTIVE, "PRIORITY_WIFI_PROSPECTIVE");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_MACRO_CELLULAR, "PRIORITY_MACRO_CELLULAR");
-        PRIORITY_TO_STRING_MAP.put(PRIORITY_ANY, "PRIORITY_ANY");
-    }
-
-    /**
-     * 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>
-     */
-    static int calculatePriorityClass(
+    /** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */
+    public static int calculatePriorityClass(
+            VcnContext vcnContext,
             UnderlyingNetworkRecord networkRecord,
+            LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
             PersistableBundle carrierConfig) {
-        final NetworkCapabilities caps = networkRecord.networkCapabilities;
-
         // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
 
         if (networkRecord.isBlocked) {
@@ -109,8 +88,167 @@
             return PRIORITY_ANY;
         }
 
-        if (caps.hasTransport(TRANSPORT_CELLULAR)
-                && isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+        if (snapshot == null) {
+            logWtf("Got null snapshot");
+            return PRIORITY_ANY;
+        }
+
+        int priorityIndex = 0;
+        for (VcnUnderlyingNetworkPriority nwPriority : underlyingNetworkPriorities) {
+            if (checkMatchesPriorityRule(
+                    vcnContext,
+                    nwPriority,
+                    networkRecord,
+                    subscriptionGroup,
+                    snapshot,
+                    currentlySelected,
+                    carrierConfig)) {
+                return priorityIndex;
+            }
+            priorityIndex++;
+        }
+        return PRIORITY_ANY;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean checkMatchesPriorityRule(
+            VcnContext vcnContext,
+            VcnUnderlyingNetworkPriority networkPriority,
+            UnderlyingNetworkRecord networkRecord,
+            ParcelUuid subscriptionGroup,
+            TelephonySubscriptionSnapshot snapshot,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        // TODO: Check Network Quality reported by metric monitors/probers.
+
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+        if (!networkPriority.allowMetered() && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)) {
+            return false;
+        }
+
+        if (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST)) {
+            return true;
+        }
+
+        if (networkPriority instanceof VcnWifiUnderlyingNetworkPriority) {
+            return checkMatchesWifiPriorityRule(
+                    (VcnWifiUnderlyingNetworkPriority) networkPriority,
+                    networkRecord,
+                    currentlySelected,
+                    carrierConfig);
+        }
+
+        if (networkPriority instanceof VcnCellUnderlyingNetworkPriority) {
+            return checkMatchesCellPriorityRule(
+                    vcnContext,
+                    (VcnCellUnderlyingNetworkPriority) networkPriority,
+                    networkRecord,
+                    subscriptionGroup,
+                    snapshot);
+        }
+
+        logWtf(
+                "Got unknown VcnUnderlyingNetworkPriority class: "
+                        + networkPriority.getClass().getSimpleName());
+        return false;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean checkMatchesWifiPriorityRule(
+            VcnWifiUnderlyingNetworkPriority networkPriority,
+            UnderlyingNetworkRecord networkRecord,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+
+        if (!caps.hasTransport(TRANSPORT_WIFI)) {
+            return false;
+        }
+
+        // TODO: Move the Network Quality check to the network metric monitor framework.
+        if (networkPriority.getNetworkQuality()
+                > getWifiQuality(networkRecord, currentlySelected, carrierConfig)) {
+            return false;
+        }
+
+        if (networkPriority.getSsid() != null && networkPriority.getSsid() != caps.getSsid()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private static int getWifiQuality(
+            UnderlyingNetworkRecord networkRecord,
+            UnderlyingNetworkRecord currentlySelected,
+            PersistableBundle carrierConfig) {
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+        final boolean isSelectedNetwork =
+                currentlySelected != null
+                        && networkRecord.network.equals(currentlySelected.network);
+
+        if (isSelectedNetwork
+                && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
+            return NETWORK_QUALITY_OK;
+        }
+
+        if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) {
+            return NETWORK_QUALITY_OK;
+        }
+
+        return NETWORK_QUALITY_ANY;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean checkMatchesCellPriorityRule(
+            VcnContext vcnContext,
+            VcnCellUnderlyingNetworkPriority networkPriority,
+            UnderlyingNetworkRecord networkRecord,
+            ParcelUuid subscriptionGroup,
+            TelephonySubscriptionSnapshot snapshot) {
+        final NetworkCapabilities caps = networkRecord.networkCapabilities;
+
+        if (!caps.hasTransport(TRANSPORT_CELLULAR)) {
+            return false;
+        }
+
+        final TelephonyNetworkSpecifier telephonyNetworkSpecifier =
+                ((TelephonyNetworkSpecifier) caps.getNetworkSpecifier());
+        if (telephonyNetworkSpecifier == null) {
+            logWtf("Got null NetworkSpecifier");
+            return false;
+        }
+
+        final int subId = telephonyNetworkSpecifier.getSubscriptionId();
+        final TelephonyManager subIdSpecificTelephonyMgr =
+                vcnContext
+                        .getContext()
+                        .getSystemService(TelephonyManager.class)
+                        .createForSubscriptionId(subId);
+
+        if (!networkPriority.getAllowedPlmnIds().isEmpty()) {
+            final String plmnId = subIdSpecificTelephonyMgr.getNetworkOperator();
+            if (!networkPriority.getAllowedPlmnIds().contains(plmnId)) {
+                return false;
+            }
+        }
+
+        if (!networkPriority.getAllowedSpecificCarrierIds().isEmpty()) {
+            final int carrierId = subIdSpecificTelephonyMgr.getSimSpecificCarrierId();
+            if (!networkPriority.getAllowedSpecificCarrierIds().contains(carrierId)) {
+                return false;
+            }
+        }
+
+        if (!networkPriority.allowRoaming() && !caps.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
+            return false;
+        }
+
+        if (networkPriority.requireOpportunistic()) {
+            if (!isOpportunistic(snapshot, caps.getSubscriptionIds())) {
+                return false;
+            }
+
             // 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,
@@ -121,36 +259,15 @@
             // 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)
+            if (snapshot.getAllSubIdsInGroup(subscriptionGroup)
                             .contains(SubscriptionManager.getActiveDataSubscriptionId())
-                    || caps.getSubscriptionIds()
+                    && !caps.getSubscriptionIds()
                             .contains(SubscriptionManager.getActiveDataSubscriptionId())) {
-                return PRIORITY_OPPORTUNISTIC_CELLULAR;
+                return false;
             }
         }
 
-        if (caps.hasTransport(TRANSPORT_WIFI)) {
-            if (caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)
-                    && currentlySelected != null
-                    && networkRecord.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;
+        return true;
     }
 
     static boolean isOpportunistic(
@@ -185,10 +302,6 @@
         return WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
     }
 
-    static String priorityClassToString(int priorityClass) {
-        return PRIORITY_TO_STRING_MAP.get(priorityClass, "unknown");
-    }
-
     private static void logWtf(String msg) {
         Slog.wtf(TAG, msg);
         LOCAL_LOG.log(TAG + " WTF: " + msg);
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 071c7a6..cd124ee 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -32,6 +32,8 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnGatewayConnectionConfig;
+import android.net.vcn.VcnUnderlyingNetworkPriority;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
@@ -68,6 +70,7 @@
     @NonNull private static final String TAG = UnderlyingNetworkController.class.getSimpleName();
 
     @NonNull private final VcnContext mVcnContext;
+    @NonNull private final VcnGatewayConnectionConfig mConnectionConfig;
     @NonNull private final ParcelUuid mSubscriptionGroup;
     @NonNull private final UnderlyingNetworkControllerCallback mCb;
     @NonNull private final Dependencies mDeps;
@@ -91,24 +94,22 @@
 
     public UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
+            @NonNull VcnGatewayConnectionConfig connectionConfig,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot snapshot,
             @NonNull UnderlyingNetworkControllerCallback cb) {
-        this(
-                vcnContext,
-                subscriptionGroup,
-                snapshot,
-                cb,
-                new Dependencies());
+        this(vcnContext, connectionConfig, subscriptionGroup, snapshot, cb, new Dependencies());
     }
 
     private UnderlyingNetworkController(
             @NonNull VcnContext vcnContext,
+            @NonNull VcnGatewayConnectionConfig connectionConfig,
             @NonNull ParcelUuid subscriptionGroup,
             @NonNull TelephonySubscriptionSnapshot snapshot,
             @NonNull UnderlyingNetworkControllerCallback cb,
             @NonNull Dependencies deps) {
         mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
+        mConnectionConfig = Objects.requireNonNull(connectionConfig, "Missing connectionConfig");
         mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
         mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot");
         mCb = Objects.requireNonNull(cb, "Missing cb");
@@ -399,6 +400,8 @@
             TreeSet<UnderlyingNetworkRecord> sorted =
                     new TreeSet<>(
                             UnderlyingNetworkRecord.getComparator(
+                                    mVcnContext,
+                                    mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
                                     mSubscriptionGroup,
                                     mLastSnapshot,
                                     mCurrentRecord,
@@ -495,12 +498,31 @@
         pw.println(
                 "Currently selected: " + (mCurrentRecord == null ? null : mCurrentRecord.network));
 
+        pw.println("VcnUnderlyingNetworkPriority list:");
+        pw.increaseIndent();
+        int index = 0;
+        for (VcnUnderlyingNetworkPriority priority :
+                mConnectionConfig.getVcnUnderlyingNetworkPriorities()) {
+            pw.println("Priority index: " + index);
+            priority.dump(pw);
+            index++;
+        }
+        pw.decreaseIndent();
+        pw.println();
+
         pw.println("Underlying networks:");
         pw.increaseIndent();
         if (mRouteSelectionCallback != null) {
             for (UnderlyingNetworkRecord record :
                     mRouteSelectionCallback.getSortedUnderlyingNetworks()) {
-                record.dump(pw, mSubscriptionGroup, mLastSnapshot, mCurrentRecord, mCarrierConfig);
+                record.dump(
+                        mVcnContext,
+                        pw,
+                        mConnectionConfig.getVcnUnderlyingNetworkPriorities(),
+                        mSubscriptionGroup,
+                        mLastSnapshot,
+                        mCurrentRecord,
+                        mCarrierConfig);
             }
         }
         pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
index 65c69de..27ba854 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -21,6 +21,7 @@
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.vcn.VcnUnderlyingNetworkPriority;
 import android.os.ParcelUuid;
 import android.os.PersistableBundle;
 
@@ -28,8 +29,10 @@
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
 
 import java.util.Comparator;
+import java.util.LinkedHashSet;
 import java.util.Objects;
 
 /**
@@ -73,22 +76,64 @@
     }
 
     static Comparator<UnderlyingNetworkRecord> getComparator(
+            VcnContext vcnContext,
+            LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
             PersistableBundle carrierConfig) {
         return (left, right) -> {
-            return Integer.compare(
+            final int leftIndex =
                     NetworkPriorityClassifier.calculatePriorityClass(
-                            left, subscriptionGroup, snapshot, currentlySelected, carrierConfig),
+                            vcnContext,
+                            left,
+                            underlyingNetworkPriorities,
+                            subscriptionGroup,
+                            snapshot,
+                            currentlySelected,
+                            carrierConfig);
+            final int rightIndex =
                     NetworkPriorityClassifier.calculatePriorityClass(
-                            right, subscriptionGroup, snapshot, currentlySelected, carrierConfig));
+                            vcnContext,
+                            right,
+                            underlyingNetworkPriorities,
+                            subscriptionGroup,
+                            snapshot,
+                            currentlySelected,
+                            carrierConfig);
+
+            // In the case of networks in the same priority class, prioritize based on other
+            // criteria (eg. actively selected network, link metrics, etc)
+            if (leftIndex == rightIndex) {
+                // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord
+                // fall into the same priority class.
+                if (isSelected(left, currentlySelected)) {
+                    return -1;
+                }
+                if (isSelected(left, currentlySelected)) {
+                    return 1;
+                }
+            }
+            return Integer.compare(leftIndex, rightIndex);
         };
     }
 
+    private static boolean isSelected(
+            UnderlyingNetworkRecord recordToCheck, UnderlyingNetworkRecord currentlySelected) {
+        if (currentlySelected == null) {
+            return false;
+        }
+        if (currentlySelected.network == recordToCheck.network) {
+            return true;
+        }
+        return false;
+    }
+
     /** Dumps the state of this record for logging and debugging purposes. */
     void dump(
+            VcnContext vcnContext,
             IndentingPrintWriter pw,
+            LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
@@ -96,15 +141,17 @@
         pw.println("UnderlyingNetworkRecord:");
         pw.increaseIndent();
 
-        final int priorityClass =
+        final int priorityIndex =
                 NetworkPriorityClassifier.calculatePriorityClass(
-                        this, subscriptionGroup, snapshot, currentlySelected, carrierConfig);
-        pw.println(
-                "Priority class: "
-                        + NetworkPriorityClassifier.priorityClassToString(priorityClass)
-                        + " ("
-                        + priorityClass
-                        + ")");
+                        vcnContext,
+                        this,
+                        underlyingNetworkPriorities,
+                        subscriptionGroup,
+                        snapshot,
+                        currentlySelected,
+                        carrierConfig);
+
+        pw.println("Priority index:" + priorityIndex);
         pw.println("mNetwork: " + network);
         pw.println("mNetworkCapabilities: " + networkCapabilities);
         pw.println("mLinkProperties: " + linkProperties);
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 8a0af2d..5628321 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -179,7 +179,7 @@
 
         doReturn(mUnderlyingNetworkController)
                 .when(mDeps)
-                .newUnderlyingNetworkController(any(), any(), any(), any());
+                .newUnderlyingNetworkController(any(), any(), any(), any(), any());
         doReturn(mWakeLock)
                 .when(mDeps)
                 .newWakeLock(eq(mContext), eq(PowerManager.PARTIAL_WAKE_LOCK), any());
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
new file mode 100644
index 0000000..2e1aab6
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.vcn.routeselection;
+
+import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_OK;
+
+import static com.android.server.vcn.VcnTestUtils.setupSystemService;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_ANY;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.calculatePriorityClass;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesCellPriorityRule;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesPriorityRule;
+import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.checkMatchesWifiPriorityRule;
+import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnCellUnderlyingNetworkPriority;
+import android.net.vcn.VcnGatewayConnectionConfig;
+import android.net.vcn.VcnManager;
+import android.net.vcn.VcnWifiUnderlyingNetworkPriority;
+import android.os.ParcelUuid;
+import android.os.PersistableBundle;
+import android.os.test.TestLooper;
+import android.telephony.TelephonyManager;
+import android.util.ArraySet;
+
+import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
+import com.android.server.vcn.VcnContext;
+import com.android.server.vcn.VcnNetworkProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Set;
+import java.util.UUID;
+
+public class NetworkPriorityClassifierTest {
+    private static final String SSID = "TestWifi";
+    private static final String SSID_OTHER = "TestWifiOther";
+    private static final String PLMN_ID = "123456";
+    private static final String PLMN_ID_OTHER = "234567";
+
+    private static final int SUB_ID = 1;
+    private static final int WIFI_RSSI = -60;
+    private static final int WIFI_RSSI_HIGH = -50;
+    private static final int WIFI_RSSI_LOW = -80;
+    private static final int CARRIER_ID = 1;
+    private static final int CARRIER_ID_OTHER = 2;
+
+    private static final ParcelUuid SUB_GROUP = new ParcelUuid(new UUID(0, 0));
+
+    private static final NetworkCapabilities WIFI_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .setSignalStrength(WIFI_RSSI)
+                    .setSsid(SSID)
+                    .build();
+
+    private static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
+            new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
+    private static final NetworkCapabilities CELL_NETWORK_CAPABILITIES =
+            new NetworkCapabilities.Builder()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                    .setSubscriptionIds(Set.of(SUB_ID))
+                    .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
+                    .build();
+
+    private static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
+
+    @Mock private Network mNetwork;
+    @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
+    @Mock private TelephonyManager mTelephonyManager;
+
+    private TestLooper mTestLooper;
+    private VcnContext mVcnContext;
+    private UnderlyingNetworkRecord mWifiNetworkRecord;
+    private UnderlyingNetworkRecord mCellNetworkRecord;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        final Context mockContext = mock(Context.class);
+        mTestLooper = new TestLooper();
+        mVcnContext =
+                spy(
+                        new VcnContext(
+                                mockContext,
+                                mTestLooper.getLooper(),
+                                mock(VcnNetworkProvider.class),
+                                false /* isInTestMode */));
+        doNothing().when(mVcnContext).ensureRunningOnLooperThread();
+
+        mWifiNetworkRecord =
+                new UnderlyingNetworkRecord(
+                        mNetwork,
+                        WIFI_NETWORK_CAPABILITIES,
+                        LINK_PROPERTIES,
+                        false /* isBlocked */);
+
+        mCellNetworkRecord =
+                new UnderlyingNetworkRecord(
+                        mNetwork,
+                        CELL_NETWORK_CAPABILITIES,
+                        LINK_PROPERTIES,
+                        false /* isBlocked */);
+
+        setupSystemService(
+                mockContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
+        when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
+        when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
+        when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
+    }
+
+    @Test
+    public void testMatchWithoutNotMeteredBit() {
+        final VcnWifiUnderlyingNetworkPriority wifiNetworkPriority =
+                new VcnWifiUnderlyingNetworkPriority.Builder()
+                        .setNetworkQuality(NETWORK_QUALITY_OK)
+                        .setAllowMetered(false /* allowMetered */)
+                        .build();
+
+        assertFalse(
+                checkMatchesPriorityRule(
+                        mVcnContext,
+                        wifiNetworkPriority,
+                        mWifiNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        null /* currentlySelecetd */,
+                        null /* carrierConfig */));
+    }
+
+    private void verifyMatchWifi(
+            boolean isSelectedNetwork, PersistableBundle carrierConfig, boolean expectMatch) {
+        final VcnWifiUnderlyingNetworkPriority wifiNetworkPriority =
+                new VcnWifiUnderlyingNetworkPriority.Builder()
+                        .setNetworkQuality(NETWORK_QUALITY_OK)
+                        .setAllowMetered(true /* allowMetered */)
+                        .build();
+        final UnderlyingNetworkRecord selectedNetworkRecord =
+                isSelectedNetwork ? mWifiNetworkRecord : null;
+        assertEquals(
+                expectMatch,
+                checkMatchesWifiPriorityRule(
+                        wifiNetworkPriority,
+                        mWifiNetworkRecord,
+                        selectedNetworkRecord,
+                        carrierConfig));
+    }
+
+    @Test
+    public void testMatchSelectedWifi() {
+        verifyMatchWifi(
+                true /* isSelectedNetwork */, null /* carrierConfig */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchSelectedWifiBelowRssiThreshold() {
+        final PersistableBundle carrierConfig = new PersistableBundle();
+        carrierConfig.putInt(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY, WIFI_RSSI_HIGH);
+        carrierConfig.putInt(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, WIFI_RSSI_HIGH);
+
+        verifyMatchWifi(true /* isSelectedNetwork */, carrierConfig, false /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchUnselectedWifi() {
+        verifyMatchWifi(
+                false /* isSelectedNetwork */, null /* carrierConfig */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchUnselectedWifiBelowRssiThreshold() {
+        final PersistableBundle carrierConfig = new PersistableBundle();
+        carrierConfig.putInt(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, WIFI_RSSI_HIGH);
+
+        verifyMatchWifi(false /* isSelectedNetwork */, carrierConfig, false /* expectMatch */);
+    }
+
+    private void verifyMatchWifiWithSsid(boolean useMatchedSsid, boolean expectMatch) {
+        final String nwPrioritySsid = useMatchedSsid ? SSID : SSID_OTHER;
+        final VcnWifiUnderlyingNetworkPriority wifiNetworkPriority =
+                new VcnWifiUnderlyingNetworkPriority.Builder()
+                        .setNetworkQuality(NETWORK_QUALITY_OK)
+                        .setAllowMetered(true /* allowMetered */)
+                        .setSsid(nwPrioritySsid)
+                        .build();
+
+        assertEquals(
+                expectMatch,
+                checkMatchesWifiPriorityRule(
+                        wifiNetworkPriority,
+                        mWifiNetworkRecord,
+                        null /* currentlySelecetd */,
+                        null /* carrierConfig */));
+    }
+
+    @Test
+    public void testMatchWifiWithSsid() {
+        verifyMatchWifiWithSsid(true /* useMatchedSsid */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchWifiFailWithWrongSsid() {
+        verifyMatchWifiWithSsid(false /* useMatchedSsid */, false /* expectMatch */);
+    }
+
+    private static VcnCellUnderlyingNetworkPriority.Builder getCellNetworkPriorityBuilder() {
+        return new VcnCellUnderlyingNetworkPriority.Builder()
+                .setNetworkQuality(NETWORK_QUALITY_OK)
+                .setAllowMetered(true /* allowMetered */)
+                .setAllowRoaming(true /* allowRoaming */);
+    }
+
+    @Test
+    public void testMatchMacroCell() {
+        assertTrue(
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        getCellNetworkPriorityBuilder().build(),
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    @Test
+    public void testMatchOpportunisticCell() {
+        final VcnCellUnderlyingNetworkPriority opportunisticCellNetworkPriority =
+                getCellNetworkPriorityBuilder()
+                        .setRequireOpportunistic(true /* requireOpportunistic */)
+                        .build();
+
+        when(mSubscriptionSnapshot.isOpportunistic(SUB_ID)).thenReturn(true);
+        when(mSubscriptionSnapshot.getAllSubIdsInGroup(SUB_GROUP)).thenReturn(new ArraySet<>());
+
+        assertTrue(
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        opportunisticCellNetworkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    private void verifyMatchMacroCellWithAllowedPlmnIds(
+            boolean useMatchedPlmnId, boolean expectMatch) {
+        final String networkPriorityPlmnId = useMatchedPlmnId ? PLMN_ID : PLMN_ID_OTHER;
+        final VcnCellUnderlyingNetworkPriority networkPriority =
+                getCellNetworkPriorityBuilder()
+                        .setAllowedPlmnIds(Set.of(networkPriorityPlmnId))
+                        .build();
+
+        assertEquals(
+                expectMatch,
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        networkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    @Test
+    public void testMatchMacroCellWithAllowedPlmnIds() {
+        verifyMatchMacroCellWithAllowedPlmnIds(true /* useMatchedPlmnId */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchMacroCellFailWithDisallowedPlmnIds() {
+        verifyMatchMacroCellWithAllowedPlmnIds(
+                false /* useMatchedPlmnId */, false /* expectMatch */);
+    }
+
+    private void verifyMatchMacroCellWithAllowedSpecificCarrierIds(
+            boolean useMatchedCarrierId, boolean expectMatch) {
+        final int networkPriorityCarrierId = useMatchedCarrierId ? CARRIER_ID : CARRIER_ID_OTHER;
+        final VcnCellUnderlyingNetworkPriority networkPriority =
+                getCellNetworkPriorityBuilder()
+                        .setAllowedSpecificCarrierIds(Set.of(networkPriorityCarrierId))
+                        .build();
+
+        assertEquals(
+                expectMatch,
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        networkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    @Test
+    public void testMatchMacroCellWithAllowedSpecificCarrierIds() {
+        verifyMatchMacroCellWithAllowedSpecificCarrierIds(
+                true /* useMatchedCarrierId */, true /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchMacroCellFailWithDisallowedSpecificCarrierIds() {
+        verifyMatchMacroCellWithAllowedSpecificCarrierIds(
+                false /* useMatchedCarrierId */, false /* expectMatch */);
+    }
+
+    @Test
+    public void testMatchWifiFailWithoutNotRoamingBit() {
+        final VcnCellUnderlyingNetworkPriority networkPriority =
+                getCellNetworkPriorityBuilder().setAllowRoaming(false /* allowRoaming */).build();
+
+        assertFalse(
+                checkMatchesCellPriorityRule(
+                        mVcnContext,
+                        networkPriority,
+                        mCellNetworkRecord,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot));
+    }
+
+    private void verifyCalculatePriorityClass(
+            UnderlyingNetworkRecord networkRecord, int expectedIndex) {
+        final int priorityIndex =
+                calculatePriorityClass(
+                        mVcnContext,
+                        networkRecord,
+                        VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_PRIORITIES,
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        null /* currentlySelected */,
+                        null /* carrierConfig */);
+
+        assertEquals(expectedIndex, priorityIndex);
+    }
+
+    @Test
+    public void testCalculatePriorityClass() throws Exception {
+        verifyCalculatePriorityClass(mCellNetworkRecord, 2);
+    }
+
+    @Test
+    public void testCalculatePriorityClassFailToMatchAny() throws Exception {
+        final NetworkCapabilities nc =
+                new NetworkCapabilities.Builder()
+                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                        .setSignalStrength(WIFI_RSSI_LOW)
+                        .setSsid(SSID)
+                        .build();
+        final UnderlyingNetworkRecord wifiNetworkRecord =
+                new UnderlyingNetworkRecord(mNetwork, nc, LINK_PROPERTIES, false /* isBlocked */);
+
+        verifyCalculatePriorityClass(wifiNetworkRecord, PRIORITY_ANY);
+    }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index c954cb8..fad9669 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -42,6 +42,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
+import android.net.vcn.VcnGatewayConnectionConfigTest;
 import android.os.ParcelUuid;
 import android.os.test.TestLooper;
 import android.telephony.CarrierConfigManager;
@@ -145,7 +146,11 @@
 
         mUnderlyingNetworkController =
                 new UnderlyingNetworkController(
-                        mVcnContext, SUB_GROUP, mSubscriptionSnapshot, mNetworkControllerCb);
+                        mVcnContext,
+                        VcnGatewayConnectionConfigTest.buildTestConfig(),
+                        SUB_GROUP,
+                        mSubscriptionSnapshot,
+                        mNetworkControllerCb);
     }
 
     private void resetVcnContext() {
@@ -153,7 +158,8 @@
         doNothing().when(mVcnContext).ensureRunningOnLooperThread();
     }
 
-    private static LinkProperties getLinkPropertiesWithName(String iface) {
+    // Package private for use in NetworkPriorityClassifierTest
+    static LinkProperties getLinkPropertiesWithName(String iface) {
         LinkProperties linkProperties = new LinkProperties();
         linkProperties.setInterfaceName(iface);
         return linkProperties;
@@ -182,7 +188,11 @@
                         true /* isInTestMode */);
 
         new UnderlyingNetworkController(
-                vcnContext, SUB_GROUP, mSubscriptionSnapshot, mNetworkControllerCb);
+                vcnContext,
+                VcnGatewayConnectionConfigTest.buildTestConfig(),
+                SUB_GROUP,
+                mSubscriptionSnapshot,
+                mNetworkControllerCb);
 
         verify(cm)
                 .registerNetworkCallback(
@@ -345,6 +355,17 @@
         return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
     }
 
+    private static NetworkCapabilities buildResponseNwCaps(
+            NetworkCapabilities requestNetworkCaps, Set<Integer> netCapsSubIds) {
+        final TelephonyNetworkSpecifier telephonyNetworkSpecifier =
+                new TelephonyNetworkSpecifier.Builder()
+                        .setSubscriptionId(netCapsSubIds.iterator().next())
+                        .build();
+        return new NetworkCapabilities.Builder(requestNetworkCaps)
+                .setNetworkSpecifier(telephonyNetworkSpecifier)
+                .build();
+    }
+
     private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback(
             NetworkCapabilities networkCapabilities) {
         verify(mConnectivityManager)
@@ -355,14 +376,17 @@
 
         UnderlyingNetworkListener cb = mUnderlyingNetworkListenerCaptor.getValue();
         cb.onAvailable(mNetwork);
-        cb.onCapabilitiesChanged(mNetwork, networkCapabilities);
+
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(networkCapabilities, INITIAL_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
         cb.onLinkPropertiesChanged(mNetwork, INITIAL_LINK_PROPERTIES);
         cb.onBlockedStatusChanged(mNetwork, false /* isFalse */);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        networkCapabilities,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -373,12 +397,14 @@
     public void testRecordTrackerCallbackNotifiedForNetworkCapabilitiesChange() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
-        cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(UPDATED_NETWORK_CAPABILITIES, UPDATED_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        UPDATED_NETWORK_CAPABILITIES,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -393,7 +419,7 @@
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
+                        buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS),
                         UPDATED_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -403,19 +429,21 @@
     public void testRecordTrackerCallbackNotifiedForNetworkSuspended() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
-        cb.onCapabilitiesChanged(mNetwork, SUSPENDED_NETWORK_CAPABILITIES);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(SUSPENDED_NETWORK_CAPABILITIES, UPDATED_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        SUSPENDED_NETWORK_CAPABILITIES,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
         // onSelectedUnderlyingNetworkChanged() won't be fired twice if network capabilities doesn't
         // change.
-        cb.onCapabilitiesChanged(mNetwork, SUSPENDED_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
@@ -425,19 +453,21 @@
         UnderlyingNetworkListener cb =
                 verifyRegistrationOnAvailableAndGetCallback(SUSPENDED_NETWORK_CAPABILITIES);
 
-        cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
+        final NetworkCapabilities responseNetworkCaps =
+                buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
 
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
+                        responseNetworkCaps,
                         INITIAL_LINK_PROPERTIES,
                         false /* isBlocked */);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
         // onSelectedUnderlyingNetworkChanged() won't be fired twice if network capabilities doesn't
         // change.
-        cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(mNetwork, responseNetworkCaps);
         verify(mNetworkControllerCb, times(1))
                 .onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
     }
@@ -451,7 +481,7 @@
         UnderlyingNetworkRecord expectedRecord =
                 new UnderlyingNetworkRecord(
                         mNetwork,
-                        INITIAL_NETWORK_CAPABILITIES,
+                        buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS),
                         INITIAL_LINK_PROPERTIES,
                         true /* isBlocked */);
         verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(eq(expectedRecord));
@@ -470,7 +500,8 @@
     public void testRecordTrackerCallbackIgnoresDuplicateRecord() {
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
 
-        cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(
+                mNetwork, buildResponseNwCaps(INITIAL_NETWORK_CAPABILITIES, INITIAL_SUB_IDS));
 
         // Verify no more calls to the UnderlyingNetworkControllerCallback when the
         // UnderlyingNetworkRecord does not actually change
@@ -482,7 +513,8 @@
         UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
         mUnderlyingNetworkController.teardown();
 
-        cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
+        cb.onCapabilitiesChanged(
+                mNetwork, buildResponseNwCaps(UPDATED_NETWORK_CAPABILITIES, INITIAL_SUB_IDS));
 
         // Verify that the only call was during onAvailable()
         verify(mNetworkControllerCb, times(1)).onSelectedUnderlyingNetworkChanged(any());