Merge "Adding per-app support for app add/remove"
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 3f11b72..e6624cd 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11988,7 +11988,7 @@
     method @NonNull public byte[] getEncodedMessage();
     method @NonNull public String getHeaderSection();
     method @NonNull public String getStartLine();
-    method @Nullable public String getViaBranchParameter();
+    method @NonNull public String getViaBranchParameter();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.SipMessage> CREATOR;
   }
diff --git a/core/java/android/net/INetworkPolicyManager.aidl b/core/java/android/net/INetworkPolicyManager.aidl
index 171c6a2..f50aa99 100644
--- a/core/java/android/net/INetworkPolicyManager.aidl
+++ b/core/java/android/net/INetworkPolicyManager.aidl
@@ -78,5 +78,4 @@
 
     boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork);
     boolean isUidRestrictedOnMeteredNetworks(int uid);
-    boolean checkUidNetworkingBlocked(int uid, int uidRules, boolean isNetworkMetered, boolean isBackgroundRestricted);
 }
diff --git a/core/java/android/net/NetworkIdentity.java b/core/java/android/net/NetworkIdentity.java
index b037261..1eef7d9 100644
--- a/core/java/android/net/NetworkIdentity.java
+++ b/core/java/android/net/NetworkIdentity.java
@@ -198,15 +198,11 @@
         final int oemManaged = getOemBitfield(snapshot.networkCapabilities);
 
         if (legacyType == TYPE_WIFI) {
-            if (snapshot.networkCapabilities.getSsid() != null) {
-                networkId = snapshot.networkCapabilities.getSsid();
-                if (networkId == null) {
-                    // TODO: Figure out if this code path never runs. If so, remove them.
-                    final WifiManager wifi = (WifiManager) context.getSystemService(
-                            Context.WIFI_SERVICE);
-                    final WifiInfo info = wifi.getConnectionInfo();
-                    networkId = info != null ? info.getSSID() : null;
-                }
+            networkId = snapshot.networkCapabilities.getSsid();
+            if (networkId == null) {
+                final WifiManager wifi = context.getSystemService(WifiManager.class);
+                final WifiInfo info = wifi.getConnectionInfo();
+                networkId = info != null ? info.getSSID() : null;
             }
         }
 
diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java
index 68606ec..da3febd 100644
--- a/core/java/android/net/NetworkPolicyManager.java
+++ b/core/java/android/net/NetworkPolicyManager.java
@@ -564,31 +564,6 @@
     }
 
     /**
-     * Figure out if networking is blocked for a given set of conditions.
-     *
-     * This is used by ConnectivityService via passing stale copies of conditions, so it must not
-     * take any locks.
-     *
-     * @param uid The target uid.
-     * @param uidRules The uid rules which are obtained from NetworkPolicyManagerService.
-     * @param isNetworkMetered True if the network is metered.
-     * @param isBackgroundRestricted True if data saver is enabled.
-     *
-     * @return true if networking is blocked for the UID under the specified conditions.
-     *
-     * @hide
-     */
-    public boolean checkUidNetworkingBlocked(int uid, int uidRules,
-            boolean isNetworkMetered, boolean isBackgroundRestricted) {
-        try {
-            return mService.checkUidNetworkingBlocked(uid, uidRules, isNetworkMetered,
-                    isBackgroundRestricted);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
-    }
-
-    /**
      * Check that the given uid is restricted from doing networking on metered networks.
      *
      * @param uid The target uid.
diff --git a/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java
index c0e0a24..4a24b42 100644
--- a/graphics/java/android/graphics/RecordingCanvas.java
+++ b/graphics/java/android/graphics/RecordingCanvas.java
@@ -21,6 +21,7 @@
 import android.util.Pools.SynchronizedPool;
 import android.view.DisplayListCanvas;
 import android.view.TextureLayer;
+import android.os.SystemProperties;
 
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
@@ -41,7 +42,14 @@
     private static final int POOL_LIMIT = 25;
 
     /** @hide */
-    public static final int MAX_BITMAP_SIZE = 100 * 1024 * 1024; // 100 MB
+    private static int getPanelFrameSize() {
+        final int DefaultSize = 100 * 1024 * 1024; // 100 MB;
+        return Math.max(SystemProperties.getInt("ro.hwui.max_texture_allocation_size", DefaultSize),
+                DefaultSize);
+    }
+
+    /** @hide */
+    public static final int MAX_BITMAP_SIZE = getPanelFrameSize();
 
     private static final SynchronizedPool<RecordingCanvas> sPool =
             new SynchronizedPool<>(POOL_LIMIT);
diff --git a/keystore/java/android/security/keystore/AttestationUtils.java b/keystore/java/android/security/keystore/AttestationUtils.java
index 1eb8541..be865a0 100644
--- a/keystore/java/android/security/keystore/AttestationUtils.java
+++ b/keystore/java/android/security/keystore/AttestationUtils.java
@@ -36,9 +36,11 @@
 import java.security.KeyPairGenerator;
 import java.security.KeyStore;
 import java.security.SecureRandom;
+import java.security.cert.Certificate;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.ECGenParameterSpec;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Random;
 import java.util.Set;
@@ -281,8 +283,9 @@
             KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
             keyStore.load(null);
 
+            Certificate[] certs = keyStore.getCertificateChain(keystoreAlias);
             X509Certificate[] certificateChain =
-                    (X509Certificate[]) keyStore.getCertificateChain(keystoreAlias);
+                Arrays.copyOf(certs, certs.length, X509Certificate[].class);
 
             keyStore.deleteEntry(keystoreAlias);
 
diff --git a/packages/Connectivity/framework/aidl-export/android/net/IpPrefix.aidl b/packages/Connectivity/framework/aidl-export/android/net/IpPrefix.aidl
index 0d70f2a..3495efc 100644
--- a/packages/Connectivity/framework/aidl-export/android/net/IpPrefix.aidl
+++ b/packages/Connectivity/framework/aidl-export/android/net/IpPrefix.aidl
@@ -18,5 +18,5 @@
 package android.net;
 
 // @JavaOnlyStableParcelable only affects the parcelable when built as stable aidl (aidl_interface
-// build rule). IpPrefix is also used in cpp but only as non-stable aidl.
-@JavaOnlyStableParcelable parcelable IpPrefix cpp_header "binder/IpPrefix.h";
+// build rule).
+@JavaOnlyStableParcelable parcelable IpPrefix;
diff --git a/packages/WAPPushManager/AndroidManifest.xml b/packages/WAPPushManager/AndroidManifest.xml
index a75fb2d..15f01e6 100644
--- a/packages/WAPPushManager/AndroidManifest.xml
+++ b/packages/WAPPushManager/AndroidManifest.xml
@@ -27,7 +27,8 @@
 
     <original-package android:name="com.android.smspush" />
     <application
-        android:allowClearUserData="false">
+        android:allowClearUserData="false"
+        android:directBootAware="true">
         <service android:name=".WapPushManager"
             android:permission="com.android.smspush.WAPPUSH_MANAGER_BIND"
             android:exported="true">
diff --git a/packages/WAPPushManager/src/com/android/smspush/WapPushManager.java b/packages/WAPPushManager/src/com/android/smspush/WapPushManager.java
index dc2707b..951e64f 100755
--- a/packages/WAPPushManager/src/com/android/smspush/WapPushManager.java
+++ b/packages/WAPPushManager/src/com/android/smspush/WapPushManager.java
@@ -26,17 +26,21 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.Cursor;
-import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.IWapPushManager;
 import com.android.internal.telephony.WapPushManagerParams;
 
+import java.io.File;
+
 /**
  * The WapPushManager service is implemented to process incoming
  * WAP Push messages and to maintain the Receiver Application/Application
@@ -67,8 +71,13 @@
     /**
      * Inner class that deals with application ID table
      */
-    private class WapPushManDBHelper extends SQLiteOpenHelper {
-        WapPushManDBHelper(Context context) {
+    @VisibleForTesting
+    public static class WapPushManDBHelper extends SQLiteOpenHelper {
+        /**
+         * Constructor
+         */
+        @VisibleForTesting
+        public WapPushManDBHelper(Context context) {
             super(context, DATABASE_NAME, null, WAP_PUSH_MANAGER_VERSION);
             if (LOCAL_LOGV) Log.v(LOG_TAG, "helper instance created.");
         }
@@ -269,10 +278,6 @@
                 int app_type, boolean need_signature, boolean further_processing) {
             WapPushManDBHelper dbh = getDatabase(mContext);
             SQLiteDatabase db = dbh.getWritableDatabase();
-            WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, x_app_id, content_type);
-            boolean ret = false;
-            boolean insert = false;
-            int sq = 0;
 
             if (!appTypeCheck(app_type)) {
                 Log.w(LOG_TAG, "invalid app_type " + app_type + ". app_type must be "
@@ -280,34 +285,8 @@
                         + WapPushManagerParams.APP_TYPE_SERVICE);
                 return false;
             }
-
-            if (lastapp == null) {
-                insert = true;
-                sq = 0;
-            } else if (!lastapp.packageName.equals(package_name) ||
-                    !lastapp.className.equals(class_name)) {
-                insert = true;
-                sq = lastapp.installOrder + 1;
-            }
-
-            if (insert) {
-                ContentValues values = new ContentValues();
-
-                values.put("x_wap_application", x_app_id);
-                values.put("content_type", content_type);
-                values.put("package_name", package_name);
-                values.put("class_name", class_name);
-                values.put("app_type", app_type);
-                values.put("need_signature", need_signature ? 1 : 0);
-                values.put("further_processing", further_processing ? 1 : 0);
-                values.put("install_order", sq);
-                db.insert(APPID_TABLE_NAME, null, values);
-                if (LOCAL_LOGV) Log.v(LOG_TAG, "add:" + x_app_id + ":" + content_type
-                        + " " + package_name + "." + class_name
-                        + ", newsq:" + sq);
-                ret = true;
-            }
-
+            boolean ret = insertPackage(dbh, db, x_app_id, content_type, package_name, class_name,
+                    app_type, need_signature, further_processing);
             db.close();
 
             return ret;
@@ -404,11 +383,91 @@
     protected WapPushManDBHelper getDatabase(Context context) {
         if (mDbHelper == null) {
             if (LOCAL_LOGV) Log.v(LOG_TAG, "create new db inst.");
-            mDbHelper = new WapPushManDBHelper(context);
+            mDbHelper = new WapPushManDBHelper(context.createDeviceProtectedStorageContext());
         }
+        // Migrate existing legacy database into the device encrypted storage.
+        migrateWapPushManDBIfNeeded(context);
         return mDbHelper;
     }
 
+    /**
+     * Inserts a package information into a database
+     */
+    @VisibleForTesting
+    public boolean insertPackage(WapPushManDBHelper dbh, SQLiteDatabase db, String appId,
+            String contentType, String packageName, String className, int appType,
+            boolean needSignature, boolean furtherProcessing) {
+
+        WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, appId, contentType);
+        boolean insert = false;
+        int sq = 0;
+
+        if (lastapp == null) {
+            insert = true;
+            sq = 0;
+        } else if (!lastapp.packageName.equals(packageName)
+                || !lastapp.className.equals(className)) {
+            insert = true;
+            sq = lastapp.installOrder + 1;
+        }
+
+        if (insert) {
+            ContentValues values = new ContentValues();
+
+            values.put("x_wap_application", appId);
+            values.put("content_type", contentType);
+            values.put("package_name", packageName);
+            values.put("class_name", className);
+            values.put("app_type", appType);
+            values.put("need_signature", needSignature ? 1 : 0);
+            values.put("further_processing", furtherProcessing ? 1 : 0);
+            values.put("install_order", sq);
+            db.insert(APPID_TABLE_NAME, null, values);
+            if (LOCAL_LOGV) {
+                Log.v(LOG_TAG, "add:" + appId + ":" + contentType + " " + packageName
+                        + "." + className + ", newsq:" + sq);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Migrates a legacy database into the device encrypted storage
+     */
+    private void migrateWapPushManDBIfNeeded(Context context) {
+        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        File file = context.getDatabasePath(DATABASE_NAME);
+        if (!userManager.isUserUnlocked() || !file.exists()) {
+            // Check if the device is unlocked because a legacy database can't access during
+            // DirectBoot.
+            return;
+        }
+
+        // Migration steps below:
+        // 1. Merge the package info to legacy database if there is any package info which is
+        // registered during DirectBoot.
+        // 2. Move the data base to the device encryped storage.
+        WapPushManDBHelper legacyDbHelper = new WapPushManDBHelper(context);
+        SQLiteDatabase legacyDb = legacyDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        Cursor cur = db.query(APPID_TABLE_NAME, null, null, null, null, null, null);
+        while (cur.moveToNext()) {
+            insertPackage(legacyDbHelper, legacyDb,
+                    cur.getString(cur.getColumnIndex("x_wap_application")),
+                    cur.getString(cur.getColumnIndex("content_type")),
+                    cur.getString(cur.getColumnIndex("package_name")),
+                    cur.getString(cur.getColumnIndex("class_name")),
+                    cur.getInt(cur.getColumnIndex("app_type")),
+                    cur.getInt(cur.getColumnIndex("need_signature")) == 1,
+                    cur.getInt(cur.getColumnIndex("further_processing")) == 1);
+        }
+        cur.close();
+        legacyDb.close();
+        db.close();
+        context.createDeviceProtectedStorageContext().moveDatabaseFrom(context, DATABASE_NAME);
+        Log.i(LOG_TAG, "Migrated the legacy database.");
+    }
 
     /**
      * This method is used for testing
diff --git a/packages/WAPPushManager/tests/src/com/android/smspush/unitTests/WapPushTest.java b/packages/WAPPushManager/tests/src/com/android/smspush/unitTests/WapPushTest.java
index f7afc57..b9dac4e 100644
--- a/packages/WAPPushManager/tests/src/com/android/smspush/unitTests/WapPushTest.java
+++ b/packages/WAPPushManager/tests/src/com/android/smspush/unitTests/WapPushTest.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.database.sqlite.SQLiteDatabase;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.provider.Telephony.Sms.Intents;
@@ -33,7 +34,9 @@
 import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.util.HexDump;
 import com.android.smspush.WapPushManager;
+import com.android.smspush.WapPushManager.WapPushManDBHelper;
 
+import java.io.File;
 import java.util.Random;
 
 /**
@@ -467,8 +470,9 @@
         try {
             super.setUp();
             // get verifier
-            getContext().bindService(new Intent(IDataVerify.class.getName()),
-                    mConn, Context.BIND_AUTO_CREATE);
+            Intent intent = new Intent(IDataVerify.class.getName());
+            intent.setPackage("com.android.smspush.unitTests");
+            getContext().bindService(intent, mConn, Context.BIND_AUTO_CREATE);
         } catch (Exception e) {
             Log.w(LOG_TAG, "super exception");
         }
@@ -552,15 +556,15 @@
     }
 
     /**
-     * Add sqlite injection test
+     * Sqlite injection test
      */
-    public void testAddPackage0() {
+    public void testSqliteInjection() {
         String inject = "' union select 0,'com.android.settings','com.android.settings.Settings',0,0,0--";
 
-        // insert new data
+        // update data
         IWapPushManager iwapman = getInterface();
         try {
-            assertFalse(iwapman.addPackage(
+            assertFalse(iwapman.updatePackage(
                     inject,
                     Integer.toString(mContentTypeValue),
                     mPackageName, mClassName,
@@ -2528,4 +2532,45 @@
         mMessageBody = originalMessageBody;
     }
 
+    /**
+     * DataBase migration test.
+     */
+    public void testDataBaseMigration() {
+        IWapPushManager iwapman = getInterface();
+        WapPushManager wpman = getService();
+        Context context = getContext();
+
+        addPackageToLegacyDB(mAppIdValue, mContentTypeValue, mPackageName, mClassName,
+                WapPushManagerParams.APP_TYPE_SERVICE, true, true);
+        addPackageToLegacyDB(mAppIdValue + 10, mContentTypeValue, mPackageName, mClassName,
+                WapPushManagerParams.APP_TYPE_SERVICE, true, true);
+
+        File oldDbFile = context.getDatabasePath("wappush.db");
+        assertTrue(oldDbFile.exists());
+        assertTrue(wpman.verifyData(Integer.toString(mAppIdValue),
+                Integer.toString(mContentTypeValue),
+                mPackageName, mClassName,
+                WapPushManagerParams.APP_TYPE_SERVICE, true, true));
+        assertFalse(oldDbFile.exists());
+
+        // Clean up DB
+        try {
+            iwapman.deletePackage(Integer.toString(mAppIdValue),
+                    Integer.toString(mContentTypeValue), mPackageName, mClassName);
+            iwapman.deletePackage(Integer.toString(mAppIdValue + 10),
+                    Integer.toString(mContentTypeValue), mPackageName, mClassName);
+        } catch (RemoteException e) {
+            assertTrue(false);
+        }
+    }
+
+    private void addPackageToLegacyDB(int appId, int contextType, String packagename,
+            String classnName, int appType, boolean signature, boolean furtherProcessing) {
+        WapPushManager wpman = getService();
+        WapPushManDBHelper dbh = new WapPushManDBHelper(getContext());
+        SQLiteDatabase db = dbh.getWritableDatabase();
+
+        wpman.insertPackage(dbh, db, Integer.toString(appId), Integer.toString(contextType),
+                packagename, classnName, appType, signature, furtherProcessing);
+    }
 }
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 8958909..a6f1a72 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -6677,7 +6677,7 @@
                     mDeps.reportNetworkInterfaceForTransports(mContext, iface,
                             caps.getTransportTypes());
                 } catch (Exception e) {
-                    loge("Exception adding interface: " + e);
+                    logw("Exception adding interface: " + e);
                 }
             }
         }
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 4fd9373..f7ae58c 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_NOT_CONFIGURED;
@@ -35,7 +37,9 @@
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
 import android.net.LinkProperties;
+import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.net.vcn.IVcnManagementService;
 import android.net.vcn.IVcnStatusCallback;
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
@@ -161,6 +165,9 @@
     @NonNull private final VcnContext mVcnContext;
     @NonNull private final BroadcastReceiver mPkgChangeReceiver;
 
+    @NonNull
+    private final TrackingNetworkCallback mTrackingNetworkCallback = new TrackingNetworkCallback();
+
     /** Can only be assigned when {@link #systemReady()} is called, since it uses AppOpsManager. */
     @Nullable private LocationPermissionChecker mLocationPermissionChecker;
 
@@ -357,6 +364,10 @@
     public void systemReady() {
         mContext.getSystemService(ConnectivityManager.class)
                 .registerNetworkProvider(mNetworkProvider);
+        mContext.getSystemService(ConnectivityManager.class)
+                .registerNetworkCallback(
+                        new NetworkRequest.Builder().clearCapabilities().build(),
+                        mTrackingNetworkCallback);
         mTelephonySubscriptionTracker.register();
         mLocationPermissionChecker = mDeps.newLocationPermissionChecker(mVcnContext.getContext());
     }
@@ -531,16 +542,7 @@
 
         if (mVcns.containsKey(subscriptionGroup)) {
             final Vcn vcn = mVcns.get(subscriptionGroup);
-            final int status = vcn.getStatus();
             vcn.updateConfig(config);
-
-            // TODO(b/183174340): Remove this once opportunistic-safe-mode is supported
-            // Only notify VcnStatusCallbacks if this VCN was previously in Safe Mode
-            if (status == VCN_STATUS_CODE_SAFE_MODE) {
-                // TODO(b/181789060): invoke asynchronously after Vcn notifies through VcnCallback
-                notifyAllPermissionedStatusCallbacksLocked(
-                        subscriptionGroup, VCN_STATUS_CODE_ACTIVE);
-            }
         } else {
             startVcnLocked(subscriptionGroup, config);
         }
@@ -792,8 +794,9 @@
                         NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
             }
 
+            final NetworkCapabilities result = ncBuilder.build();
             return new VcnUnderlyingNetworkPolicy(
-                    false /* isTearDownRequested */, ncBuilder.build());
+                    mTrackingNetworkCallback.requiresRestartForCarrierWifi(result), result);
         });
     }
 
@@ -929,8 +932,8 @@
     // TODO(b/180452282): Make name more generic and implement directly with VcnManagementService
     /** Callback for Vcn signals sent up to VcnManagementService. */
     public interface VcnCallback {
-        /** Called by a Vcn to signal that it has entered safe mode. */
-        void onEnteredSafeMode();
+        /** Called by a Vcn to signal that its safe mode status has changed. */
+        void onSafeModeStatusChanged(boolean isInSafeMode);
 
         /** Called by a Vcn to signal that an error occurred. */
         void onGatewayConnectionError(
@@ -940,6 +943,49 @@
                 @Nullable String exceptionMessage);
     }
 
+    /**
+     * TrackingNetworkCallback tracks all active networks
+     *
+     * <p>This is used to ensure that no underlying networks have immutable capabilities changed
+     * without requiring a Network restart.
+     */
+    private class TrackingNetworkCallback extends ConnectivityManager.NetworkCallback {
+        private final Map<Network, NetworkCapabilities> mCaps = new ArrayMap<>();
+
+        @Override
+        public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) {
+            synchronized (mCaps) {
+                mCaps.put(network, caps);
+            }
+        }
+
+        @Override
+        public void onLost(Network network) {
+            synchronized (mCaps) {
+                mCaps.remove(network);
+            }
+        }
+
+        private boolean requiresRestartForCarrierWifi(NetworkCapabilities caps) {
+            if (!caps.hasTransport(TRANSPORT_WIFI) || caps.getSubIds() == null) {
+                return false;
+            }
+
+            synchronized (mCaps) {
+                for (NetworkCapabilities existing : mCaps.values()) {
+                    if (existing.hasTransport(TRANSPORT_WIFI)
+                            && caps.getSubIds().equals(existing.getSubIds())) {
+                        // Restart if any immutable capabilities have changed
+                        return existing.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                                != caps.hasCapability(NET_CAPABILITY_NOT_RESTRICTED);
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+
     /** VcnCallbackImpl for Vcn signals sent up to VcnManagementService. */
     private class VcnCallbackImpl implements VcnCallback {
         @NonNull private final ParcelUuid mSubGroup;
@@ -949,15 +995,18 @@
         }
 
         @Override
-        public void onEnteredSafeMode() {
+        public void onSafeModeStatusChanged(boolean isInSafeMode) {
             synchronized (mLock) {
                 // Ignore if this subscription group doesn't exist anymore
                 if (!mVcns.containsKey(mSubGroup)) {
                     return;
                 }
 
+                final int status =
+                        isInSafeMode ? VCN_STATUS_CODE_SAFE_MODE : VCN_STATUS_CODE_ACTIVE;
+
                 notifyAllPolicyListenersLocked();
-                notifyAllPermissionedStatusCallbacksLocked(mSubGroup, VCN_STATUS_CODE_SAFE_MODE);
+                notifyAllPermissionedStatusCallbacksLocked(mSubGroup, status);
             }
         }
 
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index c127858..290307b 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -5581,17 +5581,6 @@
     }
 
     @Override
-    public boolean checkUidNetworkingBlocked(int uid, int uidRules,
-            boolean isNetworkMetered, boolean isBackgroundRestricted) {
-        mContext.enforceCallingOrSelfPermission(OBSERVE_NETWORK_POLICY, TAG);
-        // Log of invoking this function is disabled because it will be called very frequently. And
-        // metrics are unlikely needed on this method because the callers are external and this
-        // method doesn't take any locks or perform expensive operations.
-        return isUidNetworkingBlockedInternal(uid, uidRules, isNetworkMetered,
-                isBackgroundRestricted, null);
-    }
-
-    @Override
     public boolean isUidRestrictedOnMeteredNetworks(int uid) {
         mContext.enforceCallingOrSelfPermission(OBSERVE_NETWORK_POLICY, TAG);
         final int uidRules;
diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java
index 546893588..ae806aa 100644
--- a/services/core/java/com/android/server/vcn/Vcn.java
+++ b/services/core/java/com/android/server/vcn/Vcn.java
@@ -17,6 +17,7 @@
 package com.android.server.vcn;
 
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE;
@@ -96,17 +97,21 @@
      */
     private static final int MSG_EVENT_GATEWAY_CONNECTION_QUIT = MSG_EVENT_BASE + 3;
 
+    /**
+     * Triggers reevaluation of safe mode conditions.
+     *
+     * <p>Upon entering safe mode, the VCN will only provide gateway connections opportunistically,
+     * leaving the underlying networks marked as NOT_VCN_MANAGED.
+     *
+     * <p>Any VcnGatewayConnection in safe mode will result in the entire Vcn instance being put
+     * into safe mode. Upon receiving this message, the Vcn MUST query all VcnGatewayConnections to
+     * determine if any are in safe mode.
+     */
+    private static final int MSG_EVENT_SAFE_MODE_STATE_CHANGED = MSG_EVENT_BASE + 4;
+
     /** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */
     private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE;
 
-    /**
-     * Causes this VCN to immediately enter safe mode.
-     *
-     * <p>Upon entering safe mode, the VCN will unregister its RequestListener, tear down all of its
-     * VcnGatewayConnections, and notify VcnManagementService that it is in safe mode.
-     */
-    private static final int MSG_CMD_ENTER_SAFE_MODE = MSG_CMD_BASE + 1;
-
     @NonNull private final VcnContext mVcnContext;
     @NonNull private final ParcelUuid mSubscriptionGroup;
     @NonNull private final Dependencies mDeps;
@@ -233,6 +238,11 @@
 
     @Override
     public void handleMessage(@NonNull Message msg) {
+        if (mCurrentStatus != VCN_STATUS_CODE_ACTIVE
+                && mCurrentStatus != VCN_STATUS_CODE_SAFE_MODE) {
+            return;
+        }
+
         switch (msg.what) {
             case MSG_EVENT_CONFIG_UPDATED:
                 handleConfigUpdated((VcnConfig) msg.obj);
@@ -246,12 +256,12 @@
             case MSG_EVENT_GATEWAY_CONNECTION_QUIT:
                 handleGatewayConnectionQuit((VcnGatewayConnectionConfig) msg.obj);
                 break;
+            case MSG_EVENT_SAFE_MODE_STATE_CHANGED:
+                handleSafeModeStatusChanged();
+                break;
             case MSG_CMD_TEARDOWN:
                 handleTeardown();
                 break;
-            case MSG_CMD_ENTER_SAFE_MODE:
-                handleEnterSafeMode();
-                break;
             default:
                 Slog.wtf(getLogTag(), "Unknown msg.what: " + msg.what);
         }
@@ -263,40 +273,28 @@
 
         mConfig = config;
 
-        // TODO(b/183174340): Remove this once opportunistic safe mode is supported.
-        if (mCurrentStatus == VCN_STATUS_CODE_ACTIVE) {
-            // VCN is already active - teardown any GatewayConnections whose configs have been
-            // removed and get all current requests
-            for (final Entry<VcnGatewayConnectionConfig, VcnGatewayConnection> entry :
-                    mVcnGatewayConnections.entrySet()) {
-                final VcnGatewayConnectionConfig gatewayConnectionConfig = entry.getKey();
-                final VcnGatewayConnection gatewayConnection = entry.getValue();
+        // Teardown any GatewayConnections whose configs have been removed and get all current
+        // requests
+        for (final Entry<VcnGatewayConnectionConfig, VcnGatewayConnection> entry :
+                mVcnGatewayConnections.entrySet()) {
+            final VcnGatewayConnectionConfig gatewayConnectionConfig = entry.getKey();
+            final VcnGatewayConnection gatewayConnection = entry.getValue();
 
-                // GatewayConnectionConfigs must match exactly (otherwise authentication or
-                // connection details may have changed).
-                if (!mConfig.getGatewayConnectionConfigs().contains(gatewayConnectionConfig)) {
-                    if (gatewayConnection == null) {
-                        Slog.wtf(
-                                getLogTag(),
-                                "Found gatewayConnectionConfig without GatewayConnection");
-                    } else {
-                        gatewayConnection.teardownAsynchronously();
-                    }
+            // GatewayConnectionConfigs must match exactly (otherwise authentication or
+            // connection details may have changed).
+            if (!mConfig.getGatewayConnectionConfigs().contains(gatewayConnectionConfig)) {
+                if (gatewayConnection == null) {
+                    Slog.wtf(
+                            getLogTag(), "Found gatewayConnectionConfig without GatewayConnection");
+                } else {
+                    gatewayConnection.teardownAsynchronously();
                 }
             }
-
-            // Trigger a re-evaluation of all NetworkRequests (to make sure any that can be
-            // satisfied start a new GatewayConnection)
-            mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener);
-        } else if (mCurrentStatus == VCN_STATUS_CODE_SAFE_MODE) {
-            // If this VCN was not previously active, it is exiting Safe Mode. Re-register the
-            // request listener to get NetworkRequests again (and all cached requests).
-            mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener);
-        } else {
-            // Ignored; VCN was not active; config updates ignored.
-            return;
         }
-        mCurrentStatus = VCN_STATUS_CODE_ACTIVE;
+
+        // Trigger a re-evaluation of all NetworkRequests (to make sure any that can be
+        // satisfied start a new GatewayConnection)
+        mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener);
     }
 
     private void handleTeardown() {
@@ -309,21 +307,27 @@
         mCurrentStatus = VCN_STATUS_CODE_INACTIVE;
     }
 
-    private void handleEnterSafeMode() {
-        // TODO(b/183174340): Remove this once opportunistic-safe-mode is supported
-        handleTeardown();
+    private void handleSafeModeStatusChanged() {
+        boolean hasSafeModeGatewayConnection = false;
 
-        mCurrentStatus = VCN_STATUS_CODE_SAFE_MODE;
-        mVcnCallback.onEnteredSafeMode();
+        // If any VcnGatewayConnection is in safe mode, mark the entire VCN as being in safe mode
+        for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) {
+            if (gatewayConnection.isInSafeMode()) {
+                hasSafeModeGatewayConnection = true;
+                break;
+            }
+        }
+
+        final int oldStatus = mCurrentStatus;
+        mCurrentStatus =
+                hasSafeModeGatewayConnection ? VCN_STATUS_CODE_SAFE_MODE : VCN_STATUS_CODE_ACTIVE;
+        if (oldStatus != mCurrentStatus) {
+            mVcnCallback.onSafeModeStatusChanged(hasSafeModeGatewayConnection);
+        }
     }
 
     private void handleNetworkRequested(
             @NonNull NetworkRequest request, int score, int providerId) {
-        if (mCurrentStatus != VCN_STATUS_CODE_ACTIVE) {
-            Slog.v(getLogTag(), "Received NetworkRequest while inactive. Ignore for now");
-            return;
-        }
-
         if (score > getNetworkScore()) {
             if (VDBG) {
                 Slog.v(
@@ -376,25 +380,23 @@
         mVcnGatewayConnections.remove(config);
 
         // Trigger a re-evaluation of all NetworkRequests (to make sure any that can be satisfied
-        // start a new GatewayConnection), but only if the Vcn is still alive
-        if (mCurrentStatus == VCN_STATUS_CODE_ACTIVE) {
-            mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener);
-        }
+        // start a new GatewayConnection). VCN is always alive here, courtesy of the liveness check
+        // in handleMessage()
+        mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener);
     }
 
     private void handleSubscriptionsChanged(@NonNull TelephonySubscriptionSnapshot snapshot) {
         mLastSnapshot = snapshot;
 
-        if (mCurrentStatus == VCN_STATUS_CODE_ACTIVE) {
-            for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) {
-                gatewayConnection.updateSubscriptionSnapshot(mLastSnapshot);
-            }
+        for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) {
+            gatewayConnection.updateSubscriptionSnapshot(mLastSnapshot);
         }
     }
 
     private boolean isRequestSatisfiedByGatewayConnectionConfig(
             @NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) {
         final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
+        builder.addTransportType(TRANSPORT_CELLULAR);
         builder.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
         for (int cap : config.getAllExposedCapabilities()) {
             builder.addCapability(cap);
@@ -418,8 +420,8 @@
     /** Callback used for passing status signals from a VcnGatewayConnection to its managing Vcn. */
     @VisibleForTesting(visibility = Visibility.PACKAGE)
     public interface VcnGatewayStatusCallback {
-        /** Called by a VcnGatewayConnection to indicate that it has entered safe mode. */
-        void onEnteredSafeMode();
+        /** Called by a VcnGatewayConnection to indicate that it's safe mode status has changed. */
+        void onSafeModeStatusChanged();
 
         /** Callback by a VcnGatewayConnection to indicate that an error occurred. */
         void onGatewayConnectionError(
@@ -445,8 +447,8 @@
         }
 
         @Override
-        public void onEnteredSafeMode() {
-            sendMessage(obtainMessage(MSG_CMD_ENTER_SAFE_MODE));
+        public void onSafeModeStatusChanged() {
+            sendMessage(obtainMessage(MSG_EVENT_SAFE_MODE_STATE_CHANGED));
         }
 
         @Override
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 2ba8edd..20c08eb 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -32,6 +32,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.net.ConnectivityManager;
 import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.IpSecManager;
@@ -44,6 +45,7 @@
 import android.net.NetworkAgent;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
 import android.net.RouteInfo;
 import android.net.TelephonyNetworkSpecifier;
 import android.net.Uri;
@@ -92,6 +94,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
 
 /**
  * A single VCN Gateway Connection, providing a single public-facing VCN network.
@@ -504,6 +507,15 @@
     private boolean mIsQuitting = false;
 
     /**
+     * Whether the VcnGatewayConnection is in safe mode.
+     *
+     * <p>Upon hitting the safe mode timeout, this will be set to {@code true}. In safe mode, this
+     * VcnGatewayConnection will continue attempting to connect, and if a successful connection is
+     * made, safe mode will be exited.
+     */
+    private boolean mIsInSafeMode = false;
+
+    /**
      * The token used by the primary/current/active session.
      *
      * <p>This token MUST be updated when a new stateful/async session becomes the
@@ -562,8 +574,7 @@
      * <p>Set in Connected state, always @NonNull in Connected, Migrating states, @Nullable
      * otherwise.
      */
-    @VisibleForTesting(visibility = Visibility.PRIVATE)
-    NetworkAgent mNetworkAgent;
+    private NetworkAgent mNetworkAgent;
 
     @Nullable private WakeupMessage mTeardownTimeoutAlarm;
     @Nullable private WakeupMessage mDisconnectRequestAlarm;
@@ -628,6 +639,14 @@
         start();
     }
 
+    /** Queries whether this VcnGatewayConnection is in safe mode. */
+    public boolean isInSafeMode() {
+        // Accessing internal state; must only be done on looper thread.
+        mVcnContext.ensureRunningOnLooperThread();
+
+        return mIsInSafeMode;
+    }
+
     /**
      * Asynchronously tears down this GatewayConnection, and any resources used.
      *
@@ -1162,6 +1181,15 @@
             }
         }
 
+        protected void handleSafeModeTimeoutExceeded() {
+            mSafeModeTimeoutAlarm = null;
+
+            // Connectivity for this GatewayConnection is broken; tear down the Network.
+            teardownNetwork();
+            mIsInSafeMode = true;
+            mGatewayStatusCallback.onSafeModeStatusChanged();
+        }
+
         protected void logUnexpectedEvent(int what) {
             Slog.d(TAG, String.format(
                     "Unexpected event code %d in state %s", what, this.getClass().getSimpleName()));
@@ -1315,8 +1343,7 @@
                     }
                     break;
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED:
-                    mGatewayStatusCallback.onEnteredSafeMode();
-                    mSafeModeTimeoutAlarm = null;
+                    handleSafeModeTimeoutExceeded();
                     break;
                 default:
                     logUnhandledMessage(msg);
@@ -1401,8 +1428,7 @@
                     handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj);
                     break;
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED:
-                    mGatewayStatusCallback.onEnteredSafeMode();
-                    mSafeModeTimeoutAlarm = null;
+                    handleSafeModeTimeoutExceeded();
                     break;
                 default:
                     logUnhandledMessage(msg);
@@ -1432,30 +1458,35 @@
                     buildNetworkCapabilities(mConnectionConfig, mUnderlying);
             final LinkProperties lp =
                     buildConnectedLinkProperties(mConnectionConfig, tunnelIface, childConfig);
+            final NetworkAgentConfig nac =
+                    new NetworkAgentConfig.Builder()
+                            .setLegacyType(ConnectivityManager.TYPE_MOBILE)
+                            .build();
 
             final NetworkAgent agent =
-                    new NetworkAgent(
-                            mVcnContext.getContext(),
-                            mVcnContext.getLooper(),
+                    mDeps.newNetworkAgent(
+                            mVcnContext,
                             TAG,
                             caps,
                             lp,
                             Vcn.getNetworkScore(),
-                            new NetworkAgentConfig.Builder().build(),
-                            mVcnContext.getVcnNetworkProvider()) {
-                        @Override
-                        public void onNetworkUnwanted() {
-                            Slog.d(TAG, "NetworkAgent was unwanted");
-                            teardownAsynchronously();
-                        }
-
-                        @Override
-                        public void onValidationStatus(int status, @Nullable Uri redirectUri) {
-                            if (status == NetworkAgent.VALIDATION_STATUS_VALID) {
-                                clearFailedAttemptCounterAndSafeModeAlarm();
-                            }
-                        }
-                    };
+                            nac,
+                            mVcnContext.getVcnNetworkProvider(),
+                            () -> {
+                                Slog.d(TAG, "NetworkAgent was unwanted");
+                                // If network agent has already been torn down, skip sending the
+                                // disconnect. Unwanted() is always called, even when networkAgents
+                                // are unregistered in teardownNetwork(), so prevent duplicate
+                                // notifications.
+                                if (mNetworkAgent != null) {
+                                    teardownAsynchronously();
+                                }
+                            } /* networkUnwantedCallback */,
+                            (status) -> {
+                                if (status == NetworkAgent.VALIDATION_STATUS_VALID) {
+                                    clearFailedAttemptCounterAndSafeModeAlarm();
+                                }
+                            } /* validationStatusCallback */);
 
             agent.register();
             agent.markConnected();
@@ -1469,6 +1500,11 @@
             // Validated connection, clear failed attempt counter
             mFailedAttempts = 0;
             cancelSafeModeAlarm();
+
+            if (mIsInSafeMode) {
+                mIsInSafeMode = false;
+                mGatewayStatusCallback.onSafeModeStatusChanged();
+            }
         }
 
         protected void applyTransform(
@@ -1491,13 +1527,6 @@
         protected void setupInterface(
                 int token,
                 @NonNull IpSecTunnelInterface tunnelIface,
-                @NonNull VcnChildSessionConfiguration childConfig) {
-            setupInterface(token, tunnelIface, childConfig, null);
-        }
-
-        protected void setupInterface(
-                int token,
-                @NonNull IpSecTunnelInterface tunnelIface,
                 @NonNull VcnChildSessionConfiguration childConfig,
                 @Nullable VcnChildSessionConfiguration oldChildConfig) {
             try {
@@ -1579,16 +1608,17 @@
                             transformCreatedInfo.direction);
                     break;
                 case EVENT_SETUP_COMPLETED:
+                    final VcnChildSessionConfiguration oldChildConfig = mChildConfig;
                     mChildConfig = ((EventSetupCompletedInfo) msg.obj).childSessionConfig;
 
-                    setupInterfaceAndNetworkAgent(mCurrentToken, mTunnelIface, mChildConfig);
+                    setupInterfaceAndNetworkAgent(
+                            mCurrentToken, mTunnelIface, mChildConfig, oldChildConfig);
                     break;
                 case EVENT_DISCONNECT_REQUESTED:
                     handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj);
                     break;
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED:
-                    mGatewayStatusCallback.onEnteredSafeMode();
-                    mSafeModeTimeoutAlarm = null;
+                    handleSafeModeTimeoutExceeded();
                     break;
                 default:
                     logUnhandledMessage(msg);
@@ -1626,8 +1656,9 @@
         protected void setupInterfaceAndNetworkAgent(
                 int token,
                 @NonNull IpSecTunnelInterface tunnelIface,
-                @NonNull VcnChildSessionConfiguration childConfig) {
-            setupInterface(token, tunnelIface, childConfig);
+                @NonNull VcnChildSessionConfiguration childConfig,
+                @NonNull VcnChildSessionConfiguration oldChildConfig) {
+            setupInterface(token, tunnelIface, childConfig, oldChildConfig);
 
             if (mNetworkAgent == null) {
                 mNetworkAgent = buildNetworkAgent(tunnelIface, childConfig);
@@ -1692,8 +1723,7 @@
                     handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj);
                     break;
                 case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED:
-                    mGatewayStatusCallback.onEnteredSafeMode();
-                    mSafeModeTimeoutAlarm = null;
+                    handleSafeModeTimeoutExceeded();
                     break;
                 default:
                     logUnhandledMessage(msg);
@@ -1935,6 +1965,16 @@
     }
 
     @VisibleForTesting(visibility = Visibility.PRIVATE)
+    NetworkAgent getNetworkAgent() {
+        return mNetworkAgent;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    void setNetworkAgent(@Nullable NetworkAgent networkAgent) {
+        mNetworkAgent = networkAgent;
+    }
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
     void sendDisconnectRequestedAndAcquireWakelock(String reason, boolean shouldQuit) {
         sendMessageAndAcquireWakeLock(
                 EVENT_DISCONNECT_REQUESTED,
@@ -2018,6 +2058,38 @@
             return new WakeupMessage(vcnContext.getContext(), handler, tag, runnable);
         }
 
+        /** Builds a new NetworkAgent. */
+        public NetworkAgent newNetworkAgent(
+                @NonNull VcnContext vcnContext,
+                @NonNull String tag,
+                @NonNull NetworkCapabilities caps,
+                @NonNull LinkProperties lp,
+                @NonNull int score,
+                @NonNull NetworkAgentConfig nac,
+                @NonNull NetworkProvider provider,
+                @NonNull Runnable networkUnwantedCallback,
+                @NonNull Consumer<Integer> validationStatusCallback) {
+            return new NetworkAgent(
+                    vcnContext.getContext(),
+                    vcnContext.getLooper(),
+                    tag,
+                    caps,
+                    lp,
+                    score,
+                    nac,
+                    provider) {
+                @Override
+                public void onNetworkUnwanted() {
+                    networkUnwantedCallback.run();
+                }
+
+                @Override
+                public void onValidationStatus(int status, @Nullable Uri redirectUri) {
+                    validationStatusCallback.accept(status);
+                }
+            };
+        }
+
         /** Gets the elapsed real time since boot, in millis. */
         public long getElapsedRealTime() {
             return SystemClock.elapsedRealtime();
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 100d3ea..f5876fa 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -33,12 +33,6 @@
 import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND;
 import static android.net.NetworkPolicyManager.POLICY_NONE;
 import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
-import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
-import static android.net.NetworkPolicyManager.RULE_ALLOW_METERED;
-import static android.net.NetworkPolicyManager.RULE_NONE;
-import static android.net.NetworkPolicyManager.RULE_REJECT_ALL;
-import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
-import static android.net.NetworkPolicyManager.RULE_TEMPORARY_ALLOW_METERED;
 import static android.net.NetworkPolicyManager.uidPoliciesToString;
 import static android.net.NetworkPolicyManager.uidRulesToString;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
@@ -49,7 +43,6 @@
 import static android.net.NetworkTemplate.buildTemplateMobileAll;
 import static android.net.NetworkTemplate.buildTemplateWifi;
 import static android.net.TrafficStats.MB_IN_BYTES;
-import static android.os.Process.SYSTEM_UID;
 import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
 import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
@@ -141,7 +134,6 @@
 import android.util.ArrayMap;
 import android.util.DataUnit;
 import android.util.Log;
-import android.util.Pair;
 import android.util.Range;
 import android.util.RecurrenceRule;
 
@@ -1844,68 +1836,6 @@
         reset(mStatsService);
     }
 
-    /**
-     * Exhaustively test checkUidNetworkingBlocked to output the expected results based on external
-     * conditions.
-     */
-    @Test
-    public void testCheckUidNetworkingBlocked() {
-        final ArrayList<Pair<Boolean, Integer>> expectedBlockedStates = new ArrayList<>();
-
-        // Metered network. Data saver on.
-        expectedBlockedStates.add(new Pair<>(true, RULE_NONE));
-        expectedBlockedStates.add(new Pair<>(false, RULE_ALLOW_METERED));
-        expectedBlockedStates.add(new Pair<>(false, RULE_TEMPORARY_ALLOW_METERED));
-        expectedBlockedStates.add(new Pair<>(true, RULE_REJECT_METERED));
-        expectedBlockedStates.add(new Pair<>(true, RULE_ALLOW_ALL));
-        expectedBlockedStates.add(new Pair<>(true, RULE_REJECT_ALL));
-        verifyNetworkBlockedState(
-                true /* metered */, true /* backgroundRestricted */, expectedBlockedStates);
-        expectedBlockedStates.clear();
-
-        // Metered network. Data saver off.
-        expectedBlockedStates.add(new Pair<>(false, RULE_NONE));
-        expectedBlockedStates.add(new Pair<>(false, RULE_ALLOW_METERED));
-        expectedBlockedStates.add(new Pair<>(false, RULE_TEMPORARY_ALLOW_METERED));
-        expectedBlockedStates.add(new Pair<>(true, RULE_REJECT_METERED));
-        expectedBlockedStates.add(new Pair<>(false, RULE_ALLOW_ALL));
-        expectedBlockedStates.add(new Pair<>(true, RULE_REJECT_ALL));
-        verifyNetworkBlockedState(
-                true /* metered */, false /* backgroundRestricted */, expectedBlockedStates);
-        expectedBlockedStates.clear();
-
-        // Non-metered network. Data saver on.
-        expectedBlockedStates.add(new Pair<>(false, RULE_NONE));
-        expectedBlockedStates.add(new Pair<>(false, RULE_ALLOW_METERED));
-        expectedBlockedStates.add(new Pair<>(false, RULE_TEMPORARY_ALLOW_METERED));
-        expectedBlockedStates.add(new Pair<>(false, RULE_REJECT_METERED));
-        expectedBlockedStates.add(new Pair<>(false, RULE_ALLOW_ALL));
-        expectedBlockedStates.add(new Pair<>(true, RULE_REJECT_ALL));
-        verifyNetworkBlockedState(
-                false /* metered */, true /* backgroundRestricted */, expectedBlockedStates);
-
-        // Non-metered network. Data saver off. The result is the same as previous case since
-        // the network is blocked only for RULE_REJECT_ALL regardless of data saver.
-        verifyNetworkBlockedState(
-                false /* metered */, false /* backgroundRestricted */, expectedBlockedStates);
-        expectedBlockedStates.clear();
-    }
-
-    private void verifyNetworkBlockedState(boolean metered, boolean backgroundRestricted,
-            ArrayList<Pair<Boolean, Integer>> expectedBlockedStateForRules) {
-
-        for (Pair<Boolean, Integer> pair : expectedBlockedStateForRules) {
-            final boolean expectedResult = pair.first;
-            final int rule = pair.second;
-            assertEquals(formatBlockedStateError(UID_A, rule, metered, backgroundRestricted),
-                    expectedResult, mService.checkUidNetworkingBlocked(UID_A, rule,
-                            metered, backgroundRestricted));
-            assertFalse(formatBlockedStateError(SYSTEM_UID, rule, metered, backgroundRestricted),
-                    mService.checkUidNetworkingBlocked(SYSTEM_UID, rule, metered,
-                            backgroundRestricted));
-        }
-    }
-
     private void enableRestrictedMode(boolean enable) throws Exception {
         mService.mRestrictedNetworkingMode = enable;
         mService.updateRestrictedModeAllowlistUL();
diff --git a/telephony/java/android/telephony/ims/SipMessage.java b/telephony/java/android/telephony/ims/SipMessage.java
index ad6d73c..b529563 100644
--- a/telephony/java/android/telephony/ims/SipMessage.java
+++ b/telephony/java/android/telephony/ims/SipMessage.java
@@ -24,6 +24,7 @@
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.text.TextUtils;
 
 import com.android.internal.telephony.SipMessageParsingUtils;
 
@@ -60,14 +61,19 @@
      */
     public SipMessage(@NonNull String startLine, @NonNull String headerSection,
             @NonNull byte[] content) {
-        if (startLine == null || headerSection == null || content == null) {
-            throw new IllegalArgumentException("One or more null parameters entered");
-        }
+        Objects.requireNonNull(startLine, "Required parameter is null: startLine");
+        Objects.requireNonNull(headerSection, "Required parameter is null: headerSection");
+        Objects.requireNonNull(content, "Required parameter is null: content");
+
         mStartLine = startLine;
         mHeaderSection = headerSection;
         mContent = content;
 
         mViaBranchParam = SipMessageParsingUtils.getTransactionId(mHeaderSection);
+        if (TextUtils.isEmpty(mViaBranchParam)) {
+            throw new IllegalArgumentException("header section MUST contain a branch parameter "
+                    + "inside of the Via header.");
+        }
         mCallIdParam = SipMessageParsingUtils.getCallId(mHeaderSection);
     }
 
@@ -107,11 +113,9 @@
 
     /**
      * @return the branch parameter enclosed in the Via header key's value. See RFC 3261 section
-     * 20.42 for more information on the Via header. If {@code null}, then there was either no
-     * Via parameter found in this SIP message's headers or no branch parameter found in the
-     * Via header.
+     * 20.42 for more information on the Via header.
      */
-    public @Nullable String getViaBranchParameter() {
+    public @NonNull String getViaBranchParameter() {
         return mViaBranchParam;
     }
 
diff --git a/telephony/java/android/telephony/ims/aidl/SipDelegateAidlWrapper.java b/telephony/java/android/telephony/ims/aidl/SipDelegateAidlWrapper.java
index 739946b..5c9ec53 100644
--- a/telephony/java/android/telephony/ims/aidl/SipDelegateAidlWrapper.java
+++ b/telephony/java/android/telephony/ims/aidl/SipDelegateAidlWrapper.java
@@ -28,8 +28,6 @@
 import android.telephony.ims.SipDelegateManager;
 import android.telephony.ims.SipMessage;
 import android.telephony.ims.stub.SipDelegate;
-import android.text.TextUtils;
-import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.Set;
@@ -187,11 +185,6 @@
 
     private void notifyLocalMessageFailedToBeReceived(SipMessage m, int reason) {
         String transactionId = m.getViaBranchParameter();
-        if (TextUtils.isEmpty(transactionId)) {
-            Log.w(LOG_TAG, "failure to parse SipMessage.");
-            throw new IllegalArgumentException("Malformed SipMessage, can not determine "
-                    + "transaction ID.");
-        }
         SipDelegate d = mDelegate;
         if (d != null) {
             mExecutor.execute(() -> d.notifyMessageReceiveError(transactionId, reason));
diff --git a/telephony/java/android/telephony/ims/aidl/SipDelegateConnectionAidlWrapper.java b/telephony/java/android/telephony/ims/aidl/SipDelegateConnectionAidlWrapper.java
index 3cd2726..ad02fe5 100644
--- a/telephony/java/android/telephony/ims/aidl/SipDelegateConnectionAidlWrapper.java
+++ b/telephony/java/android/telephony/ims/aidl/SipDelegateConnectionAidlWrapper.java
@@ -28,7 +28,6 @@
 import android.telephony.ims.stub.DelegateConnectionMessageCallback;
 import android.telephony.ims.stub.DelegateConnectionStateCallback;
 import android.telephony.ims.stub.SipDelegate;
-import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -267,12 +266,6 @@
 
     private void notifyLocalMessageFailedToSend(SipMessage m, int reason) {
         String transactionId = m.getViaBranchParameter();
-        if (TextUtils.isEmpty(transactionId)) {
-            Log.w(LOG_TAG, "sendMessage detected a malformed SipMessage and can not get a "
-                    + "transaction ID.");
-            throw new IllegalArgumentException("Could not send SipMessage due to malformed header");
-        }
-        mExecutor.execute(() ->
-                mMessageCallback.onMessageSendFailure(transactionId, reason));
+        mExecutor.execute(() -> mMessageCallback.onMessageSendFailure(transactionId, reason));
     }
 }
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 4ad7136..43e6676 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server;
 
+import static android.net.ConnectivityManager.NetworkCallback;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -55,8 +57,10 @@
 import android.content.Intent;
 import android.net.ConnectivityManager;
 import android.net.LinkProperties;
+import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkCapabilities.Transport;
+import android.net.NetworkRequest;
 import android.net.TelephonyNetworkSpecifier;
 import android.net.vcn.IVcnStatusCallback;
 import android.net.vcn.IVcnUnderlyingNetworkPolicyListener;
@@ -258,6 +262,10 @@
 
         verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class));
         verify(mSubscriptionTracker).register();
+        verify(mConnMgr)
+                .registerNetworkCallback(
+                        eq(new NetworkRequest.Builder().clearCapabilities().build()),
+                        any(NetworkCallback.class));
     }
 
     @Test
@@ -529,17 +537,6 @@
     }
 
     @Test
-    public void testSetVcnConfigInSafeModeNotifiesStatusCallback() throws Exception {
-        setupSubscriptionAndStartVcn(TEST_SUBSCRIPTION_ID, TEST_UUID_2, false /* isActive */);
-        mVcnMgmtSvc.registerVcnStatusCallback(TEST_UUID_2, mMockStatusCallback, TEST_PACKAGE_NAME);
-        verify(mMockStatusCallback).onVcnStatusChanged(VcnManager.VCN_STATUS_CODE_SAFE_MODE);
-
-        mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
-
-        verify(mMockStatusCallback).onVcnStatusChanged(VcnManager.VCN_STATUS_CODE_ACTIVE);
-    }
-
-    @Test
     public void testClearVcnConfigRequiresNonSystemServer() throws Exception {
         doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
 
@@ -706,10 +703,8 @@
                 .checkLocationPermission(eq(TEST_PACKAGE_NAME), any(), eq(TEST_UID), any());
     }
 
-    private VcnUnderlyingNetworkPolicy startVcnAndGetPolicyForTransport(
-            int subId, ParcelUuid subGrp, boolean isVcnActive, int transport) {
-        setupSubscriptionAndStartVcn(subId, subGrp, isVcnActive);
-
+    private NetworkCapabilities.Builder getNetworkCapabilitiesBuilderForTransport(
+            int subId, int transport) {
         final NetworkCapabilities.Builder ncBuilder =
                 new NetworkCapabilities.Builder()
                         .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
@@ -718,7 +713,16 @@
             ncBuilder.setSubIds(Collections.singleton(subId));
         }
 
-        return mVcnMgmtSvc.getUnderlyingNetworkPolicy(ncBuilder.build(), new LinkProperties());
+        return ncBuilder;
+    }
+
+    private VcnUnderlyingNetworkPolicy startVcnAndGetPolicyForTransport(
+            int subId, ParcelUuid subGrp, boolean isVcnActive, int transport) {
+        setupSubscriptionAndStartVcn(subId, subGrp, isVcnActive);
+
+        return mVcnMgmtSvc.getUnderlyingNetworkPolicy(
+                getNetworkCapabilitiesBuilderForTransport(subId, transport).build(),
+                new LinkProperties());
     }
 
     @Test
@@ -780,6 +784,53 @@
                 true /* isRestricted */);
     }
 
+    private void setupTrackedCarrierWifiNetwork(NetworkCapabilities caps) {
+        mVcnMgmtSvc.systemReady();
+
+        final ArgumentCaptor<NetworkCallback> captor =
+                ArgumentCaptor.forClass(NetworkCallback.class);
+        verify(mConnMgr)
+                .registerNetworkCallback(
+                        eq(new NetworkRequest.Builder().clearCapabilities().build()),
+                        captor.capture());
+        captor.getValue().onCapabilitiesChanged(new Network(0), caps);
+    }
+
+    @Test
+    public void testGetUnderlyingNetworkPolicyVcnWifi_unrestrictingExistingNetworkRequiresRestart()
+            throws Exception {
+        final NetworkCapabilities existingNetworkCaps =
+                getNetworkCapabilitiesBuilderForTransport(TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI)
+                        .removeCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                        .build();
+        setupTrackedCarrierWifiNetwork(existingNetworkCaps);
+
+        // Trigger test without VCN instance alive; expect restart due to change of NOT_RESTRICTED
+        // immutable capability
+        final VcnUnderlyingNetworkPolicy policy =
+                mVcnMgmtSvc.getUnderlyingNetworkPolicy(
+                        getNetworkCapabilitiesBuilderForTransport(
+                                        TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI)
+                                .build(),
+                        new LinkProperties());
+        assertTrue(policy.isTeardownRequested());
+    }
+
+    @Test
+    public void testGetUnderlyingNetworkPolicyVcnWifi_restrictingExistingNetworkRequiresRestart()
+            throws Exception {
+        final NetworkCapabilities existingNetworkCaps =
+                getNetworkCapabilitiesBuilderForTransport(TEST_SUBSCRIPTION_ID, TRANSPORT_WIFI)
+                        .build();
+        setupTrackedCarrierWifiNetwork(existingNetworkCaps);
+
+        final VcnUnderlyingNetworkPolicy policy =
+                startVcnAndGetPolicyForTransport(
+                        TEST_SUBSCRIPTION_ID, TEST_UUID_2, false /* isActive */, TRANSPORT_WIFI);
+
+        assertTrue(policy.isTeardownRequested());
+    }
+
     @Test
     public void testGetUnderlyingNetworkPolicyNonVcnNetwork() throws Exception {
         setupSubscriptionAndStartVcn(TEST_SUBSCRIPTION_ID, TEST_UUID_1, true /* isActive */);
@@ -840,7 +891,9 @@
     }
 
     private void triggerVcnSafeMode(
-            @NonNull ParcelUuid subGroup, @NonNull TelephonySubscriptionSnapshot snapshot)
+            @NonNull ParcelUuid subGroup,
+            @NonNull TelephonySubscriptionSnapshot snapshot,
+            boolean isInSafeMode)
             throws Exception {
         verify(mMockDeps)
                 .newVcn(
@@ -851,22 +904,32 @@
                         mVcnCallbackCaptor.capture());
 
         VcnCallback vcnCallback = mVcnCallbackCaptor.getValue();
-        vcnCallback.onEnteredSafeMode();
+        vcnCallback.onSafeModeStatusChanged(isInSafeMode);
     }
 
-    @Test
-    public void testVcnEnteringSafeModeNotifiesPolicyListeners() throws Exception {
+    private void verifyVcnSafeModeChangesNotifiesPolicyListeners(boolean enterSafeMode)
+            throws Exception {
         TelephonySubscriptionSnapshot snapshot =
                 triggerSubscriptionTrackerCbAndGetSnapshot(Collections.singleton(TEST_UUID_1));
 
         mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener);
 
-        triggerVcnSafeMode(TEST_UUID_1, snapshot);
+        triggerVcnSafeMode(TEST_UUID_1, snapshot, enterSafeMode);
 
         verify(mMockPolicyListener).onPolicyChanged();
     }
 
-    private void triggerVcnStatusCallbackOnEnteredSafeMode(
+    @Test
+    public void testVcnEnteringSafeModeNotifiesPolicyListeners() throws Exception {
+        verifyVcnSafeModeChangesNotifiesPolicyListeners(true /* enterSafeMode */);
+    }
+
+    @Test
+    public void testVcnExitingSafeModeNotifiesPolicyListeners() throws Exception {
+        verifyVcnSafeModeChangesNotifiesPolicyListeners(false /* enterSafeMode */);
+    }
+
+    private void triggerVcnStatusCallbackOnSafeModeStatusChanged(
             @NonNull ParcelUuid subGroup,
             @NonNull String pkgName,
             int uid,
@@ -889,12 +952,13 @@
 
         mVcnMgmtSvc.registerVcnStatusCallback(subGroup, mMockStatusCallback, pkgName);
 
-        triggerVcnSafeMode(subGroup, snapshot);
+        triggerVcnSafeMode(subGroup, snapshot, true /* enterSafeMode */);
     }
 
     @Test
-    public void testVcnStatusCallbackOnEnteredSafeModeWithCarrierPrivileges() throws Exception {
-        triggerVcnStatusCallbackOnEnteredSafeMode(
+    public void testVcnStatusCallbackOnSafeModeStatusChangedWithCarrierPrivileges()
+            throws Exception {
+        triggerVcnStatusCallbackOnSafeModeStatusChanged(
                 TEST_UUID_1,
                 TEST_PACKAGE_NAME,
                 TEST_UID,
@@ -905,8 +969,9 @@
     }
 
     @Test
-    public void testVcnStatusCallbackOnEnteredSafeModeWithoutCarrierPrivileges() throws Exception {
-        triggerVcnStatusCallbackOnEnteredSafeMode(
+    public void testVcnStatusCallbackOnSafeModeStatusChangedWithoutCarrierPrivileges()
+            throws Exception {
+        triggerVcnStatusCallbackOnSafeModeStatusChanged(
                 TEST_UUID_1,
                 TEST_PACKAGE_NAME,
                 TEST_UID,
@@ -918,8 +983,9 @@
     }
 
     @Test
-    public void testVcnStatusCallbackOnEnteredSafeModeWithoutLocationPermission() throws Exception {
-        triggerVcnStatusCallbackOnEnteredSafeMode(
+    public void testVcnStatusCallbackOnSafeModeStatusChangedWithoutLocationPermission()
+            throws Exception {
+        triggerVcnStatusCallbackOnSafeModeStatusChanged(
                 TEST_UUID_1,
                 TEST_PACKAGE_NAME,
                 TEST_UID,
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 2fadd44..34c0018 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -32,6 +32,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.argThat;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -40,6 +41,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
+import android.net.ConnectivityManager;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
@@ -58,19 +61,29 @@
 import org.mockito.ArgumentCaptor;
 
 import java.io.IOException;
+import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
 
 /** Tests for VcnGatewayConnection.ConnectedState */
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
     private VcnIkeSession mIkeSession;
+    private NetworkAgent mNetworkAgent;
 
     @Before
     public void setUp() throws Exception {
         super.setUp();
 
+        mNetworkAgent = mock(NetworkAgent.class);
+        doReturn(mNetworkAgent)
+                .when(mDeps)
+                .newNetworkAgent(any(), any(), any(), any(), anyInt(), any(), any(), any(), any());
+
         mGatewayConnection.setUnderlyingNetwork(TEST_UNDERLYING_NETWORK_RECORD_1);
 
         mIkeSession = mGatewayConnection.buildIkeSession(TEST_UNDERLYING_NETWORK_RECORD_1.network);
@@ -159,21 +172,44 @@
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
     }
 
-    @Test
-    public void testChildOpenedRegistersNetwork() throws Exception {
-        // Verify scheduled but not canceled when entering ConnectedState
-        verifySafeModeTimeoutAlarmAndGetCallback(false /* expectCanceled */);
+    private void triggerChildOpened() {
+        triggerChildOpened(Collections.singletonList(TEST_INTERNAL_ADDR), TEST_DNS_ADDR);
+    }
 
+    private void triggerChildOpened(List<LinkAddress> internalAddresses, InetAddress dnsAddress) {
         final VcnChildSessionConfiguration mMockChildSessionConfig =
                 mock(VcnChildSessionConfiguration.class);
-        doReturn(Collections.singletonList(TEST_INTERNAL_ADDR))
-                .when(mMockChildSessionConfig)
-                .getInternalAddresses();
-        doReturn(Collections.singletonList(TEST_DNS_ADDR))
+        doReturn(internalAddresses).when(mMockChildSessionConfig).getInternalAddresses();
+        doReturn(Collections.singletonList(dnsAddress))
                 .when(mMockChildSessionConfig)
                 .getInternalDnsServers();
 
         getChildSessionCallback().onOpened(mMockChildSessionConfig);
+    }
+
+    private void triggerValidation(int status) {
+        final ArgumentCaptor<Consumer<Integer>> validationCallbackCaptor =
+                ArgumentCaptor.forClass(Consumer.class);
+        verify(mDeps)
+                .newNetworkAgent(
+                        any(),
+                        any(),
+                        any(),
+                        any(),
+                        anyInt(),
+                        any(),
+                        any(),
+                        any(),
+                        validationCallbackCaptor.capture());
+
+        validationCallbackCaptor.getValue().accept(status);
+    }
+
+    @Test
+    public void testChildOpenedRegistersNetwork() throws Exception {
+        // Verify scheduled but not canceled when entering ConnectedState
+        verifySafeModeTimeoutAlarmAndGetCallback(false /* expectCanceled */);
+        triggerChildOpened();
         mTestLooper.dispatchAll();
 
         assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState());
@@ -182,15 +218,20 @@
                 ArgumentCaptor.forClass(LinkProperties.class);
         final ArgumentCaptor<NetworkCapabilities> ncCaptor =
                 ArgumentCaptor.forClass(NetworkCapabilities.class);
-        verify(mConnMgr)
-                .registerNetworkAgent(
-                        any(),
-                        any(),
-                        lpCaptor.capture(),
+        verify(mDeps)
+                .newNetworkAgent(
+                        eq(mVcnContext),
+                        any(String.class),
                         ncCaptor.capture(),
+                        lpCaptor.capture(),
+                        anyInt(),
+                        argThat(nac -> nac.getLegacyType() == ConnectivityManager.TYPE_MOBILE),
                         any(),
                         any(),
-                        anyInt());
+                        any());
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+
         verify(mIpSecSvc)
                 .addAddressToTunnelInterface(
                         eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(TEST_INTERNAL_ADDR), any());
@@ -208,9 +249,78 @@
 
         // Now that Vcn Network is up, notify it as validated and verify the SafeMode alarm is
         // canceled
-        mGatewayConnection.mNetworkAgent.onValidationStatus(
-                NetworkAgent.VALIDATION_STATUS_VALID, null /* redirectUri */);
+        triggerValidation(NetworkAgent.VALIDATION_STATUS_VALID);
         verify(mSafeModeTimeoutAlarm).cancel();
+        assertFalse(mGatewayConnection.isInSafeMode());
+    }
+
+    @Test
+    public void testInternalAndDnsAddressesChanged() throws Exception {
+        final List<LinkAddress> startingInternalAddrs =
+                Arrays.asList(new LinkAddress[] {TEST_INTERNAL_ADDR, TEST_INTERNAL_ADDR_2});
+        triggerChildOpened(startingInternalAddrs, TEST_DNS_ADDR);
+        mTestLooper.dispatchAll();
+
+        for (LinkAddress addr : startingInternalAddrs) {
+            verify(mIpSecSvc)
+                    .addAddressToTunnelInterface(
+                            eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(addr), any());
+        }
+
+        verify(mDeps)
+                .newNetworkAgent(
+                        any(),
+                        any(),
+                        any(),
+                        argThat(
+                                lp ->
+                                        startingInternalAddrs.equals(lp.getLinkAddresses())
+                                                && Collections.singletonList(TEST_DNS_ADDR)
+                                                        .equals(lp.getDnsServers())),
+                        anyInt(),
+                        any(),
+                        any(),
+                        any(),
+                        any());
+
+        // Trigger another connection event, and verify that the addresses change
+        final List<LinkAddress> newInternalAddrs =
+                Arrays.asList(new LinkAddress[] {TEST_INTERNAL_ADDR_2, TEST_INTERNAL_ADDR_3});
+        triggerChildOpened(newInternalAddrs, TEST_DNS_ADDR_2);
+        mTestLooper.dispatchAll();
+
+        // Verify addresses on tunnel network added/removed
+        for (LinkAddress addr : newInternalAddrs) {
+            verify(mIpSecSvc)
+                    .addAddressToTunnelInterface(
+                            eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(addr), any());
+        }
+        verify(mIpSecSvc)
+                .removeAddressFromTunnelInterface(
+                        eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(TEST_INTERNAL_ADDR), any());
+
+        // TODO(b/184579891): Also verify link properties updated and sent when sendLinkProperties
+        // is mockable
+
+        // Verify that IpSecTunnelInterface only created once
+        verify(mIpSecSvc).createTunnelInterface(any(), any(), any(), any(), any());
+        verifyNoMoreInteractions(mIpSecSvc);
+    }
+
+    @Test
+    public void testSuccessfulConnectionExitsSafeMode() throws Exception {
+        verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent(
+                mGatewayConnection.mConnectedState);
+
+        assertTrue(mGatewayConnection.isInSafeMode());
+        assertFalse(mGatewayConnection.isQuitting());
+
+        triggerChildOpened();
+        mTestLooper.dispatchAll();
+
+        triggerValidation(NetworkAgent.VALIDATION_STATUS_VALID);
+
+        assertFalse(mGatewayConnection.isInSafeMode());
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
index 7afa449..bfe8c73 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectingStateTest.java
@@ -118,8 +118,9 @@
     }
 
     @Test
-    public void testSafeModeTimeoutNotifiesCallback() {
-        verifySafeModeTimeoutNotifiesCallback(mGatewayConnection.mConnectingState);
+    public void testSafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent() {
+        verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent(
+                mGatewayConnection.mConnectingState);
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
index 99feffd..9da8b45 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionDisconnectingStateTest.java
@@ -86,8 +86,9 @@
     }
 
     @Test
-    public void testSafeModeTimeoutNotifiesCallback() {
-        verifySafeModeTimeoutNotifiesCallback(mGatewayConnection.mDisconnectingState);
+    public void testSafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent() {
+        verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent(
+                mGatewayConnection.mDisconnectingState);
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
index 85a0277..6dbf7d5 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionRetryTimeoutStateTest.java
@@ -96,8 +96,9 @@
     }
 
     @Test
-    public void testSafeModeTimeoutNotifiesCallback() {
-        verifySafeModeTimeoutNotifiesCallback(mGatewayConnection.mRetryTimeoutState);
+    public void testSafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent() {
+        verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent(
+                mGatewayConnection.mRetryTimeoutState);
     }
 
     @Test
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index a660735..c5ed8f6 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -21,6 +21,8 @@
 import static com.android.server.vcn.VcnTestUtils.setupIpSecManager;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
@@ -42,6 +44,7 @@
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
+import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
 import android.net.ipsec.ike.ChildSessionCallback;
 import android.net.ipsec.ike.IkeSessionCallback;
@@ -71,8 +74,14 @@
     protected static final ParcelUuid TEST_SUB_GRP = new ParcelUuid(UUID.randomUUID());
     protected static final InetAddress TEST_DNS_ADDR =
             InetAddresses.parseNumericAddress("2001:DB8:0:1::");
+    protected static final InetAddress TEST_DNS_ADDR_2 =
+            InetAddresses.parseNumericAddress("2001:DB8:0:2::");
     protected static final LinkAddress TEST_INTERNAL_ADDR =
-            new LinkAddress(InetAddresses.parseNumericAddress("2001:DB8:0:2::"), 64);
+            new LinkAddress(InetAddresses.parseNumericAddress("2001:DB8:1:1::"), 64);
+    protected static final LinkAddress TEST_INTERNAL_ADDR_2 =
+            new LinkAddress(InetAddresses.parseNumericAddress("2001:DB8:1:2::"), 64);
+    protected static final LinkAddress TEST_INTERNAL_ADDR_3 =
+            new LinkAddress(InetAddresses.parseNumericAddress("2001:DB8:1:3::"), 64);
 
     protected static final int TEST_IPSEC_SPI_VALUE = 0x1234;
     protected static final int TEST_IPSEC_SPI_RESOURCE_ID = 1;
@@ -267,7 +276,12 @@
                 expectCanceled);
     }
 
-    protected void verifySafeModeTimeoutNotifiesCallback(@NonNull State expectedState) {
+    protected void verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent(
+            @NonNull State expectedState) {
+        // Set a NetworkAgent, and expect it to be unregistered and cleared
+        final NetworkAgent mockNetworkAgent = mock(NetworkAgent.class);
+        mGatewayConnection.setNetworkAgent(mockNetworkAgent);
+
         // SafeMode timer starts when VcnGatewayConnection exits DisconnectedState (the initial
         // state)
         final Runnable delayedEvent =
@@ -275,7 +289,11 @@
         delayedEvent.run();
         mTestLooper.dispatchAll();
 
-        verify(mGatewayStatusCallback).onEnteredSafeMode();
+        verify(mGatewayStatusCallback).onSafeModeStatusChanged();
         assertEquals(expectedState, mGatewayConnection.getCurrentState());
+        assertTrue(mGatewayConnection.isInSafeMode());
+
+        verify(mockNetworkAgent).unregister();
+        assertNull(mGatewayConnection.getNetworkAgent());
     }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/VcnTest.java b/tests/vcn/java/com/android/server/vcn/VcnTest.java
index 540be38..90eb75e 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnTest.java
@@ -19,14 +19,13 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE;
-import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE;
 import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.argThat;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
@@ -54,7 +53,6 @@
 import org.mockito.ArgumentCaptor;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
@@ -136,6 +134,7 @@
     private void startVcnGatewayWithCapabilities(
             NetworkRequestListener requestListener, int... netCapabilities) {
         final NetworkRequest.Builder requestBuilder = new NetworkRequest.Builder();
+        requestBuilder.addTransportType(TRANSPORT_CELLULAR);
         for (final int netCapability : netCapabilities) {
             requestBuilder.addCapability(netCapability);
         }
@@ -160,8 +159,7 @@
         mTestLooper.dispatchAll();
 
         for (final VcnGatewayConnection gateway : gatewayConnections) {
-            verify(gateway, status == VCN_STATUS_CODE_ACTIVE ? times(1) : never())
-                    .updateSubscriptionSnapshot(eq(updatedSnapshot));
+            verify(gateway).updateSubscriptionSnapshot(eq(updatedSnapshot));
         }
     }
 
@@ -202,32 +200,53 @@
 
     private void verifySafeMode(
             NetworkRequestListener requestListener,
-            Set<VcnGatewayConnection> expectedGatewaysTornDown) {
-        assertEquals(VCN_STATUS_CODE_SAFE_MODE, mVcn.getStatus());
-        for (final VcnGatewayConnection gatewayConnection : expectedGatewaysTornDown) {
-            verify(gatewayConnection).teardownAsynchronously();
+            Set<VcnGatewayConnection> activeGateways,
+            boolean expectInSafeMode) {
+        for (VcnGatewayConnection gatewayConnection : activeGateways) {
+            verify(gatewayConnection, never()).teardownAsynchronously();
         }
-        verify(mVcnNetworkProvider).unregisterListener(requestListener);
-        verify(mVcnCallback).onEnteredSafeMode();
+
+        assertEquals(
+                expectInSafeMode ? VCN_STATUS_CODE_SAFE_MODE : VCN_STATUS_CODE_ACTIVE,
+                mVcn.getStatus());
+        verify(mVcnCallback).onSafeModeStatusChanged(expectInSafeMode);
     }
 
     @Test
-    public void testGatewayEnteringSafeModeNotifiesVcn() {
+    public void testGatewayEnteringAndExitingSafeModeNotifiesVcn() {
         final NetworkRequestListener requestListener = verifyAndGetRequestListener();
         final Set<VcnGatewayConnection> gatewayConnections =
                 startGatewaysAndGetGatewayConnections(requestListener);
 
-        // Doesn't matter which callback this gets - any Gateway entering Safemode should shut down
-        // all Gateways
+        // Doesn't matter which callback this gets, or which VCN is in safe mode - any Gateway
+        // entering Safemode should trigger safe mode
         final VcnGatewayStatusCallback statusCallback = mGatewayStatusCallbackCaptor.getValue();
-        statusCallback.onEnteredSafeMode();
+        final VcnGatewayConnection gatewayConnection = gatewayConnections.iterator().next();
+
+        doReturn(true).when(gatewayConnection).isInSafeMode();
+        statusCallback.onSafeModeStatusChanged();
         mTestLooper.dispatchAll();
 
-        verifySafeMode(requestListener, gatewayConnections);
+        verifySafeMode(requestListener, gatewayConnections, true /* expectInSafeMode */);
+
+        // Verify that when all GatewayConnections exit safe mode, the VCN also exits safe mode
+        doReturn(false).when(gatewayConnection).isInSafeMode();
+        statusCallback.onSafeModeStatusChanged();
+        mTestLooper.dispatchAll();
+
+        verifySafeMode(requestListener, gatewayConnections, false /* expectInSafeMode */);
+
+        // Re-trigger, verify safe mode callback does not get fired again for identical state
+        statusCallback.onSafeModeStatusChanged();
+        mTestLooper.dispatchAll();
+
+        // Expect only once still; from above.
+        verify(mVcnCallback).onSafeModeStatusChanged(false);
     }
 
-    @Test
-    public void testGatewayQuit() {
+    private void verifyGatewayQuit(int status) {
+        mVcn.setStatus(status);
+
         final NetworkRequestListener requestListener = verifyAndGetRequestListener();
         final Set<VcnGatewayConnection> gatewayConnections =
                 new ArraySet<>(startGatewaysAndGetGatewayConnections(requestListener));
@@ -240,7 +259,7 @@
         assertEquals(1, mVcn.getVcnGatewayConnections().size());
         verify(mVcnNetworkProvider).resendAllRequests(requestListener);
 
-        // Verify that the VcnGatewayConnection is restarted
+        // Verify that the VcnGatewayConnection is restarted if a request exists for it
         triggerVcnRequestListeners(requestListener);
         mTestLooper.dispatchAll();
         assertEquals(2, mVcn.getVcnGatewayConnections().size());
@@ -254,21 +273,13 @@
     }
 
     @Test
-    public void testGatewayQuitWhileInactive() {
-        final NetworkRequestListener requestListener = verifyAndGetRequestListener();
-        final Set<VcnGatewayConnection> gatewayConnections =
-                new ArraySet<>(startGatewaysAndGetGatewayConnections(requestListener));
+    public void testGatewayQuitReevaluatesRequests() {
+        verifyGatewayQuit(VCN_STATUS_CODE_ACTIVE);
+    }
 
-        mVcn.teardownAsynchronously();
-        mTestLooper.dispatchAll();
-
-        final VcnGatewayStatusCallback statusCallback = mGatewayStatusCallbackCaptor.getValue();
-        statusCallback.onQuit();
-        mTestLooper.dispatchAll();
-
-        // Verify that the VCN requests the networkRequests be resent
-        assertEquals(1, mVcn.getVcnGatewayConnections().size());
-        verify(mVcnNetworkProvider, never()).resendAllRequests(requestListener);
+    @Test
+    public void testGatewayQuitReevaluatesRequestsInSafeMode() {
+        verifyGatewayQuit(VCN_STATUS_CODE_SAFE_MODE);
     }
 
     @Test
@@ -298,49 +309,4 @@
         verify(removedGatewayConnection).teardownAsynchronously();
         verify(mVcnNetworkProvider).resendAllRequests(requestListener);
     }
-
-    @Test
-    public void testUpdateConfigExitsSafeMode() {
-        final NetworkRequestListener requestListener = verifyAndGetRequestListener();
-        final Set<VcnGatewayConnection> gatewayConnections =
-                new ArraySet<>(startGatewaysAndGetGatewayConnections(requestListener));
-
-        final VcnGatewayStatusCallback statusCallback = mGatewayStatusCallbackCaptor.getValue();
-        statusCallback.onEnteredSafeMode();
-        mTestLooper.dispatchAll();
-        verifySafeMode(requestListener, gatewayConnections);
-
-        doAnswer(invocation -> {
-            final NetworkRequestListener listener = invocation.getArgument(0);
-            triggerVcnRequestListeners(listener);
-            return null;
-        }).when(mVcnNetworkProvider).registerListener(eq(requestListener));
-
-        mVcn.updateConfig(mConfig);
-        mTestLooper.dispatchAll();
-
-        // Registered on start, then re-registered with new configs
-        verify(mVcnNetworkProvider, times(2)).registerListener(eq(requestListener));
-        assertEquals(VCN_STATUS_CODE_ACTIVE, mVcn.getStatus());
-        for (final int[] caps : TEST_CAPS) {
-            // Expect each gateway connection created only on initial startup
-            verify(mDeps)
-                    .newVcnGatewayConnection(
-                            eq(mVcnContext),
-                            eq(TEST_SUB_GROUP),
-                            eq(mSubscriptionSnapshot),
-                            argThat(config -> Arrays.equals(caps, config.getExposedCapabilities())),
-                            any());
-        }
-    }
-
-    @Test
-    public void testIgnoreNetworkRequestWhileInactive() {
-        mVcn.setStatus(VCN_STATUS_CODE_INACTIVE);
-
-        final NetworkRequestListener requestListener = verifyAndGetRequestListener();
-        triggerVcnRequestListeners(requestListener);
-
-        verify(mDeps, never()).newVcnGatewayConnection(any(), any(), any(), any(), any());
-    }
 }