Add CarrierConfig to TelephonySubscriptionSnapshot

This change ensures that carrier configs are saved as part of the
snapshot, allowing all entities in the VCN to query telephony
subscription information from the same source.

Test: atest FrameworksVcnTests
Change-Id: I31447d84fe98d6bd2cc78ead41746198728c400b
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index f1b110a..40e4083 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -104,6 +104,14 @@
 
     // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
 
+    /** List of Carrier Config options to extract from Carrier Config bundles. @hide */
+    @NonNull
+    public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS =
+            new String[] {
+                VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
+                VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY
+            };
+
     private static final Map<
                     VcnNetworkPolicyChangeListener, VcnUnderlyingNetworkPolicyListenerBinder>
             REGISTERED_POLICY_LISTENERS = new ConcurrentHashMap<>();
diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
index 89470ec..5c305c6 100644
--- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
+++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -29,6 +29,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.net.vcn.VcnManager;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
@@ -47,6 +48,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.vcn.util.PersistableBundleUtils;
+import com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -95,6 +98,10 @@
 
     // TODO (Android T+): Add ability to handle multiple subIds per slot.
     @NonNull private final Map<Integer, Integer> mReadySubIdsBySlotId = new HashMap<>();
+
+    @NonNull
+    private final Map<Integer, PersistableBundleWrapper> mSubIdToCarrierConfigMap = new HashMap<>();
+
     @NonNull private final OnSubscriptionsChangedListener mSubscriptionChangedListener;
 
     @NonNull
