Merge changes Ifabf5e2b,I2e632532

* changes:
  Add persistence for VcnConfig objects by Subscription Group
  Implement basic VcnConfig and VcnGatewayConnectionConfig
diff --git a/Android.bp b/Android.bp
index 26e7165..4b3c22d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -314,6 +314,7 @@
         ":framework-telecomm-sources",
         ":framework-telephony-common-sources",
         ":framework-telephony-sources",
+        ":framework-vcn-util-sources",
         ":framework-wifi-annotations",
         ":framework-wifi-non-updatable-sources",
         ":PacProcessor-aidl-sources",
diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java
index 148acf1..d4a3fa7 100644
--- a/core/java/android/net/vcn/VcnConfig.java
+++ b/core/java/android/net/vcn/VcnConfig.java
@@ -15,30 +15,104 @@
  */
 package android.net.vcn;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.vcn.util.PersistableBundleUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
 
 /**
  * This class represents a configuration for a Virtual Carrier Network.
  *
+ * <p>Each {@link VcnGatewayConnectionConfig} instance added represents a connection that will be
+ * brought up on demand based on active {@link NetworkRequest}(s).
+ *
+ * @see VcnManager for more information on the Virtual Carrier Network feature
  * @hide
  */
 public final class VcnConfig implements Parcelable {
     @NonNull private static final String TAG = VcnConfig.class.getSimpleName();
 
-    private VcnConfig() {
+    private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs";
+    @NonNull private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs;
+
+    private VcnConfig(@NonNull Set<VcnGatewayConnectionConfig> tunnelConfigs) {
+        mGatewayConnectionConfigs = Collections.unmodifiableSet(tunnelConfigs);
+
         validate();
     }
-    // TODO: Implement getters, validators, etc
 
     /**
-     * Validates this configuration.
+     * Deserializes a VcnConfig from a PersistableBundle.
      *
      * @hide
      */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public VcnConfig(@NonNull PersistableBundle in) {
+        final PersistableBundle gatewayConnectionConfigsBundle =
+                in.getPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY);
+        mGatewayConnectionConfigs =
+                new ArraySet<>(
+                        PersistableBundleUtils.toList(
+                                gatewayConnectionConfigsBundle, VcnGatewayConnectionConfig::new));
+
+        validate();
+    }
+
     private void validate() {
-        // TODO: implement validation logic
+        Preconditions.checkCollectionNotEmpty(
+                mGatewayConnectionConfigs, "gatewayConnectionConfigs");
+    }
+
+    /** Retrieves the set of configured tunnels. */
+    @NonNull
+    public Set<VcnGatewayConnectionConfig> getGatewayConnectionConfigs() {
+        return Collections.unmodifiableSet(mGatewayConnectionConfigs);
+    }
+
+    /**
+     * Serializes this object to a PersistableBundle.
+     *
+     * @hide
+     */
+    @NonNull
+    public PersistableBundle toPersistableBundle() {
+        final PersistableBundle result = new PersistableBundle();
+
+        final PersistableBundle gatewayConnectionConfigsBundle =
+                PersistableBundleUtils.fromList(
+                        new ArrayList<>(mGatewayConnectionConfigs),
+                        VcnGatewayConnectionConfig::toPersistableBundle);
+        result.putPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY, gatewayConnectionConfigsBundle);
+
+        return result;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mGatewayConnectionConfigs);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (!(other instanceof VcnConfig)) {
+            return false;
+        }
+
+        final VcnConfig rhs = (VcnConfig) other;
+        return mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs);
     }
 
     // Parcelable methods
@@ -49,15 +123,16 @@
     }
 
     @Override
-    public void writeToParcel(Parcel out, int flags) {}
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(toPersistableBundle(), flags);
+    }
 
     @NonNull
     public static final Parcelable.Creator<VcnConfig> CREATOR =
             new Parcelable.Creator<VcnConfig>() {
                 @NonNull
                 public VcnConfig createFromParcel(Parcel in) {
-                    // TODO: Ensure all methods are pulled from the parcels
-                    return new VcnConfig();
+                    return new VcnConfig((PersistableBundle) in.readParcelable(null));
                 }
 
                 @NonNull
@@ -68,7 +143,23 @@
 
     /** This class is used to incrementally build {@link VcnConfig} objects. */
     public static class Builder {
-        // TODO: Implement this builder
+        @NonNull
+        private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs = new ArraySet<>();
+
+        /**
+         * Adds a configuration for an individual gateway connection.
+         *
+         * @param gatewayConnectionConfig the configuration for an individual gateway connection
+         * @return this {@link Builder} instance, for chaining
+         */
+        @NonNull
+        public Builder addGatewayConnectionConfig(
+                @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig) {
+            Objects.requireNonNull(gatewayConnectionConfig, "gatewayConnectionConfig was null");
+
+            mGatewayConnectionConfigs.add(gatewayConnectionConfig);
+            return this;
+        }
 
         /**
          * Builds and validates the VcnConfig.
@@ -77,7 +168,7 @@
          */
         @NonNull
         public VcnConfig build() {
-            return new VcnConfig();
+            return new VcnConfig(mGatewayConnectionConfigs);
         }
     }
 }
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index 8160edc..039360a 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -15,7 +15,27 @@
  */
 package android.net.vcn;
 
+import static android.net.NetworkCapabilities.NetCapability;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import android.annotation.IntRange;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.os.PersistableBundle;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.vcn.util.PersistableBundleUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * This class represents a configuration for a connection to a Virtual Carrier Network gateway.
@@ -49,38 +69,399 @@
  *   <li>{@link android.net.NetworkCapabilities.NET_CAPABILITY_MCX}
  * </ul>
  *
+ * <p>The meteredness and roaming of the VCN {@link Network} will be determined by that of the
+ * underlying Network(s).
+ *
  * @hide
  */
 public final class VcnGatewayConnectionConfig {
-    private VcnGatewayConnectionConfig() {
+    // TODO: Use MIN_MTU_V6 once it is public, @hide
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final int MIN_MTU_V6 = 1280;
+
+    private static final Set<Integer> ALLOWED_CAPABILITIES;
+
+    static {
+        Set<Integer> allowedCaps = new ArraySet<>();
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_MMS);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_SUPL);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_DUN);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_FOTA);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_IMS);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_CBS);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_IA);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_RCS);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_XCAP);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_EIMS);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_MCX);
+
+        ALLOWED_CAPABILITIES = Collections.unmodifiableSet(allowedCaps);
+    }
+
+    private static final int DEFAULT_MAX_MTU = 1500;
+
+    /**
+     * The maximum number of retry intervals that may be specified.
+     *
+     * <p>Limited to ensure an upper bound on config sizes.
+     */
+    private static final int MAX_RETRY_INTERVAL_COUNT = 10;
+
+    /**
+     * The minimum allowable repeating retry interval
+     *
+     * <p>To ensure the device is not constantly being woken up, this retry interval MUST be greater
+     * than this value.
+     *
+     * @see {@link Builder#setRetryInterval()}
+     */
+    private static final long MINIMUM_REPEATING_RETRY_INTERVAL_MS = TimeUnit.MINUTES.toMillis(15);
+
+    private static final long[] DEFAULT_RETRY_INTERVALS_MS =
+            new long[] {
+                TimeUnit.SECONDS.toMillis(1),
+                TimeUnit.SECONDS.toMillis(2),
+                TimeUnit.SECONDS.toMillis(5),
+                TimeUnit.SECONDS.toMillis(30),
+                TimeUnit.MINUTES.toMillis(1),
+                TimeUnit.MINUTES.toMillis(5),
+                TimeUnit.MINUTES.toMillis(15)
+            };
+
+    private static final String EXPOSED_CAPABILITIES_KEY = "mExposedCapabilities";
+    @NonNull private final Set<Integer> mExposedCapabilities;
+
+    private static final String UNDERLYING_CAPABILITIES_KEY = "mUnderlyingCapabilities";
+    @NonNull private final Set<Integer> mUnderlyingCapabilities;
+
+    // TODO: Add Ike/ChildSessionParams as a subclass - maybe VcnIkeGatewayConnectionConfig
+
+    private static final String MAX_MTU_KEY = "mMaxMtu";
+    private final int mMaxMtu;
+
+    private static final String RETRY_INTERVAL_MS_KEY = "mRetryIntervalsMs";
+    @NonNull private final long[] mRetryIntervalsMs;
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public VcnGatewayConnectionConfig(
+            @NonNull Set<Integer> exposedCapabilities,
+            @NonNull Set<Integer> underlyingCapabilities,
+            @NonNull long[] retryIntervalsMs,
+            @IntRange(from = MIN_MTU_V6) int maxMtu) {
+        mExposedCapabilities = exposedCapabilities;
+        mUnderlyingCapabilities = underlyingCapabilities;
+        mRetryIntervalsMs = retryIntervalsMs;
+        mMaxMtu = maxMtu;
+
         validate();
     }
 