@@ -250,7 +257,10 @@
 
         final TelephonySubscriptionSnapshot newSnapshot =
                 new TelephonySubscriptionSnapshot(
-                        mDeps.getActiveDataSubscriptionId(), newSubIdToInfoMap, privilegedPackages);
+                        mDeps.getActiveDataSubscriptionId(),
+                        newSubIdToInfoMap,
+                        mSubIdToCarrierConfigMap,
+                        privilegedPackages);
 
         // If snapshot was meaningfully updated, fire the callback
         if (!newSnapshot.equals(mCurrentSnapshot)) {
@@ -311,47 +321,77 @@
         }
 
         if (SubscriptionManager.isValidSubscriptionId(subId)) {
-            final PersistableBundle carrierConfigs = mCarrierConfigManager.getConfigForSubId(subId);
-            if (mDeps.isConfigForIdentifiedCarrier(carrierConfigs)) {
+            final PersistableBundle carrierConfig = mCarrierConfigManager.getConfigForSubId(subId);
+            if (mDeps.isConfigForIdentifiedCarrier(carrierConfig)) {
                 mReadySubIdsBySlotId.put(slotId, subId);
+
+                final PersistableBundle minimized =
+                        PersistableBundleUtils.minimizeBundle(
+                                carrierConfig, VcnManager.VCN_RELATED_CARRIER_CONFIG_KEYS);
+                if (minimized != null) {
+                    mSubIdToCarrierConfigMap.put(subId, new PersistableBundleWrapper(minimized));
+                }
                 handleSubscriptionsChanged();
             }
         } else {
-            mReadySubIdsBySlotId.remove(slotId);
+            final Integer oldSubid = mReadySubIdsBySlotId.remove(slotId);
+            if (oldSubid != null) {
+                mSubIdToCarrierConfigMap.remove(oldSubid);
+            }
             handleSubscriptionsChanged();
         }
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     void setReadySubIdsBySlotId(Map<Integer, Integer> readySubIdsBySlotId) {
+        mReadySubIdsBySlotId.clear();
         mReadySubIdsBySlotId.putAll(readySubIdsBySlotId);
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
+    void setSubIdToCarrierConfigMap(
+            Map<Integer, PersistableBundleWrapper> subIdToCarrierConfigMap) {
+        mSubIdToCarrierConfigMap.clear();
+        mSubIdToCarrierConfigMap.putAll(subIdToCarrierConfigMap);
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
     Map<Integer, Integer> getReadySubIdsBySlotId() {
         return Collections.unmodifiableMap(mReadySubIdsBySlotId);
     }
 
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    Map<Integer, PersistableBundleWrapper> getSubIdToCarrierConfigMap() {
+        return Collections.unmodifiableMap(mSubIdToCarrierConfigMap);
+    }
+
     /** TelephonySubscriptionSnapshot is a class containing info about active subscriptions */
     public static class TelephonySubscriptionSnapshot {
         private final int mActiveDataSubId;
         private final Map<Integer, SubscriptionInfo> mSubIdToInfoMap;
+        private final Map<Integer, PersistableBundleWrapper> mSubIdToCarrierConfigMap;
         private final Map<ParcelUuid, Set<String>> mPrivilegedPackages;
 
         public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT =
                 new TelephonySubscriptionSnapshot(
-                        INVALID_SUBSCRIPTION_ID, Collections.emptyMap(), Collections.emptyMap());
+                        INVALID_SUBSCRIPTION_ID,
+                        Collections.emptyMap(),
+                        Collections.emptyMap(),
+                        Collections.emptyMap());
 
         @VisibleForTesting(visibility = Visibility.PRIVATE)
         TelephonySubscriptionSnapshot(
                 int activeDataSubId,
                 @NonNull Map<Integer, SubscriptionInfo> subIdToInfoMap,
+                @NonNull Map<Integer, PersistableBundleWrapper> subIdToCarrierConfigMap,
                 @NonNull Map<ParcelUuid, Set<String>> privilegedPackages) {
             mActiveDataSubId = activeDataSubId;
             Objects.requireNonNull(subIdToInfoMap, "subIdToInfoMap was null");
             Objects.requireNonNull(privilegedPackages, "privilegedPackages was null");
+            Objects.requireNonNull(subIdToCarrierConfigMap, "subIdToCarrierConfigMap was null");
 
             mSubIdToInfoMap = Collections.unmodifiableMap(subIdToInfoMap);
+            mSubIdToCarrierConfigMap = Collections.unmodifiableMap(subIdToCarrierConfigMap);
 
             final Map<ParcelUuid, Set<String>> unmodifiableInnerSets = new ArrayMap<>();
             for (Entry<ParcelUuid, Set<String>> entry : privilegedPackages.entrySet()) {
@@ -423,9 +463,40 @@
                     : false;
         }
 
+        /**
+         * Retrieves a carrier config for a subscription in the provided group.
+         *
+         * <p>This method will prioritize non-opportunistic subscriptions, but will use the a
+         * carrier config for an opportunistic subscription if no other subscriptions are found.
+         */
+        @Nullable
+        public PersistableBundleWrapper getCarrierConfigForSubGrp(@NonNull ParcelUuid subGrp) {
+            PersistableBundleWrapper result = null;
+
+            for (int subId : getAllSubIdsInGroup(subGrp)) {
+                final PersistableBundleWrapper config = mSubIdToCarrierConfigMap.get(subId);
+                if (config != null) {
+                    result = 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(subId)) {
+                        return config;
+                    }
+                }
+            }
+
+            return result;
+        }
+
         @Override
         public int hashCode() {
-            return Objects.hash(mActiveDataSubId, mSubIdToInfoMap, mPrivilegedPackages);
+            return Objects.hash(
+                    mActiveDataSubId,
+                    mSubIdToInfoMap,
+                    mSubIdToCarrierConfigMap,
+                    mPrivilegedPackages);
         }
 
         @Override
@@ -438,6 +509,7 @@
 
             return mActiveDataSubId == other.mActiveDataSubId
                     && mSubIdToInfoMap.equals(other.mSubIdToInfoMap)
+                    && mSubIdToCarrierConfigMap.equals(other.mSubIdToCarrierConfigMap)
                     && mPrivilegedPackages.equals(other.mPrivilegedPackages);
         }
 
@@ -448,6 +520,7 @@
 
             pw.println("mActiveDataSubId: " + mActiveDataSubId);
             pw.println("mSubIdToInfoMap: " + mSubIdToInfoMap);
+            pw.println("mSubIdToCarrierConfigMap: " + mSubIdToCarrierConfigMap);
             pw.println("mPrivilegedPackages: " + mPrivilegedPackages);
 
             pw.decreaseIndent();
@@ -458,6 +531,7 @@
             return "TelephonySubscriptionSnapshot{ "
                     + "mActiveDataSubId=" + mActiveDataSubId
                     + ", mSubIdToInfoMap=" + mSubIdToInfoMap
+                    + ", mSubIdToCarrierConfigMap=" + mSubIdToCarrierConfigMap
                     + ", mPrivilegedPackages=" + mPrivilegedPackages
                     + " }";
         }
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 c96c1ee..2f84fdd 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java
@@ -24,6 +24,7 @@
 import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED;
 
 import static com.android.server.VcnManagementService.LOCAL_LOG;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,7 +35,6 @@
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.net.vcn.VcnWifiUnderlyingNetworkTemplate;
 import android.os.ParcelUuid;
-import android.os.PersistableBundle;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Slog;
@@ -81,7 +81,7 @@
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
 
         if (networkRecord.isBlocked) {
@@ -119,7 +119,7 @@
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
         final boolean isSelectedUnderlyingNetwork =
                 currentlySelected != null
@@ -181,7 +181,7 @@
             VcnWifiUnderlyingNetworkTemplate networkPriority,
             UnderlyingNetworkRecord networkRecord,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
 
         if (!caps.hasTransport(TRANSPORT_WIFI)) {
@@ -204,7 +204,7 @@
     private static boolean isWifiRssiAcceptable(
             UnderlyingNetworkRecord networkRecord,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         final NetworkCapabilities caps = networkRecord.networkCapabilities;
         final boolean isSelectedNetwork =
                 currentlySelected != null
@@ -314,7 +314,7 @@
         return false;
     }
 
-    static int getWifiEntryRssiThreshold(@Nullable PersistableBundle carrierConfig) {
+    static int getWifiEntryRssiThreshold(@Nullable PersistableBundleWrapper carrierConfig) {
         if (carrierConfig != null) {
             return carrierConfig.getInt(
                     VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
@@ -323,7 +323,7 @@
         return WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
     }
 
-    static int getWifiExitRssiThreshold(@Nullable PersistableBundle carrierConfig) {
+    static int getWifiExitRssiThreshold(@Nullable PersistableBundleWrapper carrierConfig) {
         if (carrierConfig != null) {
             return carrierConfig.getInt(
                     VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
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 a3babf7..d474c5d 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -21,7 +21,7 @@
 import static com.android.server.VcnManagementService.LOCAL_LOG;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.getWifiEntryRssiThreshold;
 import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.getWifiExitRssiThreshold;
-import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.isOpportunistic;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -37,8 +37,6 @@
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
-import android.os.PersistableBundle;
-import android.telephony.CarrierConfigManager;
 import android.telephony.TelephonyCallback;
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
@@ -51,7 +49,6 @@
 import com.android.server.vcn.util.LogUtils;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -87,7 +84,7 @@
     @Nullable private UnderlyingNetworkListener mRouteSelectionCallback;
 
     @NonNull private TelephonySubscriptionSnapshot mLastSnapshot;
-    @Nullable private PersistableBundle mCarrierConfig;
+    @Nullable private PersistableBundleWrapper mCarrierConfig;
     private boolean mIsQuitting = false;
 
     @Nullable private UnderlyingNetworkRecord mCurrentRecord;
@@ -124,25 +121,7 @@
                 .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;
-                }
-            }
-        }
+        mCarrierConfig = mLastSnapshot.getCarrierConfigForSubGrp(mSubscriptionGroup);
 
         registerOrUpdateNetworkRequests();
     }
@@ -334,6 +313,9 @@
         final TelephonySubscriptionSnapshot oldSnapshot = mLastSnapshot;
         mLastSnapshot = newSnapshot;
 
+        // Update carrier config
+        mCarrierConfig = mLastSnapshot.getCarrierConfigForSubGrp(mSubscriptionGroup);
+
         // Only trigger re-registration if subIds in this group have changed
         if (oldSnapshot
                 .getAllSubIdsInGroup(mSubscriptionGroup)
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 06f9280..319680e 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java
@@ -16,6 +16,8 @@
 
 package com.android.server.vcn.routeselection;
 
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkProperties;
@@ -23,7 +25,6 @@
 import android.net.NetworkCapabilities;
 import android.net.vcn.VcnUnderlyingNetworkTemplate;
 import android.os.ParcelUuid;
-import android.os.PersistableBundle;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
@@ -68,7 +69,7 @@
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         // Never changes after the underlying network record is created.
         if (mPriorityClass == PRIORITY_CLASS_INVALID) {
             mPriorityClass =
@@ -113,7 +114,7 @@
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         return (left, right) -> {
             final int leftIndex =
                     left.getOrCalculatePriorityClass(
@@ -167,7 +168,7 @@
             ParcelUuid subscriptionGroup,
             TelephonySubscriptionSnapshot snapshot,
             UnderlyingNetworkRecord currentlySelected,
-            PersistableBundle carrierConfig) {
+            PersistableBundleWrapper carrierConfig) {
         pw.println("UnderlyingNetworkRecord:");
         pw.increaseIndent();
 
diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
index 1c675c2..08e8eebb 100644
--- a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
+++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
@@ -28,11 +28,13 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
+import java.util.TreeSet;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
@@ -354,4 +356,182 @@
             }
         }
     }
+
+    /**
+     * Returns a copy of the persistable bundle with only the specified keys
+     *
+     * <p>This allows for holding minimized copies for memory-saving purposes.
+     */
+    @NonNull
+    public static PersistableBundle minimizeBundle(
+            @NonNull PersistableBundle bundle, String... keys) {
+        final PersistableBundle minimized = new PersistableBundle();
+
+        if (bundle == null) {
+            return minimized;
+        }
+
+        for (String key : keys) {
+            if (bundle.containsKey(key)) {
+                final Object value = bundle.get(key);
+                if (value == null) {
+                    continue;
+                }
+
+                if (value instanceof Boolean) {
+                    minimized.putBoolean(key, (Boolean) value);
+                } else if (value instanceof boolean[]) {
+                    minimized.putBooleanArray(key, (boolean[]) value);
+                } else if (value instanceof Double) {
+                    minimized.putDouble(key, (Double) value);
+                } else if (value instanceof double[]) {
+                    minimized.putDoubleArray(key, (double[]) value);
+                } else if (value instanceof Integer) {
+                    minimized.putInt(key, (Integer) value);
+                } else if (value instanceof int[]) {
+                    minimized.putIntArray(key, (int[]) value);
+                } else if (value instanceof Long) {
+                    minimized.putLong(key, (Long) value);
+                } else if (value instanceof long[]) {
+                    minimized.putLongArray(key, (long[]) value);
+                } else if (value instanceof String) {
+                    minimized.putString(key, (String) value);
+                } else if (value instanceof String[]) {
+                    minimized.putStringArray(key, (String[]) value);
+                } else if (value instanceof PersistableBundle) {
+                    minimized.putPersistableBundle(key, (PersistableBundle) value);
+                } else {
+                    continue;
+                }
+            }
+        }
+
+        return minimized;
+    }
+
+    /** Builds a stable hashcode */
+    public static int getHashCode(@Nullable PersistableBundle bundle) {
+        if (bundle == null) {
+            return -1;
+        }
+
+        int iterativeHashcode = 0;
+        TreeSet<String> treeSet = new TreeSet<>(bundle.keySet());
+        for (String key : treeSet) {
+            Object val = bundle.get(key);
+            if (val instanceof PersistableBundle) {
+                iterativeHashcode =
+                        Objects.hash(iterativeHashcode, key, getHashCode((PersistableBundle) val));
+            } else {
+                iterativeHashcode = Objects.hash(iterativeHashcode, key, val);
+            }
+        }
+
+        return iterativeHashcode;
+    }
+
+    /** Checks for persistable bundle equality */
+    public static boolean isEqual(
+            @Nullable PersistableBundle left, @Nullable PersistableBundle right) {
+        // Check for pointer equality & null equality
+        if (Objects.equals(left, right)) {
+            return true;
+        }
+
+        // If only one of the two is null, but not the other, not equal by definition.
+        if (Objects.isNull(left) != Objects.isNull(right)) {
+            return false;
+        }
+
+        if (!left.keySet().equals(right.keySet())) {
+            return false;
+        }
+
+        for (String key : left.keySet()) {
+            Object leftVal = left.get(key);
+            Object rightVal = right.get(key);
+
+            // Check for equality
+            if (Objects.equals(leftVal, rightVal)) {
+                continue;
+            } else if (Objects.isNull(leftVal) != Objects.isNull(rightVal)) {
+                // If only one of the two is null, but not the other, not equal by definition.
+                return false;
+            } else if (!Objects.equals(leftVal.getClass(), rightVal.getClass())) {
+                // If classes are different, not equal by definition.
+                return false;
+            }
+            if (leftVal instanceof PersistableBundle) {
+                if (!isEqual((PersistableBundle) leftVal, (PersistableBundle) rightVal)) {
+                    return false;
+                }
+            } else if (leftVal.getClass().isArray()) {
+                if (leftVal instanceof boolean[]) {
+                    if (!Arrays.equals((boolean[]) leftVal, (boolean[]) rightVal)) {
+                        return false;
+                    }
+                } else if (leftVal instanceof double[]) {
+                    if (!Arrays.equals((double[]) leftVal, (double[]) rightVal)) {
+                        return false;
+                    }
+                } else if (leftVal instanceof int[]) {
+                    if (!Arrays.equals((int[]) leftVal, (int[]) rightVal)) {
+                        return false;
+                    }
+                } else if (leftVal instanceof long[]) {
+                    if (!Arrays.equals((long[]) leftVal, (long[]) rightVal)) {
+                        return false;
+                    }
+                } else if (!Arrays.equals((Object[]) leftVal, (Object[]) rightVal)) {
+                    return false;
+                }
+            } else {
+                if (!Objects.equals(leftVal, rightVal)) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Wrapper class around PersistableBundles to allow equality comparisons
+     *
+     * <p>This class exposes the minimal getters to retrieve values.
+     */
+    public static class PersistableBundleWrapper {
+        @NonNull private final PersistableBundle mBundle;
+
+        public PersistableBundleWrapper(@NonNull PersistableBundle bundle) {
+            mBundle = Objects.requireNonNull(bundle, "Bundle was null");
+        }
+
+        /**
+         * Retrieves the integer associated with the provided key.
+         *
+         * @param key the string key to query
+         * @param defaultValue the value to return if key does not exist
+         * @return the int value, or the default
+         */
+        public int getInt(String key, int defaultValue) {
+            return mBundle.getInt(key, defaultValue);
+        }
+
+        @Override
+        public int hashCode() {
+            return getHashCode(mBundle);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof PersistableBundleWrapper)) {
+                return false;
+            }
+
+            final PersistableBundleWrapper other = (PersistableBundleWrapper) obj;
+
+            return isEqual(mBundle, other.mBundle);
+        }
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index 5f606e1..09080be 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -26,6 +26,7 @@
 
 import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
 import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback;
+import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -50,9 +51,11 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.net.vcn.VcnManager;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.ParcelUuid;
+import android.os.PersistableBundle;
 import android.os.test.TestLooper;
 import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionInfo;
@@ -104,6 +107,26 @@
         TEST_SUBID_TO_INFO_MAP = Collections.unmodifiableMap(subIdToGroupMap);
     }
 
+    private static final String TEST_CARRIER_CONFIG_KEY_1 = "TEST_CARRIER_CONFIG_KEY_1";
+    private static final String TEST_CARRIER_CONFIG_KEY_2 = "TEST_CARRIER_CONFIG_KEY_2";
+    private static final PersistableBundle TEST_CARRIER_CONFIG = new PersistableBundle();
+    private static final PersistableBundleWrapper TEST_CARRIER_CONFIG_WRAPPER;
+    private static final Map<Integer, PersistableBundleWrapper> TEST_SUBID_TO_CARRIER_CONFIG_MAP;
+
+    static {
+        TEST_CARRIER_CONFIG.putString(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY);
+        TEST_CARRIER_CONFIG.putString(
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
+                VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY);
+        TEST_CARRIER_CONFIG_WRAPPER = new PersistableBundleWrapper(TEST_CARRIER_CONFIG);
+
+        final Map<Integer, PersistableBundleWrapper> subIdToCarrierConfigMap = new HashMap<>();
+        subIdToCarrierConfigMap.put(TEST_SUBSCRIPTION_ID_1, TEST_CARRIER_CONFIG_WRAPPER);
+        TEST_SUBID_TO_CARRIER_CONFIG_MAP = Collections.unmodifiableMap(subIdToCarrierConfigMap);
+    }
+
     @NonNull private final Context mContext;
     @NonNull private final TestLooper mTestLooper;
     @NonNull private final Handler mHandler;
@@ -144,6 +167,9 @@
         doReturn(mCarrierConfigManager)
                 .when(mContext)
                 .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        doReturn(TEST_CARRIER_CONFIG)
+                .when(mCarrierConfigManager)
+                .getConfigForSubId(eq(TEST_SUBSCRIPTION_ID_1));
 
         // subId 1, 2 are in same subGrp, only subId 1 is active
         doReturn(TEST_PARCEL_UUID).when(TEST_SUBINFO_1).getGroupUuid();
@@ -227,14 +253,24 @@
     private TelephonySubscriptionSnapshot buildExpectedSnapshot(
             Map<Integer, SubscriptionInfo> subIdToInfoMap,
             Map<ParcelUuid, Set<String>> privilegedPackages) {
-        return new TelephonySubscriptionSnapshot(0, subIdToInfoMap, privilegedPackages);
+        return buildExpectedSnapshot(0, subIdToInfoMap, privilegedPackages);
     }
 
     private TelephonySubscriptionSnapshot buildExpectedSnapshot(
             int activeSubId,
             Map<Integer, SubscriptionInfo> subIdToInfoMap,
             Map<ParcelUuid, Set<String>> privilegedPackages) {
-        return new TelephonySubscriptionSnapshot(activeSubId, subIdToInfoMap, privilegedPackages);
+        return buildExpectedSnapshot(
+                activeSubId, subIdToInfoMap, TEST_SUBID_TO_CARRIER_CONFIG_MAP, privilegedPackages);
+    }
+
+    private TelephonySubscriptionSnapshot buildExpectedSnapshot(
+            int activeSubId,
+            Map<Integer, SubscriptionInfo> subIdToInfoMap,
+            Map<Integer, PersistableBundleWrapper> subIdToCarrierConfigMap,
+            Map<ParcelUuid, Set<String>> privilegedPackages) {
+        return new TelephonySubscriptionSnapshot(
+                activeSubId, subIdToInfoMap, subIdToCarrierConfigMap, privilegedPackages);
     }
 
     private void verifyNoActiveSubscriptions() {
@@ -245,6 +281,8 @@
     private void setupReadySubIds() {
         mTelephonySubscriptionTracker.setReadySubIdsBySlotId(
                 Collections.singletonMap(TEST_SIM_SLOT_INDEX, TEST_SUBSCRIPTION_ID_1));
+        mTelephonySubscriptionTracker.setSubIdToCarrierConfigMap(
+                Collections.singletonMap(TEST_SUBSCRIPTION_ID_1, TEST_CARRIER_CONFIG_WRAPPER));
     }
 
     private void setPrivilegedPackagesForMock(@NonNull List<String> privilegedPackages) {
@@ -300,6 +338,7 @@
         readySubIdsBySlotId.put(TEST_SIM_SLOT_INDEX + 1, TEST_SUBSCRIPTION_ID_1);
 
         mTelephonySubscriptionTracker.setReadySubIdsBySlotId(readySubIdsBySlotId);
+        mTelephonySubscriptionTracker.setSubIdToCarrierConfigMap(TEST_SUBID_TO_CARRIER_CONFIG_MAP);
         doReturn(1).when(mTelephonyManager).getActiveModemCount();
 
         List<CarrierPrivilegesCallback> carrierPrivilegesCallbacks =
@@ -464,8 +503,16 @@
 
         mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(false));
         mTestLooper.dispatchAll();
-        verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap())));
+        verify(mCallback)
+                .onNewSnapshot(
+                        eq(
+                                buildExpectedSnapshot(
+                                        0, TEST_SUBID_TO_INFO_MAP, emptyMap(), emptyMap())));
         assertNull(mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
+        assertNull(
+                mTelephonySubscriptionTracker
+                        .getSubIdToCarrierConfigMap()
+                        .get(TEST_SUBSCRIPTION_ID_1));
     }
 
     @Test
@@ -493,7 +540,7 @@
     public void testTelephonySubscriptionSnapshotGetGroupForSubId() throws Exception {
         final TelephonySubscriptionSnapshot snapshot =
                 new TelephonySubscriptionSnapshot(
-                        TEST_SUBSCRIPTION_ID_1, TEST_SUBID_TO_INFO_MAP, emptyMap());
+                        TEST_SUBSCRIPTION_ID_1, TEST_SUBID_TO_INFO_MAP, emptyMap(), emptyMap());
 
         assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_1));
         assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_2));
@@ -503,7 +550,7 @@
     public void testTelephonySubscriptionSnapshotGetAllSubIdsInGroup() throws Exception {
         final TelephonySubscriptionSnapshot snapshot =
                 new TelephonySubscriptionSnapshot(
-                        TEST_SUBSCRIPTION_ID_1, TEST_SUBID_TO_INFO_MAP, emptyMap());
+                        TEST_SUBSCRIPTION_ID_1, TEST_SUBID_TO_INFO_MAP, emptyMap(), emptyMap());
 
         assertEquals(
                 new ArraySet<>(Arrays.asList(TEST_SUBSCRIPTION_ID_1, TEST_SUBSCRIPTION_ID_2)),
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 5628321..4d25684 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -129,6 +129,7 @@
             new TelephonySubscriptionSnapshot(
                     TEST_SUB_ID,
                     Collections.singletonMap(TEST_SUB_ID, TEST_SUB_INFO),
+                    Collections.EMPTY_MAP,
                     Collections.EMPTY_MAP);
 
     @NonNull protected final Context mContext;
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
index 6c849b5..b0d6895 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkPriorityClassifierTest.java
@@ -30,6 +30,7 @@
 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 com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -309,7 +310,9 @@
                         wifiNetworkPriority,
                         mWifiNetworkRecord,
                         selectedNetworkRecord,
-                        carrierConfig));
+                        carrierConfig == null
+                                ? null
+                                : new PersistableBundleWrapper(carrierConfig)));
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java
index a44a734..294f5c1 100644
--- a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java
+++ b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java
@@ -18,6 +18,8 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import android.os.PersistableBundle;
 