-    // TODO: Implement getters, validators, etc
+    /** @hide */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public VcnGatewayConnectionConfig(@NonNull PersistableBundle in) {
+        final PersistableBundle exposedCapsBundle =
+                in.getPersistableBundle(EXPOSED_CAPABILITIES_KEY);
+        final PersistableBundle underlyingCapsBundle =
+                in.getPersistableBundle(UNDERLYING_CAPABILITIES_KEY);
+
+        mExposedCapabilities = new ArraySet<>(PersistableBundleUtils.toList(
+                exposedCapsBundle, PersistableBundleUtils.INTEGER_DESERIALIZER));
+        mUnderlyingCapabilities = new ArraySet<>(PersistableBundleUtils.toList(
+                underlyingCapsBundle, PersistableBundleUtils.INTEGER_DESERIALIZER));
+        mRetryIntervalsMs = in.getLongArray(RETRY_INTERVAL_MS_KEY);
+        mMaxMtu = in.getInt(MAX_MTU_KEY);
+
+        validate();
+    }
+
+    private void validate() {
+        Preconditions.checkArgument(
+                mExposedCapabilities != null && !mExposedCapabilities.isEmpty(),
+                "exposedCapsBundle was null or empty");
+        for (Integer cap : getAllExposedCapabilities()) {
+            checkValidCapability(cap);
+        }
+
+        Preconditions.checkArgument(
+                mUnderlyingCapabilities != null && !mUnderlyingCapabilities.isEmpty(),
+                "underlyingCapabilities was null or empty");
+        for (Integer cap : getAllUnderlyingCapabilities()) {
+            checkValidCapability(cap);
+        }
+
+        Objects.requireNonNull(mRetryIntervalsMs, "retryIntervalsMs was null");
+        validateRetryInterval(mRetryIntervalsMs);
+
+        Preconditions.checkArgument(
+                mMaxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)");
+    }
+
+    private static void checkValidCapability(int capability) {
+        Preconditions.checkArgument(
+                ALLOWED_CAPABILITIES.contains(capability),
+                "NetworkCapability " + capability + "out of range");
+    }
+
+    private static void validateRetryInterval(@Nullable long[] retryIntervalsMs) {
+        Preconditions.checkArgument(
+                retryIntervalsMs != null
+                        && retryIntervalsMs.length > 0
+                        && retryIntervalsMs.length <= MAX_RETRY_INTERVAL_COUNT,
+                "retryIntervalsMs was null, empty or exceed max interval count");
+
+        final long repeatingInterval = retryIntervalsMs[retryIntervalsMs.length - 1];
+        if (repeatingInterval < MINIMUM_REPEATING_RETRY_INTERVAL_MS) {
+            throw new IllegalArgumentException(
+                    "Repeating retry interval was too short, must be a minimum of 15 minutes: "
+                            + repeatingInterval);
+        }
+    }
 
     /**
-     * Validates this configuration
+     * Returns all exposed capabilities.
      *
      * @hide
      */
-    private void validate() {
-        // TODO: implement validation logic
+    @NonNull
+    public Set<Integer> getAllExposedCapabilities() {
+        return Collections.unmodifiableSet(mExposedCapabilities);
     }
 
-    // Parcelable methods
+    /**
+     * Checks if this config is configured to support/expose a specific capability.
+     *
+     * @param capability the capability to check for
+     */
+    public boolean hasExposedCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
 
-    /** This class is used to incrementally build {@link VcnGatewayConnectionConfig} objects */
+        return mExposedCapabilities.contains(capability);
+    }
+
+    /**
+     * Returns all capabilities required of underlying networks.
+     *
+     * @hide
+     */
+    @NonNull
+    public Set<Integer> getAllUnderlyingCapabilities() {
+        return Collections.unmodifiableSet(mUnderlyingCapabilities);
+    }
+
+    /**
+     * Checks if this config requires an underlying network to have the specified capability.
+     *
+     * @param capability the capability to check for
+     */
+    public boolean requiresUnderlyingCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+
+        return mUnderlyingCapabilities.contains(capability);
+    }
+
+    /** Retrieves the configured retry intervals. */
+    @NonNull
+    public long[] getRetryIntervalsMs() {
+        return Arrays.copyOf(mRetryIntervalsMs, mRetryIntervalsMs.length);
+    }
+
+    /** Retrieves the maximum MTU allowed for this Gateway Connection. */
+    @IntRange(from = MIN_MTU_V6)
+    public int getMaxMtu() {
+        return mMaxMtu;
+    }
+
+    /**
+     * Converts this config to a PersistableBundle.
+     *
+     * @hide
+     */
+    @NonNull
+    @VisibleForTesting(visibility = Visibility.PROTECTED)
+    public PersistableBundle toPersistableBundle() {
+        final PersistableBundle result = new PersistableBundle();
+
+        final PersistableBundle exposedCapsBundle =
+                PersistableBundleUtils.fromList(
+                        new ArrayList<>(mExposedCapabilities),
+                        PersistableBundleUtils.INTEGER_SERIALIZER);
+        final PersistableBundle underlyingCapsBundle =
+                PersistableBundleUtils.fromList(
+                        new ArrayList<>(mUnderlyingCapabilities),
+                        PersistableBundleUtils.INTEGER_SERIALIZER);
+
+        result.putPersistableBundle(EXPOSED_CAPABILITIES_KEY, exposedCapsBundle);
+        result.putPersistableBundle(UNDERLYING_CAPABILITIES_KEY, underlyingCapsBundle);
+        result.putLongArray(RETRY_INTERVAL_MS_KEY, mRetryIntervalsMs);
+        result.putInt(MAX_MTU_KEY, mMaxMtu);
+
+        return result;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mExposedCapabilities,
+                mUnderlyingCapabilities,
+                Arrays.hashCode(mRetryIntervalsMs),
+                mMaxMtu);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (!(other instanceof VcnGatewayConnectionConfig)) {
+            return false;
+        }
+
+        final VcnGatewayConnectionConfig rhs = (VcnGatewayConnectionConfig) other;
+        return mExposedCapabilities.equals(rhs.mExposedCapabilities)
+                && mUnderlyingCapabilities.equals(rhs.mUnderlyingCapabilities)
+                && Arrays.equals(mRetryIntervalsMs, rhs.mRetryIntervalsMs)
+                && mMaxMtu == rhs.mMaxMtu;
+    }
+
+    /** This class is used to incrementally build {@link VcnGatewayConnectionConfig} objects. */
     public static class Builder {
-        // TODO: Implement this builder
+        @NonNull private final Set<Integer> mExposedCapabilities = new ArraySet();
+        @NonNull private final Set<Integer> mUnderlyingCapabilities = new ArraySet();
+        @NonNull private long[] mRetryIntervalsMs = DEFAULT_RETRY_INTERVALS_MS;
+        private int mMaxMtu = DEFAULT_MAX_MTU;
+
+        // TODO: (b/175829816) Consider VCN-exposed capabilities that may be transport dependent.
+        //       Consider the case where the VCN might only expose MMS on WiFi, but defer to MMS
+        //       when on Cell.
 
         /**
-         * Builds and validates the VcnGatewayConnectionConfig
+         * Add a capability that this VCN Gateway Connection will support.
+         *
+         * @param exposedCapability the app-facing capability to be exposed by this VCN Gateway
+         *     Connection (i.e., the capabilities that this VCN Gateway Connection will support).
+         * @return this {@link Builder} instance, for chaining
+         * @see VcnGatewayConnectionConfig for a list of capabilities may be exposed by a Gateway
+         *     Connection
+         */
+        public Builder addExposedCapability(@NetCapability int exposedCapability) {
+            checkValidCapability(exposedCapability);
+
+            mExposedCapabilities.add(exposedCapability);
+            return this;
+        }
+
+        /**
+         * Remove a capability that this VCN Gateway Connection will support.
+         *
+         * @param exposedCapability the app-facing capability to not be exposed by this VCN Gateway
+         *     Connection (i.e., the capabilities that this VCN Gateway Connection will support)
+         * @return this {@link Builder} instance, for chaining
+         * @see VcnGatewayConnectionConfig for a list of capabilities may be exposed by a Gateway
+         *     Connection
+         */
+        public Builder removeExposedCapability(@NetCapability int exposedCapability) {
+            checkValidCapability(exposedCapability);
+
+            mExposedCapabilities.remove(exposedCapability);
+            return this;
+        }
+
+        /**
+         * Require a capability for Networks underlying this VCN Gateway Connection.
+         *
+         * @param underlyingCapability the capability that a network MUST have in order to be an
+         *     underlying network for this VCN Gateway Connection.
+         * @return this {@link Builder} instance, for chaining
+         * @see VcnGatewayConnectionConfig for a list of capabilities may be required of underlying
+         *     networks
+         */
+        public Builder addRequiredUnderlyingCapability(@NetCapability int underlyingCapability) {
+            checkValidCapability(underlyingCapability);
+
+            mUnderlyingCapabilities.add(underlyingCapability);
+            return this;
+        }
+
+        /**
+         * Remove a requirement of a capability for Networks underlying this VCN Gateway Connection.
+         *
+         * <p>Calling this method will allow Networks that do NOT have this capability to be
+         * selected as an underlying network for this VCN Gateway Connection. However, underlying
+         * networks MAY still have the removed capability.
+         *
+         * @param underlyingCapability the capability that a network DOES NOT need to have in order
+         *     to be an underlying network for this VCN Gateway Connection.
+         * @return this {@link Builder} instance, for chaining
+         * @see VcnGatewayConnectionConfig for a list of capabilities may be required of underlying
+         *     networks
+         */
+        public Builder removeRequiredUnderlyingCapability(@NetCapability int underlyingCapability) {
+            checkValidCapability(underlyingCapability);
+
+            mUnderlyingCapabilities.remove(underlyingCapability);
+            return this;
+        }
+
+        /**
+         * Set the retry interval between VCN establishment attempts upon successive failures.
+         *
+         * <p>The last retry interval will be repeated until safe mode is entered, or a connection
+         * is successfully established, at which point the retry timers will be reset. For power
+         * reasons, the last (repeated) retry interval MUST be at least 15 minutes.
+         *
+         * <p>Retry intervals MAY be subject to system power saving modes. That is to say that if
+         * the system enters a power saving mode, the retry may not occur until the device leaves
+         * the specified power saving mode. Intervals are sequential, and intervals will NOT be
+         * skipped if system power saving results in delaying retries (even if it exceed multiple
+         * retry intervals).
+         *
+         * <p>Each Gateway Connection will retry according to the retry intervals configured, but if
+         * safe mode is enabled, all Gateway Connection(s) will be disabled.
+         *
+         * @param retryIntervalsMs an array of between 1 and 10 millisecond intervals after which
+         *     the VCN will attempt to retry a session initiation. The last (repeating) retry
+         *     interval must be at least 15 minutes. Defaults to: {@code [1s, 2s, 5s, 30s, 1m, 5m,
+         *     15m]}
+         * @return this {@link Builder} instance, for chaining
+         * @see VcnManager for additional discussion on fail-safe mode
+         */
+        @NonNull
+        public Builder setRetryInterval(@NonNull long[] retryIntervalsMs) {
+            validateRetryInterval(retryIntervalsMs);
+
+            mRetryIntervalsMs = retryIntervalsMs;
+            return this;
+        }
+
+        /**
+         * Sets the maximum MTU allowed for this VCN Gateway Connection.
+         *
+         * <p>This MTU is applied to the VCN Gateway Connection exposed Networks, and represents the
+         * MTU of the virtualized network.
+         *
+         * <p>The system may reduce the MTU below the maximum specified based on signals such as the
+         * MTU of the underlying networks (and adjusted for Gateway Connection overhead).
+         *
+         * @param maxMtu the maximum MTU allowed for this Gateway Connection. Must be greater than
+         *     the IPv6 minimum MTU of 1280. Defaults to 1500.
+         * @return this {@link Builder} instance, for chaining
+         */
+        @NonNull
+        public Builder setMaxMtu(@IntRange(from = MIN_MTU_V6) int maxMtu) {
+            Preconditions.checkArgument(
+                    maxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)");
+
+            mMaxMtu = maxMtu;
+            return this;
+        }
+
+        /**
+         * Builds and validates the VcnGatewayConnectionConfig.
          *
          * @return an immutable VcnGatewayConnectionConfig instance
          */
         @NonNull
         public VcnGatewayConnectionConfig build() {
-            return new VcnGatewayConnectionConfig();
+            return new VcnGatewayConnectionConfig(
+                    mExposedCapabilities, mUnderlyingCapabilities, mRetryIntervalsMs, mMaxMtu);
         }
     }
 }
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 6769b9e..46d1c1c 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -23,6 +23,9 @@
 import android.content.Context;
 import android.os.ParcelUuid;
 import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import java.io.IOException;
 
 /**
  * VcnManager publishes APIs for applications to configure and manage Virtual Carrier Networks.
@@ -63,15 +66,20 @@
      * @param config the configuration parameters for the VCN
      * @throws SecurityException if the caller does not have carrier privileges, or is not running
      *     as the primary user
+     * @throws IOException if the configuration failed to be persisted. A caller encountering this
+     *     exception should attempt to retry (possibly after a delay).
      * @hide
      */
     @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