@@ -211,4 +213,84 @@
 
         assertEquals(testInt, result);
     }
+
+    private PersistableBundle getTestBundle() {
+        final PersistableBundle bundle = new PersistableBundle();
+
+        bundle.putBoolean(TEST_KEY + "Boolean", true);
+        bundle.putBooleanArray(TEST_KEY + "BooleanArray", new boolean[] {true, false});
+        bundle.putDouble(TEST_KEY + "Double", 0.1);
+        bundle.putDoubleArray(TEST_KEY + "DoubleArray", new double[] {0.1, 0.2, 0.3});
+        bundle.putInt(TEST_KEY + "Int", 1);
+        bundle.putIntArray(TEST_KEY + "IntArray", new int[] {1, 2});
+        bundle.putLong(TEST_KEY + "Long", 5L);
+        bundle.putLongArray(TEST_KEY + "LongArray", new long[] {0L, -1L, -2L});
+        bundle.putString(TEST_KEY + "String", "TEST");
+        bundle.putStringArray(TEST_KEY + "StringArray", new String[] {"foo", "bar", "bas"});
+        bundle.putPersistableBundle(
+                TEST_KEY + "PersistableBundle",
+                new TestClass(1, TEST_INT_ARRAY, TEST_STRING_PREFIX, new PersistableBundle())
+                        .toPersistableBundle());
+
+        return bundle;
+    }
+
+    @Test
+    public void testMinimizeBundle() throws Exception {
+        final String[] minimizedKeys =
+                new String[] {
+                    TEST_KEY + "Boolean",
+                    TEST_KEY + "BooleanArray",
+                    TEST_KEY + "Double",
+                    TEST_KEY + "DoubleArray",
+                    TEST_KEY + "Int",
+                    TEST_KEY + "IntArray",
+                    TEST_KEY + "Long",
+                    TEST_KEY + "LongArray",
+                    TEST_KEY + "String",
+                    TEST_KEY + "StringArray",
+                    TEST_KEY + "PersistableBundle"
+                };
+
+        final PersistableBundle testBundle = getTestBundle();
+        testBundle.putBoolean(TEST_KEY + "Boolean2", true);
+
+        final PersistableBundle minimized =
+                PersistableBundleUtils.minimizeBundle(testBundle, minimizedKeys);
+
+        // Verify that the minimized bundle is NOT the same in size OR values due to the extra
+        // Boolean2 key
+        assertFalse(PersistableBundleUtils.isEqual(testBundle, minimized));
+
+        // Verify that removing the extra key from the source bundle results in equality.
+        testBundle.remove(TEST_KEY + "Boolean2");
+        assertTrue(PersistableBundleUtils.isEqual(testBundle, minimized));
+    }
+
+    @Test
+    public void testEquality_identical() throws Exception {
+        final PersistableBundle left = getTestBundle();
+        final PersistableBundle right = getTestBundle();
+
+        assertTrue(PersistableBundleUtils.isEqual(left, right));
+    }
+
+    @Test
+    public void testEquality_different() throws Exception {
+        final PersistableBundle left = getTestBundle();
+        final PersistableBundle right = getTestBundle();
+
+        left.putBoolean(TEST_KEY + "Boolean2", true);
+        assertFalse(PersistableBundleUtils.isEqual(left, right));
+
+        left.remove(TEST_KEY + "Boolean2");
+        assertTrue(PersistableBundleUtils.isEqual(left, right));
+    }
+
+    @Test
+    public void testEquality_null() throws Exception {
+        assertFalse(PersistableBundleUtils.isEqual(getTestBundle(), null));
+        assertFalse(PersistableBundleUtils.isEqual(null, getTestBundle()));
+        assertTrue(PersistableBundleUtils.isEqual(null, null));
+    }
 }