-    public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
+    public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config)
+            throws IOException {
         requireNonNull(subscriptionGroup, "subscriptionGroup was null");
         requireNonNull(config, "config was null");
 
         try {
             mService.setVcnConfig(subscriptionGroup, config);
+        } catch (ServiceSpecificException e) {
+            throw new IOException(e);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -88,14 +96,18 @@
      * @param subscriptionGroup the subscription group that the configuration should be applied to
      * @throws SecurityException if the caller does not have carrier privileges, or is not running
      *     as the primary user
+     * @throws IOException if the configuration failed to be cleared. A caller encountering this
+     *     exception should attempt to retry (possibly after a delay).
      * @hide
      */
     @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
-    public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) {
+    public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) throws IOException {
         requireNonNull(subscriptionGroup, "subscriptionGroup was null");
 
         try {
             mService.clearVcnConfig(subscriptionGroup);
+        } catch (ServiceSpecificException e) {
+            throw new IOException(e);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index e9f17ff..5e85409 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -26,20 +26,31 @@
 import android.net.vcn.IVcnManagementService;
 import android.net.vcn.VcnConfig;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.ParcelUuid;
+import android.os.PersistableBundle;
 import android.os.Process;
+import android.os.ServiceSpecificException;
 import android.os.UserHandle;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.server.vcn.util.PersistableBundleUtils;
 
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * VcnManagementService manages Virtual Carrier Network profiles and lifecycles.
@@ -101,20 +112,72 @@
 
     public static final boolean VDBG = false; // STOPSHIP: if true
 
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml";
+
     /* Binder context for this service */
     @NonNull private final Context mContext;
     @NonNull private final Dependencies mDeps;
 
     @NonNull private final Looper mLooper;
+    @NonNull private final Handler mHandler;
     @NonNull private final VcnNetworkProvider mNetworkProvider;
 
+    @GuardedBy("mLock")
+    @NonNull
+    private final Map<ParcelUuid, VcnConfig> mConfigs = new ArrayMap<>();
+
+    @NonNull private final Object mLock = new Object();
+
+    @NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper;
+
     @VisibleForTesting(visibility = Visibility.PRIVATE)
     VcnManagementService(@NonNull Context context, @NonNull Dependencies deps) {
         mContext = requireNonNull(context, "Missing context");
         mDeps = requireNonNull(deps, "Missing dependencies");
 
         mLooper = mDeps.getLooper();
+        mHandler = new Handler(mLooper);
         mNetworkProvider = new VcnNetworkProvider(mContext, mLooper);
+
+        mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE);
+
+        // Run on handler to ensure I/O does not block system server startup
+        mHandler.post(() -> {
+            PersistableBundle configBundle = null;
+            try {
+                configBundle = mConfigDiskRwHelper.readFromDisk();
+            } catch (IOException e1) {
+                Slog.e(TAG, "Failed to read configs from disk; retrying", e1);
+
+                // Retry immediately. The IOException may have been transient.
+                try {
+                    configBundle = mConfigDiskRwHelper.readFromDisk();
+                } catch (IOException e2) {
+                    Slog.wtf(TAG, "Failed to read configs from disk", e2);
+                    return;
+                }
+            }
+
+            if (configBundle != null) {
+                final Map<ParcelUuid, VcnConfig> configs =
+                        PersistableBundleUtils.toMap(
+                                configBundle,
+                                PersistableBundleUtils::toParcelUuid,
+                                VcnConfig::new);
+
+                synchronized (mLock) {
+                    for (Entry<ParcelUuid, VcnConfig> entry : configs.entrySet()) {
+                        // Ensure no new configs are overwritten; a carrier app may have added a new
+                        // config.
+                        if (!mConfigs.containsKey(entry.getKey())) {
+                            mConfigs.put(entry.getKey(), entry.getValue());
+                        }
+                    }
+                    // TODO: Trigger re-evaluation of active VCNs; start/stop VCNs as needed.
+                }
+            }
+        });
     }
 
     // Package-visibility for SystemServer to create instances.
@@ -151,12 +214,21 @@
         public int getBinderCallingUid() {
             return Binder.getCallingUid();
         }
+
+        /**
+         * Creates and returns a new {@link PersistableBundle.LockingReadWriteHelper}
+         *
+         * @param path the file path to read/write from/to.
+         * @return the {@link PersistableBundleUtils.LockingReadWriteHelper} instance
+         */
+        public PersistableBundleUtils.LockingReadWriteHelper
+                newPersistableBundleLockingReadWriteHelper(@NonNull String path) {
+            return new PersistableBundleUtils.LockingReadWriteHelper(path);
+        }
     }
 
     /** Notifies the VcnManagementService that external dependencies can be set up. */
     public void systemReady() {
-        // TODO: Retrieve existing profiles from KeyStore
-
         mContext.getSystemService(ConnectivityManager.class)
                 .registerNetworkProvider(mNetworkProvider);
     }
@@ -217,9 +289,15 @@
 
         enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
 
-        // TODO: Clear Binder calling identity
+        synchronized (mLock) {
+            mConfigs.put(subscriptionGroup, config);
 
-        // TODO: Store VCN configuration, trigger startup as necessary
+            // Must be done synchronously to ensure that writes do not happen out-of-order.
+            writeConfigsToDiskLocked();
+        }
+
+        // TODO: Clear Binder calling identity
+        // TODO: Trigger startup as necessary
     }
 
     /**
@@ -233,9 +311,38 @@
 
         enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
 
-        // TODO: Clear Binder calling identity
+        synchronized (mLock) {
+            mConfigs.remove(subscriptionGroup);
 
-        // TODO: Clear VCN configuration, trigger teardown as necessary
+            // Must be done synchronously to ensure that writes do not happen out-of-order.
+            writeConfigsToDiskLocked();
+        }
+
+        // TODO: Clear Binder calling identity
+        // TODO: Trigger teardown as necessary
+    }
+
+    @GuardedBy("mLock")
+    private void writeConfigsToDiskLocked() {
+        try {
+            PersistableBundle bundle =
+                    PersistableBundleUtils.fromMap(
+                            mConfigs,
+                            PersistableBundleUtils::fromParcelUuid,
+                            VcnConfig::toPersistableBundle);
+            mConfigDiskRwHelper.writeToDisk(bundle);
+        } catch (IOException e) {
+            Slog.e(TAG, "Failed to save configs to disk", e);
+            throw new ServiceSpecificException(0, "Failed to save configs");
+        }
+    }
+
+    /** Get current configuration list for testing purposes */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    Map<ParcelUuid, VcnConfig> getConfigs() {
+        synchronized (mLock) {
+            return Collections.unmodifiableMap(mConfigs);
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/vcn/Android.bp b/services/core/java/com/android/server/vcn/Android.bp
new file mode 100644
index 0000000..5ed204f
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/Android.bp
@@ -0,0 +1,4 @@
+filegroup {
+    name: "framework-vcn-util-sources",
+    srcs: ["util/**/*.java"],
+}
\ No newline at end of file
diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
new file mode 100644
index 0000000..77944de
--- /dev/null
+++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 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 android.net.vcn;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VcnConfigTest {
+    private static final Set<VcnGatewayConnectionConfig> GATEWAY_CONNECTION_CONFIGS =
+            Collections.singleton(VcnGatewayConnectionConfigTest.buildTestConfig());
+
+    // Public visibility for VcnManagementServiceTest
+    public static VcnConfig buildTestConfig() {
+        VcnConfig.Builder builder = new VcnConfig.Builder();
+
+        for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) {
+            builder.addGatewayConnectionConfig(gatewayConnectionConfig);
+        }
+
+        return builder.build();
+    }
+
+    @Test
+    public void testBuilderRequiresGatewayConnectionConfig() {
+        try {
+            new VcnConfig.Builder().build();
+            fail("Expected exception due to no VcnGatewayConnectionConfigs provided");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testBuilderAndGetters() {
+        final VcnConfig config = buildTestConfig();
+
+        assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs());
+    }
+
+    @Test
+    public void testPersistableBundle() {
+        final VcnConfig config = buildTestConfig();
+
+        assertEquals(config, new VcnConfig(config.toPersistableBundle()));
+    }
+
+    @Test
+    public void testParceling() {
+        final VcnConfig config = buildTestConfig();
+
+        Parcel parcel = Parcel.obtain();
+        config.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        assertEquals(config, VcnConfig.CREATOR.createFromParcel(parcel));
+    }
+}
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
new file mode 100644
index 0000000..e98b6ef
--- /dev/null
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 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 android.net.vcn;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.net.NetworkCapabilities;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VcnGatewayConnectionConfigTest {
+    private static final int[] EXPOSED_CAPS =
+            new int[] {
+                NetworkCapabilities.NET_CAPABILITY_INTERNET, NetworkCapabilities.NET_CAPABILITY_MMS
+            };
+    private static final int[] UNDERLYING_CAPS = new int[] {NetworkCapabilities.NET_CAPABILITY_DUN};
+    private static final long[] RETRY_INTERVALS_MS =
+            new long[] {
+                TimeUnit.SECONDS.toMillis(5),
+                TimeUnit.SECONDS.toMillis(30),
+                TimeUnit.MINUTES.toMillis(1),
+                TimeUnit.MINUTES.toMillis(5),
+                TimeUnit.MINUTES.toMillis(15),
+                TimeUnit.MINUTES.toMillis(30)
+            };
+    private static final int MAX_MTU = 1360;
+
+    // Package protected for use in VcnConfigTest
+    static VcnGatewayConnectionConfig buildTestConfig() {
+        final VcnGatewayConnectionConfig.Builder builder =
+                new VcnGatewayConnectionConfig.Builder()
+                        .setRetryInterval(RETRY_INTERVALS_MS)
+                        .setMaxMtu(MAX_MTU);
+
+        for (int caps : EXPOSED_CAPS) {
+            builder.addExposedCapability(caps);
+        }
+
+        for (int caps : UNDERLYING_CAPS) {
+            builder.addRequiredUnderlyingCapability(caps);
+        }
+
+        return builder.build();
+    }
+
+    @Test
+    public void testBuilderRequiresNonEmptyExposedCaps() {
+        try {
+            new VcnGatewayConnectionConfig.Builder()
+                    .addRequiredUnderlyingCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .build();
+
+            fail("Expected exception due to invalid exposed capabilities");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testBuilderRequiresNonEmptyUnderlyingCaps() {
+        try {
+            new VcnGatewayConnectionConfig.Builder()
+                    .addExposedCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                    .build();
+
+            fail("Expected exception due to invalid required underlying capabilities");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testBuilderRequiresNonNullRetryInterval() {
+        try {
+            new VcnGatewayConnectionConfig.Builder().setRetryInterval(null);
+            fail("Expected exception due to invalid retryIntervalMs");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testBuilderRequiresNonEmptyRetryInterval() {
+        try {
+            new VcnGatewayConnectionConfig.Builder().setRetryInterval(new long[0]);
+            fail("Expected exception due to invalid retryIntervalMs");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testBuilderRequiresValidMtu() {
+        try {
+            new VcnGatewayConnectionConfig.Builder()
+                    .setMaxMtu(VcnGatewayConnectionConfig.MIN_MTU_V6 - 1);
+            fail("Expected exception due to invalid mtu");
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Test
+    public void testBuilderAndGetters() {
+        final VcnGatewayConnectionConfig config = buildTestConfig();
+
+        for (int cap : EXPOSED_CAPS) {
+            config.hasExposedCapability(cap);
+        }
+        for (int cap : UNDERLYING_CAPS) {
+            config.requiresUnderlyingCapability(cap);
+        }
+
+        assertArrayEquals(RETRY_INTERVALS_MS, config.getRetryIntervalsMs());
+        assertEquals(MAX_MTU, config.getMaxMtu());
+    }
+
+    @Test
+    public void testPersistableBundle() {
+        final VcnGatewayConnectionConfig config = buildTestConfig();
+
+        assertEquals(config, new VcnGatewayConnectionConfig(config.toPersistableBundle()));
+    }
+}
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 633cf64..1cc9532 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
@@ -26,7 +29,9 @@
 import android.content.Context;
 import android.net.ConnectivityManager;
 import android.net.vcn.VcnConfig;
+import android.net.vcn.VcnConfigTest;
 import android.os.ParcelUuid;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.test.TestLooper;
@@ -37,10 +42,14 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.vcn.util.PersistableBundleUtils;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.FileNotFoundException;
 import java.util.Collections;
+import java.util.Map;
 import java.util.UUID;
 
 /** Tests for {@link VcnManagementService}. */
@@ -48,6 +57,11 @@
 @SmallTest
 public class VcnManagementServiceTest {
     private static final ParcelUuid TEST_UUID_1 = new ParcelUuid(new UUID(0, 0));
+    private static final ParcelUuid TEST_UUID_2 = new ParcelUuid(new UUID(1, 1));
+    private static final VcnConfig TEST_VCN_CONFIG = VcnConfigTest.buildTestConfig();
+    private static final Map<ParcelUuid, VcnConfig> TEST_VCN_CONFIG_MAP =
+            Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG));
+
     private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO =
             new SubscriptionInfo(
                     1 /* id */,
@@ -79,6 +93,8 @@
     private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
     private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
     private final VcnManagementService mVcnMgmtSvc;
+    private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper =
+            mock(PersistableBundleUtils.LockingReadWriteHelper.class);
 
     public VcnManagementServiceTest() throws Exception {
         setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
@@ -88,6 +104,16 @@
 
         doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper();
         doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid();
+        doReturn(mConfigReadWriteHelper)
+                .when(mMockDeps)
+                .newPersistableBundleLockingReadWriteHelper(any());
+
+        final PersistableBundle bundle =
+                PersistableBundleUtils.fromMap(
+                        TEST_VCN_CONFIG_MAP,
+                        PersistableBundleUtils::fromParcelUuid,
+                        VcnConfig::toPersistableBundle);
+        doReturn(bundle).when(mConfigReadWriteHelper).readFromDisk();
 
         setupMockedCarrierPrivilege(true);
         mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps);
@@ -116,11 +142,41 @@
     }
 
     @Test
+    public void testNonSystemServerRealConfigFileAccessPermission() throws Exception {
+        // Attempt to build a real instance of the dependencies, and verify we cannot write to the
+        // file.
+        VcnManagementService.Dependencies deps = new VcnManagementService.Dependencies();
+        PersistableBundleUtils.LockingReadWriteHelper configReadWriteHelper =
+                deps.newPersistableBundleLockingReadWriteHelper(
+                        VcnManagementService.VCN_CONFIG_FILE);
+
+        // Even tests should not be able to read/write configs from disk; SELinux policies restrict
+        // it to only the system server.
+        // Reading config should always return null since the file "does not exist", and writing
+        // should throw an IOException.
+        assertNull(configReadWriteHelper.readFromDisk());
+
+        try {
+            configReadWriteHelper.writeToDisk(new PersistableBundle());
+            fail("Expected IOException due to SELinux policy");
+        } catch (FileNotFoundException expected) {
+        }
+    }
+
+    @Test
+    public void testLoadVcnConfigsOnStartup() throws Exception {
+        mTestLooper.dispatchAll();
+
+        assertEquals(TEST_VCN_CONFIG_MAP, mVcnMgmtSvc.getConfigs());
+        verify(mConfigReadWriteHelper).readFromDisk();
+    }
+
+    @Test
     public void testSetVcnConfigRequiresNonSystemServer() throws Exception {
         doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
 
         try {
-            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig());
             fail("Expected IllegalStateException exception for system server");
         } catch (IllegalStateException expected) {
         }
@@ -133,7 +189,7 @@
                 .getBinderCallingUid();
 
         try {
-            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig());
             fail("Expected security exception for non system user");
         } catch (SecurityException expected) {
         }
@@ -144,13 +200,21 @@
         setupMockedCarrierPrivilege(false);
 
         try {
-            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build());
+            mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig());
             fail("Expected security exception for missing carrier privileges");
         } catch (SecurityException expected) {
         }
     }
 
     @Test
+    public void testSetVcnConfig() throws Exception {
+        // Use a different UUID to simulate a new VCN config.
+        mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG);
+        assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2));
+        verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
+    }
+
+    @Test
     public void testClearVcnConfigRequiresNonSystemServer() throws Exception {
         doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
 
@@ -184,4 +248,11 @@
         } catch (SecurityException expected) {
         }
     }
+
+    @Test
+    public void testClearVcnConfig() throws Exception {
+        mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
+        assertTrue(mVcnMgmtSvc.getConfigs().isEmpty());
+        verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
+    }
 }